Merge branch 'dev' into switch-video-view

pull/792/head
ryanzhao 2 years ago
commit e261678d6d

@ -11,7 +11,7 @@ abstract_target 'GlobalDependencies' do
# FIXME: If https://github.com/jedisct1/swift-sodium/pull/249 gets resolved then revert this back to the standard pod
pod 'Sodium', :git => 'https://github.com/oxen-io/session-ios-swift-sodium.git', branch: 'session-build'
pod 'GRDB.swift/SQLCipher'
pod 'SQLCipher', '~> 4.5.0' # FIXME: Version 4.5.2 is crashing when access DB settings
pod 'SQLCipher', '~> 4.5.3'
# FIXME: We want to remove this once it's been long enough since the migration to GRDB
pod 'YapDatabase/SQLCipher', :git => 'https://github.com/oxen-io/session-ios-yap-database.git', branch: 'signal-release'
@ -105,6 +105,9 @@ post_install do |installer|
enable_whole_module_optimization_for_crypto_swift(installer)
set_minimum_deployment_target(installer)
enable_fts5_support(installer)
#FIXME: Remove this workaround once an official fix is released (hopefully Cocoapods 1.12.1)
xcode_14_3_workaround(installer)
end
def enable_whole_module_optimization_for_crypto_swift(installer)
@ -135,3 +138,12 @@ def enable_fts5_support(installer)
end
end
end
# Workaround for Xcode 14.3:
# Sourced from https://github.com/flutter/flutter/issues/123852#issuecomment-1493232105
def xcode_14_3_workaround(installer)
system('sed -i \'\' \'44s/readlink/readlink -f/\' \'Pods/Target Support Files/Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionMessagingKit-SessionMessagingKitTests/Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionMessagingKit-SessionMessagingKitTests-frameworks.sh\'')
system('sed -i \'\' \'44s/readlink/readlink -f/\' \'Pods/Target Support Files/Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionUtilitiesKit-SessionUtilitiesKitTests/Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionUtilitiesKit-SessionUtilitiesKitTests-frameworks.sh\'')
system('sed -i \'\' \'44s/readlink/readlink -f/\' \'Pods/Target Support Files/Pods-GlobalDependencies-Session/Pods-GlobalDependencies-Session-frameworks.sh\'')
system('sed -i \'\' \'44s/readlink/readlink -f/\' \'Pods/Target Support Files/Pods-GlobalDependencies-Session-SessionTests/Pods-GlobalDependencies-Session-SessionTests-frameworks.sh\'')
end

@ -14,9 +14,9 @@ PODS:
- AFNetworking/Serialization (4.0.1)
- AFNetworking/UIKit (4.0.1):
- AFNetworking/NSURLSession
- CocoaLumberjack (3.7.4):
- CocoaLumberjack/Core (= 3.7.4)
- CocoaLumberjack/Core (3.7.4)
- CocoaLumberjack (3.8.0):
- CocoaLumberjack/Core (= 3.8.0)
- CocoaLumberjack/Core (3.8.0)
- CryptoSwift (1.4.2)
- Curve25519Kit (2.1.0):
- CocoaLumberjack
@ -27,7 +27,7 @@ PODS:
- DifferenceKit/Core (1.3.0)
- DifferenceKit/UIKitExtension (1.3.0):
- DifferenceKit/Core
- GRDB.swift/SQLCipher (6.1.0):
- GRDB.swift/SQLCipher (6.10.1):
- SQLCipher (>= 3.4.2)
- libwebp (1.2.1):
- libwebp/demux (= 1.2.1)
@ -61,10 +61,10 @@ PODS:
- OpenSSL-Universal
- SocketRocket (0.5.1)
- Sodium (0.9.1)
- SQLCipher (4.5.0):
- SQLCipher/standard (= 4.5.0)
- SQLCipher/common (4.5.0)
- SQLCipher/standard (4.5.0):
- SQLCipher (4.5.3):
- SQLCipher/standard (= 4.5.3)
- SQLCipher/common (4.5.3)
- SQLCipher/standard (4.5.3):
- SQLCipher/common
- SwiftProtobuf (1.5.0)
- WebRTC-lib (96.0.0)
@ -154,7 +154,7 @@ DEPENDENCIES:
- SignalCoreKit (from `https://github.com/oxen-io/session-ios-core-kit`, branch `session-version`)
- SocketRocket (~> 0.5.1)
- Sodium (from `https://github.com/oxen-io/session-ios-swift-sodium.git`, branch `session-build`)
- SQLCipher (~> 4.5.0)
- SQLCipher (~> 4.5.3)
- SwiftProtobuf (~> 1.5.0)
- WebRTC-lib
- YapDatabase/SQLCipher (from `https://github.com/oxen-io/session-ios-yap-database.git`, branch `signal-release`)
@ -201,7 +201,7 @@ EXTERNAL SOURCES:
CHECKOUT OPTIONS:
Curve25519Kit:
:commit: b79c2ace600bfd3784e9c33cf1f254b121312edc
:commit: ee1bc83e61d9d672105eed85a4b8fbaec3d376f5
:git: https://github.com/oxen-io/session-ios-curve-25519-kit.git
SignalCoreKit:
:commit: 4590c2737a2b5dc0ef4ace9f9019b581caccc1de
@ -217,12 +217,12 @@ CHECKOUT OPTIONS:
:git: https://github.com/signalapp/YYImage
SPEC CHECKSUMS:
AFNetworking: 7864c38297c79aaca1500c33288e429c3451fdce
CocoaLumberjack: 543c79c114dadc3b1aba95641d8738b06b05b646
AFNetworking: 3bd23d814e976cd148d7d44c3ab78017b744cd58
CocoaLumberjack: 78abfb691154e2a9df8ded4350d504ee19d90732
CryptoSwift: a532e74ed010f8c95f611d00b8bbae42e9fe7c17
Curve25519Kit: e63f9859ede02438ae3defc5e1a87e09d1ec7ee6
DifferenceKit: ab185c4d7f9cef8af3fcf593e5b387fb81e999ca
GRDB.swift: 611778a5e113385373baeb3e2ce474887d1aadb7
GRDB.swift: 1cc67278f1a9878d6eb1b849485518112b79cab7
libwebp: 98a37e597e40bfdb4c911fc98f2c53d0b12d05fc
Nimble: 5316ef81a170ce87baf72dd961f22f89a602ff84
NVActivityIndicatorView: 1f6c5687f1171810aa27a3296814dc2d7dec3667
@ -235,13 +235,13 @@ SPEC CHECKSUMS:
SignalCoreKit: 1fbd8732163ef76de16cd1107d1fa3684b607e5d
SocketRocket: d57c7159b83c3c6655745cd15302aa24b6bae531
Sodium: a7d42cb46e789d2630fa552d35870b416ed055ae
SQLCipher: 98dc22f27c0b1790d39e710d440f22a466ebdb59
SQLCipher: 57fa9f863fa4a3ed9dd3c90ace52315db8c0fdca
SwiftProtobuf: 241400280f912735c1e1b9fe675fdd2c6c4d42e2
WebRTC-lib: 508fe02efa0c1a3a8867082a77d24c9be5d29aeb
YapDatabase: b418a4baa6906e8028748938f9159807fd039af4
YYImage: f1ddd15ac032a58b78bbed1e012b50302d318331
ZXingObjC: fdbb269f25dd2032da343e06f10224d62f537bdb
PODFILE CHECKSUM: 7452ce88370eadd58d21fdf6a4c4945d6554ee95
PODFILE CHECKSUM: e9443a8235dbff1fc342aa9bf08bbc66923adf68
COCOAPODS: 1.11.3

@ -109,10 +109,12 @@
7B1D74AA27BCC16E0030B423 /* NSENotificationPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B1D74A927BCC16E0030B423 /* NSENotificationPresenter.swift */; };
7B1D74AC27BDE7510030B423 /* Promise+Timeout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B1D74AB27BDE7510030B423 /* Promise+Timeout.swift */; };
7B1D74B027C365960030B423 /* Timer+MainThread.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B1D74AF27C365960030B423 /* Timer+MainThread.swift */; };
7B2E985829AC227C001792D7 /* UIContextualAction+Theming.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B2E985729AC227C001792D7 /* UIContextualAction+Theming.swift */; };
7B46AAAF28766DF4001AF2DC /* AllMediaViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B46AAAE28766DF4001AF2DC /* AllMediaViewController.swift */; };
7B4C75CB26B37E0F0000AC89 /* UnsendRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B4C75CA26B37E0F0000AC89 /* UnsendRequest.swift */; };
7B4C75CD26BB92060000AC89 /* DeletedMessageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B4C75CC26BB92060000AC89 /* DeletedMessageView.swift */; };
7B50D64D28AC7CF80086CCEC /* silence.aiff in Resources */ = {isa = PBXBuildFile; fileRef = 7B50D64C28AC7CF80086CCEC /* silence.aiff */; };
7B521E0A29BFF84400C3C36A /* GroupLeavingJob.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B521E0929BFF84400C3C36A /* GroupLeavingJob.swift */; };
7B7037432834B81F000DCF35 /* ReactionContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B7037422834B81F000DCF35 /* ReactionContainerView.swift */; };
7B7037452834BCC0000DCF35 /* ReactionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B7037442834BCC0000DCF35 /* ReactionView.swift */; };
7B7CB18E270D066F0079FF93 /* IncomingCallBanner.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B7CB18D270D066F0079FF93 /* IncomingCallBanner.swift */; };
@ -122,6 +124,7 @@
7B81682828B310D50069F315 /* _007_HomeQueryOptimisationIndexes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B81682728B310D50069F315 /* _007_HomeQueryOptimisationIndexes.swift */; };
7B81682A28B6F1420069F315 /* ReactionResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B81682928B6F1420069F315 /* ReactionResponse.swift */; };
7B81682C28B72F480069F315 /* PendingChange.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B81682B28B72F480069F315 /* PendingChange.swift */; };
7B89FF4629C016E300C4C708 /* _012_AddFTSIfNeeded.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B89FF4529C016E300C4C708 /* _012_AddFTSIfNeeded.swift */; };
7B8C44C528B49DDA00FBE25F /* NewConversationVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B8C44C428B49DDA00FBE25F /* NewConversationVC.swift */; };
7B8D5FC428332600008324D9 /* VisibleMessage+Reaction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B8D5FC328332600008324D9 /* VisibleMessage+Reaction.swift */; };
7B93D06A27CF173D00811CB6 /* MessageRequestsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B93D06927CF173D00811CB6 /* MessageRequestsViewController.swift */; };
@ -1177,10 +1180,12 @@
7B1D74AB27BDE7510030B423 /* Promise+Timeout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Promise+Timeout.swift"; sourceTree = "<group>"; };
7B1D74AF27C365960030B423 /* Timer+MainThread.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Timer+MainThread.swift"; sourceTree = "<group>"; };
7B2DB2AD26F1B0FF0035B509 /* si */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = si; path = si.lproj/Localizable.strings; sourceTree = "<group>"; };
7B2E985729AC227C001792D7 /* UIContextualAction+Theming.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIContextualAction+Theming.swift"; sourceTree = "<group>"; };
7B46AAAE28766DF4001AF2DC /* AllMediaViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AllMediaViewController.swift; sourceTree = "<group>"; };
7B4C75CA26B37E0F0000AC89 /* UnsendRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnsendRequest.swift; sourceTree = "<group>"; };
7B4C75CC26BB92060000AC89 /* DeletedMessageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeletedMessageView.swift; sourceTree = "<group>"; };
7B50D64C28AC7CF80086CCEC /* silence.aiff */ = {isa = PBXFileReference; lastKnownFileType = audio.aiff; path = silence.aiff; sourceTree = "<group>"; };
7B521E0929BFF84400C3C36A /* GroupLeavingJob.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupLeavingJob.swift; sourceTree = "<group>"; };
7B7037422834B81F000DCF35 /* ReactionContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReactionContainerView.swift; sourceTree = "<group>"; };
7B7037442834BCC0000DCF35 /* ReactionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReactionView.swift; sourceTree = "<group>"; };
7B7CB18D270D066F0079FF93 /* IncomingCallBanner.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IncomingCallBanner.swift; sourceTree = "<group>"; };
@ -1190,6 +1195,7 @@
7B81682728B310D50069F315 /* _007_HomeQueryOptimisationIndexes.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = _007_HomeQueryOptimisationIndexes.swift; sourceTree = "<group>"; };
7B81682928B6F1420069F315 /* ReactionResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReactionResponse.swift; sourceTree = "<group>"; };
7B81682B28B72F480069F315 /* PendingChange.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PendingChange.swift; sourceTree = "<group>"; };
7B89FF4529C016E300C4C708 /* _012_AddFTSIfNeeded.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = _012_AddFTSIfNeeded.swift; sourceTree = "<group>"; };
7B8C44C428B49DDA00FBE25F /* NewConversationVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewConversationVC.swift; sourceTree = "<group>"; };
7B8D5FC328332600008324D9 /* VisibleMessage+Reaction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "VisibleMessage+Reaction.swift"; sourceTree = "<group>"; };
7B93D06927CF173D00811CB6 /* MessageRequestsViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageRequestsViewController.swift; sourceTree = "<group>"; };
@ -2816,6 +2822,7 @@
B885D5F52334A32100EE0D8E /* UIView+Constraints.swift */,
C33100272559000A00070591 /* UIView+Utilities.swift */,
FD71161F28D97ABC00B47552 /* UIImage+Tinting.swift */,
7B2E985729AC227C001792D7 /* UIContextualAction+Theming.swift */,
);
path = Utilities;
sourceTree = "<group>";
@ -3547,6 +3554,7 @@
7BAA7B6528D2DE4700AE1489 /* _009_OpenGroupPermission.swift */,
FD7115F128C6CB3900B47552 /* _010_AddThreadIdToFTS.swift */,
FD432431299C6933008A0213 /* _011_AddPendingReadReceipts.swift */,
7B89FF4529C016E300C4C708 /* _012_AddFTSIfNeeded.swift */,
);
path = Migrations;
sourceTree = "<group>";
@ -4133,6 +4141,7 @@
FDF0B74E28079E5E004C14C5 /* SendReadReceiptsJob.swift */,
C352A348255781F400338F3E /* AttachmentDownloadJob.swift */,
C352A35A2557824E00338F3E /* AttachmentUploadJob.swift */,
7B521E0929BFF84400C3C36A /* GroupLeavingJob.swift */,
);
path = Types;
sourceTree = "<group>";
@ -5139,6 +5148,7 @@
FD37E9D528A1FCE8003AE748 /* Theme+OceanLight.swift in Sources */,
FD37E9C828A1D73F003AE748 /* Theme+Colors.swift in Sources */,
FD37EA0128A60473003AE748 /* UIKit+Theme.swift in Sources */,
7B2E985829AC227C001792D7 /* UIContextualAction+Theming.swift in Sources */,
FD37E9CF28A1EB1B003AE748 /* Theme.swift in Sources */,
C331FFB92558FA8D00070591 /* UIView+Constraints.swift in Sources */,
FD37E9F628A5F106003AE748 /* Configuration.swift in Sources */,
@ -5397,11 +5407,13 @@
buildActionMask = 2147483647;
files = (
7B81682828B310D50069F315 /* _007_HomeQueryOptimisationIndexes.swift in Sources */,
7B89FF4629C016E300C4C708 /* _012_AddFTSIfNeeded.swift in Sources */,
FD245C52285065D500B966DD /* SignalAttachment.swift in Sources */,
B8856D08256F10F1001CE70E /* DeviceSleepManager.swift in Sources */,
C3471F4C25553AB000297E91 /* MessageReceiver+Decryption.swift in Sources */,
FD245C672850665E00B966DD /* AttachmentDownloadJob.swift in Sources */,
C300A5D32554B05A00555489 /* TypingIndicator.swift in Sources */,
7B521E0A29BFF84400C3C36A /* GroupLeavingJob.swift in Sources */,
FD09799927FFC1A300936362 /* Attachment.swift in Sources */,
FD245C5F2850662200B966DD /* OWSWindowManager.m in Sources */,
C3471ECB2555356A00297E91 /* MessageSender+Encryption.swift in Sources */,
@ -6040,7 +6052,7 @@
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CODE_SIGN_STYLE = Automatic;
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 395;
CURRENT_PROJECT_VERSION = 399;
DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = SUQ8J2PCT7;
FRAMEWORK_SEARCH_PATHS = "$(inherited)";
@ -6065,7 +6077,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 2.2.8;
MARKETING_VERSION = 2.2.10;
MTL_ENABLE_DEBUG_INFO = YES;
PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger.ShareExtension";
PRODUCT_NAME = "$(TARGET_NAME)";
@ -6113,7 +6125,7 @@
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CODE_SIGN_STYLE = Automatic;
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 395;
CURRENT_PROJECT_VERSION = 399;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = SUQ8J2PCT7;
ENABLE_NS_ASSERTIONS = NO;
@ -6143,7 +6155,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 2.2.8;
MARKETING_VERSION = 2.2.10;
MTL_ENABLE_DEBUG_INFO = NO;
PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger.ShareExtension";
PRODUCT_NAME = "$(TARGET_NAME)";
@ -6179,7 +6191,7 @@
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CODE_SIGN_STYLE = Automatic;
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 395;
CURRENT_PROJECT_VERSION = 399;
DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = SUQ8J2PCT7;
FRAMEWORK_SEARCH_PATHS = "$(inherited)";
@ -6202,7 +6214,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 2.2.8;
MARKETING_VERSION = 2.2.10;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger.NotificationServiceExtension";
@ -6253,7 +6265,7 @@
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CODE_SIGN_STYLE = Automatic;
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 395;
CURRENT_PROJECT_VERSION = 399;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = SUQ8J2PCT7;
ENABLE_NS_ASSERTIONS = NO;
@ -6281,7 +6293,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 2.2.8;
MARKETING_VERSION = 2.2.10;
MTL_ENABLE_DEBUG_INFO = NO;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger.NotificationServiceExtension";
@ -7181,7 +7193,7 @@
CODE_SIGN_ENTITLEMENTS = Session/Meta/Signal.entitlements;
CODE_SIGN_IDENTITY = "iPhone Developer";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CURRENT_PROJECT_VERSION = 395;
CURRENT_PROJECT_VERSION = 399;
DEVELOPMENT_TEAM = SUQ8J2PCT7;
FRAMEWORK_SEARCH_PATHS = (
"$(inherited)",
@ -7220,7 +7232,7 @@
"$(SRCROOT)",
);
LLVM_LTO = NO;
MARKETING_VERSION = 2.2.8;
MARKETING_VERSION = 2.2.10;
OTHER_LDFLAGS = "$(inherited)";
OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\"";
PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger";
@ -7253,7 +7265,7 @@
CODE_SIGN_ENTITLEMENTS = Session/Meta/Signal.entitlements;
CODE_SIGN_IDENTITY = "iPhone Developer";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CURRENT_PROJECT_VERSION = 395;
CURRENT_PROJECT_VERSION = 399;
DEVELOPMENT_TEAM = SUQ8J2PCT7;
FRAMEWORK_SEARCH_PATHS = (
"$(inherited)",
@ -7292,7 +7304,7 @@
"$(SRCROOT)",
);
LLVM_LTO = NO;
MARKETING_VERSION = 2.2.8;
MARKETING_VERSION = 2.2.10;
OTHER_LDFLAGS = "$(inherited)";
PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger";
PRODUCT_NAME = Session;

@ -451,7 +451,12 @@ final class EditClosedGroupVC: BaseVC, UITableViewDataSource, UITableViewDelegat
Storage.shared
.writeAsync { db in
if !updatedMemberIds.contains(userPublicKey) {
return try MessageSender.leave(db, groupPublicKey: threadId)
try MessageSender.leave(
db,
groupPublicKey: threadId,
deleteThread: false
)
return Promise.value(())
}
return try MessageSender.update(

@ -136,7 +136,8 @@ extension ContextMenuVC {
switch cellViewModel.variant {
case .standardIncomingDeleted, .infoCall,
.infoScreenshotNotification, .infoMediaSavedNotification,
.infoClosedGroupCreated, .infoClosedGroupUpdated, .infoClosedGroupCurrentUserLeft,
.infoClosedGroupCreated, .infoClosedGroupUpdated,
.infoClosedGroupCurrentUserLeft, .infoClosedGroupCurrentUserLeaving, .infoClosedGroupCurrentUserErrorLeaving,
.infoMessageRequestAccepted, .infoDisappearingMessagesUpdate:
// Let the user delete info messages and unsent messages
return [ Action.delete(cellViewModel, delegate) ]

@ -1611,7 +1611,19 @@ extension ConversationVC:
quotedAttachment.downloadUrl == Attachment.nonMediaQuoteFileId,
let quotedInteraction = try? quote.originalInteraction.fetchOne(db)
{
let attachment = try? quotedInteraction.attachments.fetchAll(db).first
let attachment: Attachment? = {
if let attachment = try? quotedInteraction.attachments.fetchOne(db) {
return attachment
}
if
let linkPreview = try? quotedInteraction.linkPreview.fetchOne(db),
let linkPreviewAttachment = try? linkPreview.attachment.fetchOne(db)
{
return linkPreviewAttachment
}
return nil
}()
try quote.with(
attachmentId: attachment?.cloneAsQuoteThumbnail()?.inserted(db).id
).update(db)
@ -1694,7 +1706,8 @@ extension ConversationVC:
switch cellViewModel.variant {
case .standardIncomingDeleted, .infoCall,
.infoScreenshotNotification, .infoMediaSavedNotification,
.infoClosedGroupCreated, .infoClosedGroupUpdated, .infoClosedGroupCurrentUserLeft,
.infoClosedGroupCreated, .infoClosedGroupUpdated,
.infoClosedGroupCurrentUserLeft, .infoClosedGroupCurrentUserLeaving, .infoClosedGroupCurrentUserErrorLeaving,
.infoMessageRequestAccepted, .infoDisappearingMessagesUpdate:
// Info messages and unsent messages should just trigger a local
// deletion (they are created as side effects so we wouldn't be
@ -2003,7 +2016,8 @@ extension ConversationVC:
try MessageSender.send(
db,
message: DataExtractionNotification(
kind: .mediaSaved(timestamp: UInt64(cellViewModel.timestampMs))
kind: .mediaSaved(timestamp: UInt64(cellViewModel.timestampMs)),
sentTimestamp: UInt64(SnodeAPI.currentOffsetTimestampMs())
),
interactionId: nil,
in: thread
@ -2257,7 +2271,8 @@ extension ConversationVC:
try MessageSender.send(
db,
message: DataExtractionNotification(
kind: .screenshot
kind: .screenshot,
sentTimestamp: UInt64(SnodeAPI.currentOffsetTimestampMs())
),
interactionId: nil,
in: thread

@ -146,6 +146,7 @@ public class ConversationViewModel: OWSAudioPlayerDelegate {
.map { $0.with(recentReactionEmoji: recentReactionEmoji) }
.map { viewModel -> SessionThreadViewModel in
viewModel.populatingCurrentUserBlindedKey(
db,
currentUserBlindedPublicKeyForThisThread: self?.threadData.currentUserBlindedPublicKey
)
}

@ -97,6 +97,7 @@ final class InfoMessageCell: MessageCell {
iconImageViewHeightConstraint.constant = (icon != nil) ? InfoMessageCell.iconSize : 0
self.label.text = cellViewModel.body
self.label.themeTextColor = (cellViewModel.variant == .infoClosedGroupCurrentUserErrorLeaving) ? .danger : .textPrimary
}
override func dynamicUpdate(with cellViewModel: MessageViewModel, playbackInfo: ConversationViewModel.PlaybackInfo?) {

@ -70,7 +70,8 @@ public class MessageCell: UITableViewCell {
case .standardOutgoing, .standardIncoming, .standardIncomingDeleted:
return VisibleMessageCell.self
case .infoClosedGroupCreated, .infoClosedGroupUpdated, .infoClosedGroupCurrentUserLeft,
case .infoClosedGroupCreated, .infoClosedGroupUpdated,
.infoClosedGroupCurrentUserLeft, .infoClosedGroupCurrentUserLeaving, .infoClosedGroupCurrentUserErrorLeaving,
.infoDisappearingMessagesUpdate, .infoScreenshotNotification, .infoMediaSavedNotification,
.infoMessageRequestAccepted:
return InfoMessageCell.self

@ -1,6 +1,7 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import UIKit
import SessionUIKit
import SignalUtilitiesKit
import SessionUtilitiesKit
import SessionMessagingKit
@ -774,15 +775,21 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate {
// MARK: - Interaction
// override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
// if let bodyTappableLabel = bodyTappableLabel {
// let btIngetBodyTappableLabelCoordinates = convert(point, to: bodyTappableLabel)
// if bodyTappableLabel.bounds.contains(btIngetBodyTappableLabelCoordinates) {
// return bodyTappableLabel
// }
// }
// return super.hitTest(point, with: event)
// }
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
// We are currently using Appium to do automated UI testing, unfortunately it seems to run into
// issues when trying to long-press an element which has custom interaction logic - the TappableLabel
// only needs to custom handle touches for interacting with links so we check to see if it contains
// links before forwarding touches to it
if let bodyTappableLabel: TappableLabel = bodyTappableLabel, bodyTappableLabel.containsLinks {
let bodyTappableLabelLocalTapCoordinate: CGPoint = convert(point, to: bodyTappableLabel)
if bodyTappableLabel.bounds.contains(bodyTappableLabelLocalTapCoordinate) {
return bodyTappableLabel
}
}
return super.hitTest(point, with: event)
}
override func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return true // Needed for the pan gesture recognizer to work with the table view's pan gesture recognizer

@ -394,18 +394,32 @@ class ThreadSettingsViewModel: SessionTableViewModel<ThreadSettingsViewModel.Nav
accessibilityIdentifier: "Leave group",
accessibilityLabel: "Leave group",
confirmationInfo: ConfirmationModal.Info(
title: "CONFIRM_LEAVE_GROUP_TITLE".localized(),
explanation: (currentUserIsClosedGroupAdmin ?
"Because you are the creator of this group it will be deleted for everyone. This cannot be undone." :
"CONFIRM_LEAVE_GROUP_DESCRIPTION".localized()
),
title: "leave_group_confirmation_alert_title".localized(),
attributedExplanation: {
if currentUserIsClosedGroupAdmin {
return NSAttributedString(string: "admin_group_leave_warning".localized())
}
let mutableAttributedString = NSMutableAttributedString(
string: String(
format: "leave_community_confirmation_alert_message".localized(),
threadViewModel.displayName
)
)
mutableAttributedString.addAttribute(
.font,
value: UIFont.boldSystemFont(ofSize: Values.smallFontSize),
range: (mutableAttributedString.string as NSString).range(of: threadViewModel.displayName)
)
return mutableAttributedString
}(),
confirmTitle: "LEAVE_BUTTON_TITLE".localized(),
confirmStyle: .danger,
cancelStyle: .alert_text
),
onTap: { [weak self] in
dependencies.storage.writeAsync { db in
try MessageSender.leave(db, groupPublicKey: threadId)
try MessageSender.leave(db, groupPublicKey: threadId, deleteThread: false)
}
}
)

@ -618,6 +618,10 @@ final class HomeVC: BaseVC, UITableViewDataSource, UITableViewDelegate, SeedRemi
return true
}
func tableView(_ tableView: UITableView, leadingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
return nil
}
func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
let section: HomeViewModel.SectionModel = self.viewModel.threadData[indexPath.section]
let unswipeAnimationDelay: DispatchTimeInterval = .milliseconds(500)
@ -634,44 +638,18 @@ final class HomeVC: BaseVC, UITableViewDataSource, UITableViewDelegate, SeedRemi
case .threads:
let threadViewModel: SessionThreadViewModel = section.elements[indexPath.row]
let delete: UIContextualAction = UIContextualAction(
style: .destructive,
title: "TXT_DELETE_TITLE".localized()
) { [weak self] _, _, completionHandler in
let confirmationModal: ConfirmationModal = ConfirmationModal(
info: ConfirmationModal.Info(
title: "CONVERSATION_DELETE_CONFIRMATION_ALERT_TITLE".localized(),
explanation: (threadViewModel.currentUserIsClosedGroupAdmin == true ?
"admin_group_leave_warning".localized() :
"CONVERSATION_DELETE_CONFIRMATION_ALERT_MESSAGE".localized()
),
confirmTitle: "TXT_DELETE_TITLE".localized(),
confirmStyle: .danger,
cancelStyle: .alert_text,
dismissOnConfirm: true,
onConfirm: { [weak self] _ in
self?.viewModel.delete(
threadId: threadViewModel.threadId,
threadVariant: threadViewModel.threadVariant
)
self?.dismiss(animated: true, completion: nil)
completionHandler(true)
},
afterClosed: { completionHandler(false) }
)
)
self?.present(confirmationModal, animated: true, completion: nil)
}
delete.themeBackgroundColor = .conversationButton_swipeDestructive
guard threadViewModel.interactionVariant != .infoClosedGroupCurrentUserLeaving else { return nil }
let pin: UIContextualAction = UIContextualAction(
style: .normal,
title: (threadViewModel.threadIsPinned ?
"UNPIN_BUTTON_TEXT".localized() :
"PIN_BUTTON_TEXT".localized()
)
title: (threadViewModel.threadIsPinned ? "UNPIN_BUTTON_TEXT".localized() : "PIN_BUTTON_TEXT".localized()),
icon: UIImage(systemName: "pin"),
iconHeight: Values.mediumFontSize,
themeTintColor: .white,
themeBackgroundColor: .conversationButton_swipeDestructive,
side: .trailing,
actionIndex: 0,
indexPath: indexPath,
tableView: tableView
) { _, _, completionHandler in
(tableView.cellForRow(at: indexPath) as? FullConversationCell)?.optimisticUpdate(
isPinned: !threadViewModel.threadIsPinned
@ -688,51 +666,201 @@ final class HomeVC: BaseVC, UITableViewDataSource, UITableViewDelegate, SeedRemi
}
}
pin.themeBackgroundColor = .conversationButton_swipeTertiary
guard threadViewModel.threadVariant == .contact && !threadViewModel.threadIsNoteToSelf else {
return UISwipeActionsConfiguration(actions: [ delete, pin ])
}
let block: UIContextualAction = UIContextualAction(
style: .normal,
title: (threadViewModel.threadIsBlocked == true ?
"BLOCK_LIST_UNBLOCK_BUTTON".localized() :
"BLOCK_LIST_BLOCK_BUTTON".localized()
)
let mute: UIContextualAction = UIContextualAction(
title: ((threadViewModel.threadMutedUntilTimestamp != nil) ? "unmute_button_text".localized() : "mute_button_text".localized()),
icon: UIImage(systemName: "speaker.slash"),
iconHeight: Values.mediumFontSize,
themeTintColor: .white,
themeBackgroundColor: .conversationButton_swipeDestructive,
side: .trailing,
actionIndex: 1,
indexPath: indexPath,
tableView: tableView
) { _, _, completionHandler in
(tableView.cellForRow(at: indexPath) as? FullConversationCell)?.optimisticUpdate(
isBlocked: (threadViewModel.threadIsBlocked == false)
isMuted: !(threadViewModel.threadMutedUntilTimestamp != nil)
)
completionHandler(true)
// Delay the change to give the cell "unswipe" animation some time to complete
DispatchQueue.global(qos: .default).asyncAfter(deadline: .now() + unswipeAnimationDelay) {
Storage.shared.writeAsync { db in
try Contact
let currentValue: TimeInterval? = try SessionThread
.filter(id: threadViewModel.threadId)
.select(.mutedUntilTimestamp)
.asRequest(of: TimeInterval.self)
.fetchOne(db)
try SessionThread
.filter(id: threadViewModel.threadId)
.updateAll(
db,
Contact.Columns.isBlocked.set(
to: (threadViewModel.threadIsBlocked == false ?
true:
false
SessionThread.Columns.mutedUntilTimestamp.set(
to: (currentValue == nil ?
Date.distantFuture.timeIntervalSince1970 :
nil
)
)
)
try MessageSender.syncConfiguration(db, forceSyncNow: true)
.retainUntilComplete()
}
}
}
block.themeBackgroundColor = .conversationButton_swipeSecondary
return UISwipeActionsConfiguration(actions: [ delete, block, pin ])
mute.themeBackgroundColor = .conversationButton_swipeSecondary
switch (threadViewModel.threadVariant, threadViewModel.currentUserIsClosedGroupMember) {
case (.contact, _):
let delete: UIContextualAction = UIContextualAction(
title: "TXT_DELETE_TITLE".localized(),
icon: UIImage(named: "icon_bin")?.resizedImage(to: CGSize(width: Values.mediumFontSize, height: Values.mediumFontSize)),
iconHeight: Values.mediumFontSize,
themeTintColor: .white,
themeBackgroundColor: .conversationButton_swipeDestructive,
side: .trailing,
actionIndex: 2,
indexPath: indexPath,
tableView: tableView
) { [weak self] _, _, completionHandler in
let confirmationModalExplanation: NSAttributedString = {
let mutableAttributedString = NSMutableAttributedString(
string: String(
format: "delete_conversation_confirmation_alert_message".localized(),
threadViewModel.displayName
)
)
mutableAttributedString.addAttribute(
.font,
value: UIFont.boldSystemFont(ofSize: Values.smallFontSize),
range: (mutableAttributedString.string as NSString).range(of: threadViewModel.displayName)
)
return mutableAttributedString
}()
let confirmationModal: ConfirmationModal = ConfirmationModal(
info: ConfirmationModal.Info(
title: "delete_conversation_confirmation_alert_title".localized(),
attributedExplanation: confirmationModalExplanation,
confirmTitle: "TXT_DELETE_TITLE".localized(),
confirmStyle: .danger,
cancelStyle: .alert_text,
dismissOnConfirm: true,
onConfirm: { [weak self] _ in
self?.viewModel.delete(
threadId: threadViewModel.threadId,
threadVariant: threadViewModel.threadVariant
)
self?.dismiss(animated: true, completion: nil)
completionHandler(true)
},
afterClosed: { completionHandler(false) }
)
)
self?.present(confirmationModal, animated: true, completion: nil)
}
delete.themeBackgroundColor = .conversationButton_swipeDestructive
return UISwipeActionsConfiguration(actions: [ delete, mute, pin ])
case (.closedGroup, false):
let delete: UIContextualAction = UIContextualAction(
title: "TXT_DELETE_TITLE".localized(),
icon: UIImage(named: "icon_bin")?.resizedImage(to: CGSize(width: Values.mediumFontSize, height: Values.mediumFontSize)),
iconHeight: Values.mediumFontSize,
themeTintColor: .white,
themeBackgroundColor: .conversationButton_swipeDestructive,
side: .trailing,
actionIndex: 2,
indexPath: indexPath,
tableView: tableView
) { [weak self] _, _, completionHandler in
self?.viewModel.delete(
threadId: threadViewModel.threadId,
threadVariant: threadViewModel.threadVariant,
force: true
)
completionHandler(true)
}
return UISwipeActionsConfiguration(actions: [ delete, mute, pin ])
default:
let leave: UIContextualAction = UIContextualAction(
title: "LEAVE_BUTTON_TITLE".localized(),
icon: UIImage(systemName: "rectangle.portrait.and.arrow.right"),
iconHeight: Values.mediumFontSize,
themeTintColor: .white,
themeBackgroundColor: .conversationButton_swipeDestructive,
side: .trailing,
actionIndex: 2,
indexPath: indexPath,
tableView: tableView
) { [weak self] _, _, completionHandler in
let confirmationModalTitle: String = (threadViewModel.threadVariant == .closedGroup) ?
"leave_group_confirmation_alert_title".localized() :
"leave_community_confirmation_alert_title".localized()
let confirmationModalExplanation: NSAttributedString = {
if threadViewModel.threadVariant == .closedGroup && threadViewModel.currentUserIsClosedGroupAdmin == true {
return NSAttributedString(string: "admin_group_leave_warning".localized())
}
let mutableAttributedString = NSMutableAttributedString(
string: String(
format: "leave_community_confirmation_alert_message".localized(),
threadViewModel.displayName
)
)
mutableAttributedString.addAttribute(
.font,
value: UIFont.boldSystemFont(ofSize: Values.smallFontSize),
range: (mutableAttributedString.string as NSString).range(of: threadViewModel.displayName)
)
return mutableAttributedString
}()
let confirmationModal: ConfirmationModal = ConfirmationModal(
info: ConfirmationModal.Info(
title: confirmationModalTitle,
attributedExplanation: confirmationModalExplanation,
confirmTitle: "LEAVE_BUTTON_TITLE".localized(),
confirmStyle: .danger,
cancelStyle: .alert_text,
dismissOnConfirm: true,
onConfirm: { [weak self] _ in
self?.viewModel.delete(
threadId: threadViewModel.threadId,
threadVariant: threadViewModel.threadVariant
)
self?.dismiss(animated: true, completion: nil)
completionHandler(true)
},
afterClosed: { completionHandler(false) }
)
)
self?.present(confirmationModal, animated: true, completion: nil)
}
leave.themeBackgroundColor = .conversationButton_swipeDestructive
return UISwipeActionsConfiguration(actions: [ leave, mute, pin ])
}
default: return nil
}
}
func tableView(_ tableView: UITableView, willBeginEditingRowAt indexPath: IndexPath) {
UIContextualAction.willBeginEditing(indexPath: indexPath, tableView: tableView)
}
func tableView(_ tableView: UITableView, didEndEditingRowAt indexPath: IndexPath?) {
UIContextualAction.didEndEditing(indexPath: indexPath, tableView: tableView)
}
// MARK: - Interaction
func handleContinueButtonTapped(from seedReminderView: SeedReminderView) {

@ -301,23 +301,29 @@ public class HomeViewModel {
// MARK: - Functions
public func delete(threadId: String, threadVariant: SessionThread.Variant) {
public func delete(threadId: String, threadVariant: SessionThread.Variant, force: Bool = false) {
func delete(_ db: Database, threadId: String) throws {
_ = try SessionThread
.filter(id: threadId)
.deleteAll(db)
}
Storage.shared.writeAsync { db in
switch threadVariant {
case .closedGroup:
try MessageSender
.leave(db, groupPublicKey: threadId)
.retainUntilComplete()
switch (threadVariant, force) {
case (.closedGroup, false):
try MessageSender.leave(
db,
groupPublicKey: threadId,
deleteThread: true
)
case .openGroup:
case (.openGroup, _):
OpenGroupManager.shared.delete(db, openGroupId: threadId)
default: break
default:
try delete(db, threadId: threadId)
}
_ = try SessionThread
.filter(id: threadId)
.deleteAll(db)
}
}
}

@ -540,7 +540,8 @@ class MediaPageViewController: UIPageViewController, UIPageViewControllerDataSou
message: DataExtractionNotification(
kind: .mediaSaved(
timestamp: UInt64(currentViewController.galleryItem.interactionTimestampMs)
)
),
sentTimestamp: UInt64(SnodeAPI.currentOffsetTimestampMs())
),
interactionId: nil, // Show no interaction for the current user
in: thread

@ -60,14 +60,6 @@
"BUTTON_DONE" = "Fertig";
/* Button text to enable batch selection mode */
"BUTTON_SELECT" = "Auswählen";
/* Alert body */
"CONFIRM_LEAVE_GROUP_DESCRIPTION" = "Du wirst in dieser Gruppe keine Nachrichten mehr versenden oder empfangen können.";
/* Alert title */
"CONFIRM_LEAVE_GROUP_TITLE" = "Wollen Sie die Gruppe wirklich verlassen?";
/* Message for the 'conversation delete confirmation' alert. */
"CONVERSATION_DELETE_CONFIRMATION_ALERT_MESSAGE" = "Dies kann nicht rückgängig gemacht werden.";
/* Title for the 'conversation delete confirmation' alert. */
"CONVERSATION_DELETE_CONFIRMATION_ALERT_TITLE" = "Unterhaltung löschen?";
/* keyboard toolbar label when starting to search with no current results */
"CONVERSATION_SEARCH_SEARCHING" = "Searching...";
/* keyboard toolbar label when no messages match the search string */
@ -609,3 +601,15 @@
"context_menu_resync" = "Resync";
"GIPHY_PERMISSION_TITLE" = "Search GIFs?";
"GIPHY_PERMISSION_MESSAGE" = "Session will connect to Giphy to provide search results. You will not have full metadata protection when sending GIFs.";
"mute_button_text" = "Mute";
"unmute_button_text" = "Unmute";
"mark_read_button_text" = "Mark read";
"mark_unread_button_text" = "Mark unread";
"leave_group_confirmation_alert_title" = "Leave Group";
"leave_community_confirmation_alert_title" = "Leave Community";
"leave_community_confirmation_alert_message" = "Are you sure you want to leave %@?";
"group_you_leaving" = "Leaving...";
"group_leave_error" = "Failed to leave Group!";
"group_unable_to_leave" = "Unable to leave the Group, please try again";
"delete_conversation_confirmation_alert_message" = "Are you sure you want to delete your conversation with %@?";
"delete_conversation_confirmation_alert_title" = "Delete Conversation";

@ -60,14 +60,6 @@
"BUTTON_DONE" = "Done";
/* Button text to enable batch selection mode */
"BUTTON_SELECT" = "Select";
/* Alert body */
"CONFIRM_LEAVE_GROUP_DESCRIPTION" = "You will no longer be able to send or receive messages in this group.";
/* Alert title */
"CONFIRM_LEAVE_GROUP_TITLE" = "Do you really want to leave?";
/* Message for the 'conversation delete confirmation' alert. */
"CONVERSATION_DELETE_CONFIRMATION_ALERT_MESSAGE" = "This cannot be undone.";
/* Title for the 'conversation delete confirmation' alert. */
"CONVERSATION_DELETE_CONFIRMATION_ALERT_TITLE" = "Delete Conversation?";
/* keyboard toolbar label when starting to search with no current results */
"CONVERSATION_SEARCH_SEARCHING" = "Searching...";
/* keyboard toolbar label when no messages match the search string */
@ -609,3 +601,15 @@
"context_menu_resync" = "Resync";
"GIPHY_PERMISSION_TITLE" = "Search GIFs?";
"GIPHY_PERMISSION_MESSAGE" = "Session will connect to Giphy to provide search results. You will not have full metadata protection when sending GIFs.";
"mute_button_text" = "Mute";
"unmute_button_text" = "Unmute";
"mark_read_button_text" = "Mark read";
"mark_unread_button_text" = "Mark unread";
"leave_group_confirmation_alert_title" = "Leave Group";
"leave_community_confirmation_alert_title" = "Leave Community";
"leave_community_confirmation_alert_message" = "Are you sure you want to leave %@?";
"group_you_leaving" = "Leaving...";
"group_leave_error" = "Failed to leave Group!";
"group_unable_to_leave" = "Unable to leave the Group, please try again";
"delete_conversation_confirmation_alert_message" = "Are you sure you want to delete your conversation with %@?";
"delete_conversation_confirmation_alert_title" = "Delete Conversation";

@ -60,14 +60,6 @@
"BUTTON_DONE" = "Hecho";
/* Button text to enable batch selection mode */
"BUTTON_SELECT" = "Seleccionar";
/* Alert body */
"CONFIRM_LEAVE_GROUP_DESCRIPTION" = "No podrás enviar o recibir más mensajes en este grupo.";
/* Alert title */
"CONFIRM_LEAVE_GROUP_TITLE" = "¿De verdad quieres abandonar el grupo?";
/* Message for the 'conversation delete confirmation' alert. */
"CONVERSATION_DELETE_CONFIRMATION_ALERT_MESSAGE" = "Este paso no se puede deshacer.";
/* Title for the 'conversation delete confirmation' alert. */
"CONVERSATION_DELETE_CONFIRMATION_ALERT_TITLE" = "¿Eliminar conversación?";
/* keyboard toolbar label when starting to search with no current results */
"CONVERSATION_SEARCH_SEARCHING" = "Searching...";
/* keyboard toolbar label when no messages match the search string */
@ -609,3 +601,15 @@
"context_menu_resync" = "Resync";
"GIPHY_PERMISSION_TITLE" = "Search GIFs?";
"GIPHY_PERMISSION_MESSAGE" = "Session will connect to Giphy to provide search results. You will not have full metadata protection when sending GIFs.";
"mute_button_text" = "Mute";
"unmute_button_text" = "Unmute";
"mark_read_button_text" = "Mark read";
"mark_unread_button_text" = "Mark unread";
"leave_group_confirmation_alert_title" = "Leave Group";
"leave_community_confirmation_alert_title" = "Leave Community";
"leave_community_confirmation_alert_message" = "Are you sure you want to leave %@?";
"group_you_leaving" = "Leaving...";
"group_leave_error" = "Failed to leave Group!";
"group_unable_to_leave" = "Unable to leave the Group, please try again";
"delete_conversation_confirmation_alert_message" = "Are you sure you want to delete your conversation with %@?";
"delete_conversation_confirmation_alert_title" = "Delete Conversation";

@ -60,14 +60,6 @@
"BUTTON_DONE" = "انجام شد";
/* Button text to enable batch selection mode */
"BUTTON_SELECT" = "انتخاب";
/* Alert body */
"CONFIRM_LEAVE_GROUP_DESCRIPTION" = "شما دیگر قادر به ارسال یا دریافت پیام از این گروه نخواهید بود";
/* Alert title */
"CONFIRM_LEAVE_GROUP_TITLE" = "آیا واقعا قصد ترک کردن دارید؟";
/* Message for the 'conversation delete confirmation' alert. */
"CONVERSATION_DELETE_CONFIRMATION_ALERT_MESSAGE" = "این نمیتواند انجام نشود.";
/* Title for the 'conversation delete confirmation' alert. */
"CONVERSATION_DELETE_CONFIRMATION_ALERT_TITLE" = "گفتگو حذف شود؟";
/* keyboard toolbar label when starting to search with no current results */
"CONVERSATION_SEARCH_SEARCHING" = "درحال جستجو...";
/* keyboard toolbar label when no messages match the search string */
@ -609,3 +601,15 @@
"context_menu_resync" = "Resync";
"GIPHY_PERMISSION_TITLE" = "Search GIFs?";
"GIPHY_PERMISSION_MESSAGE" = "Session will connect to Giphy to provide search results. You will not have full metadata protection when sending GIFs.";
"mute_button_text" = "Mute";
"unmute_button_text" = "Unmute";
"mark_read_button_text" = "Mark read";
"mark_unread_button_text" = "Mark unread";
"leave_group_confirmation_alert_title" = "Leave Group";
"leave_community_confirmation_alert_title" = "Leave Community";
"leave_community_confirmation_alert_message" = "Are you sure you want to leave %@?";
"group_you_leaving" = "Leaving...";
"group_leave_error" = "Failed to leave Group!";
"group_unable_to_leave" = "Unable to leave the Group, please try again";
"delete_conversation_confirmation_alert_message" = "Are you sure you want to delete your conversation with %@?";
"delete_conversation_confirmation_alert_title" = "Delete Conversation";

@ -60,14 +60,6 @@
"BUTTON_DONE" = "Valmis";
/* Button text to enable batch selection mode */
"BUTTON_SELECT" = "Valitse";
/* Alert body */
"CONFIRM_LEAVE_GROUP_DESCRIPTION" = "Et pysty enään lähettämään tai vastaanottamaan viestejä tässä ryhmässä.";
/* Alert title */
"CONFIRM_LEAVE_GROUP_TITLE" = "Haluatko varmasti poistua ryhmästä?";
/* Message for the 'conversation delete confirmation' alert. */
"CONVERSATION_DELETE_CONFIRMATION_ALERT_MESSAGE" = "Tätä ei voida perua.";
/* Title for the 'conversation delete confirmation' alert. */
"CONVERSATION_DELETE_CONFIRMATION_ALERT_TITLE" = "Poistetaanko keskustelu?";
/* keyboard toolbar label when starting to search with no current results */
"CONVERSATION_SEARCH_SEARCHING" = "Searching...";
/* keyboard toolbar label when no messages match the search string */
@ -609,3 +601,15 @@
"context_menu_resync" = "Resync";
"GIPHY_PERMISSION_TITLE" = "Search GIFs?";
"GIPHY_PERMISSION_MESSAGE" = "Session will connect to Giphy to provide search results. You will not have full metadata protection when sending GIFs.";
"mute_button_text" = "Mute";
"unmute_button_text" = "Unmute";
"mark_read_button_text" = "Mark read";
"mark_unread_button_text" = "Mark unread";
"leave_group_confirmation_alert_title" = "Leave Group";
"leave_community_confirmation_alert_title" = "Leave Community";
"leave_community_confirmation_alert_message" = "Are you sure you want to leave %@?";
"group_you_leaving" = "Leaving...";
"group_leave_error" = "Failed to leave Group!";
"group_unable_to_leave" = "Unable to leave the Group, please try again";
"delete_conversation_confirmation_alert_message" = "Are you sure you want to delete your conversation with %@?";
"delete_conversation_confirmation_alert_title" = "Delete Conversation";

@ -60,14 +60,6 @@
"BUTTON_DONE" = "Terminé";
/* Button text to enable batch selection mode */
"BUTTON_SELECT" = "Sélectionner";
/* Alert body */
"CONFIRM_LEAVE_GROUP_DESCRIPTION" = "Vous ne pourrez plus recevoir ni envoyer de messages dans ce groupe.";
/* Alert title */
"CONFIRM_LEAVE_GROUP_TITLE" = "Voulez-vous vraiment quitter ce groupe?";
/* Message for the 'conversation delete confirmation' alert. */
"CONVERSATION_DELETE_CONFIRMATION_ALERT_MESSAGE" = "Cette action est irréversible.";
/* Title for the 'conversation delete confirmation' alert. */
"CONVERSATION_DELETE_CONFIRMATION_ALERT_TITLE" = "Supprimer la conversation?";
/* keyboard toolbar label when starting to search with no current results */
"CONVERSATION_SEARCH_SEARCHING" = "Searching...";
/* keyboard toolbar label when no messages match the search string */
@ -609,3 +601,15 @@
"context_menu_resync" = "Resync";
"GIPHY_PERMISSION_TITLE" = "Search GIFs?";
"GIPHY_PERMISSION_MESSAGE" = "Session will connect to Giphy to provide search results. You will not have full metadata protection when sending GIFs.";
"mute_button_text" = "Mute";
"unmute_button_text" = "Unmute";
"mark_read_button_text" = "Mark read";
"mark_unread_button_text" = "Mark unread";
"leave_group_confirmation_alert_title" = "Leave Group";
"leave_community_confirmation_alert_title" = "Leave Community";
"leave_community_confirmation_alert_message" = "Are you sure you want to leave %@?";
"group_you_leaving" = "Leaving...";
"group_leave_error" = "Failed to leave Group!";
"group_unable_to_leave" = "Unable to leave the Group, please try again";
"delete_conversation_confirmation_alert_message" = "Are you sure you want to delete your conversation with %@?";
"delete_conversation_confirmation_alert_title" = "Delete Conversation";

@ -60,14 +60,6 @@
"BUTTON_DONE" = "पूरा हुआ";
/* Button text to enable batch selection mode */
"BUTTON_SELECT" = "चुनें";
/* Alert body */
"CONFIRM_LEAVE_GROUP_DESCRIPTION" = "अब आप इस समूह में संदेश भेजने या प्राप्त करने में सक्षम नहीं होंगे।";
/* Alert title */
"CONFIRM_LEAVE_GROUP_TITLE" = "क्या आप वाकई छोड़ना चाहते हैं?";
/* Message for the 'conversation delete confirmation' alert. */
"CONVERSATION_DELETE_CONFIRMATION_ALERT_MESSAGE" = "This cannot be undone.";
/* Title for the 'conversation delete confirmation' alert. */
"CONVERSATION_DELETE_CONFIRMATION_ALERT_TITLE" = "Delete Conversation?";
/* keyboard toolbar label when starting to search with no current results */
"CONVERSATION_SEARCH_SEARCHING" = "Searching...";
/* keyboard toolbar label when no messages match the search string */
@ -609,3 +601,15 @@
"context_menu_resync" = "Resync";
"GIPHY_PERMISSION_TITLE" = "Search GIFs?";
"GIPHY_PERMISSION_MESSAGE" = "Session will connect to Giphy to provide search results. You will not have full metadata protection when sending GIFs.";
"mute_button_text" = "Mute";
"unmute_button_text" = "Unmute";
"mark_read_button_text" = "Mark read";
"mark_unread_button_text" = "Mark unread";
"leave_group_confirmation_alert_title" = "Leave Group";
"leave_community_confirmation_alert_title" = "Leave Community";
"leave_community_confirmation_alert_message" = "Are you sure you want to leave %@?";
"group_you_leaving" = "Leaving...";
"group_leave_error" = "Failed to leave Group!";
"group_unable_to_leave" = "Unable to leave the Group, please try again";
"delete_conversation_confirmation_alert_message" = "Are you sure you want to delete your conversation with %@?";
"delete_conversation_confirmation_alert_title" = "Delete Conversation";

@ -60,14 +60,6 @@
"BUTTON_DONE" = "Gotovo";
/* Button text to enable batch selection mode */
"BUTTON_SELECT" = "Odaberi";
/* Alert body */
"CONFIRM_LEAVE_GROUP_DESCRIPTION" = "Više nećete moći slati niti primati poruke u ovoj grupi.";
/* Alert title */
"CONFIRM_LEAVE_GROUP_TITLE" = "Da li zaista želite izaći?";
/* Message for the 'conversation delete confirmation' alert. */
"CONVERSATION_DELETE_CONFIRMATION_ALERT_MESSAGE" = "Ovaj je postupak nepovratan.";
/* Title for the 'conversation delete confirmation' alert. */
"CONVERSATION_DELETE_CONFIRMATION_ALERT_TITLE" = "Obriši razgovor?";
/* keyboard toolbar label when starting to search with no current results */
"CONVERSATION_SEARCH_SEARCHING" = "Searching...";
/* keyboard toolbar label when no messages match the search string */
@ -609,3 +601,15 @@
"context_menu_resync" = "Resync";
"GIPHY_PERMISSION_TITLE" = "Search GIFs?";
"GIPHY_PERMISSION_MESSAGE" = "Session will connect to Giphy to provide search results. You will not have full metadata protection when sending GIFs.";
"mute_button_text" = "Mute";
"unmute_button_text" = "Unmute";
"mark_read_button_text" = "Mark read";
"mark_unread_button_text" = "Mark unread";
"leave_group_confirmation_alert_title" = "Leave Group";
"leave_community_confirmation_alert_title" = "Leave Community";
"leave_community_confirmation_alert_message" = "Are you sure you want to leave %@?";
"group_you_leaving" = "Leaving...";
"group_leave_error" = "Failed to leave Group!";
"group_unable_to_leave" = "Unable to leave the Group, please try again";
"delete_conversation_confirmation_alert_message" = "Are you sure you want to delete your conversation with %@?";
"delete_conversation_confirmation_alert_title" = "Delete Conversation";

@ -60,14 +60,6 @@
"BUTTON_DONE" = "Selesai";
/* Button text to enable batch selection mode */
"BUTTON_SELECT" = "Pilih";
/* Alert body */
"CONFIRM_LEAVE_GROUP_DESCRIPTION" = "Anda tidak dapat lagi mengirim atau menerima pesan dari grup ini.";
/* Alert title */
"CONFIRM_LEAVE_GROUP_TITLE" = "Apakah Anda benar-benar ingin keluar?";
/* Message for the 'conversation delete confirmation' alert. */
"CONVERSATION_DELETE_CONFIRMATION_ALERT_MESSAGE" = "Tindakan ini tidak dapat dibatalkan.";
/* Title for the 'conversation delete confirmation' alert. */
"CONVERSATION_DELETE_CONFIRMATION_ALERT_TITLE" = "Hapus Percakapan?";
/* keyboard toolbar label when starting to search with no current results */
"CONVERSATION_SEARCH_SEARCHING" = "Searching...";
/* keyboard toolbar label when no messages match the search string */
@ -609,3 +601,15 @@
"context_menu_resync" = "Resync";
"GIPHY_PERMISSION_TITLE" = "Search GIFs?";
"GIPHY_PERMISSION_MESSAGE" = "Session will connect to Giphy to provide search results. You will not have full metadata protection when sending GIFs.";
"mute_button_text" = "Mute";
"unmute_button_text" = "Unmute";
"mark_read_button_text" = "Mark read";
"mark_unread_button_text" = "Mark unread";
"leave_group_confirmation_alert_title" = "Leave Group";
"leave_community_confirmation_alert_title" = "Leave Community";
"leave_community_confirmation_alert_message" = "Are you sure you want to leave %@?";
"group_you_leaving" = "Leaving...";
"group_leave_error" = "Failed to leave Group!";
"group_unable_to_leave" = "Unable to leave the Group, please try again";
"delete_conversation_confirmation_alert_message" = "Are you sure you want to delete your conversation with %@?";
"delete_conversation_confirmation_alert_title" = "Delete Conversation";

@ -60,14 +60,6 @@
"BUTTON_DONE" = "Fatto";
/* Button text to enable batch selection mode */
"BUTTON_SELECT" = "Seleziona";
/* Alert body */
"CONFIRM_LEAVE_GROUP_DESCRIPTION" = "Non sarei più in grado di inviare o ricevere messaggi in questo gruppo.";
/* Alert title */
"CONFIRM_LEAVE_GROUP_TITLE" = "Vuoi davvero lasciare?";
/* Message for the 'conversation delete confirmation' alert. */
"CONVERSATION_DELETE_CONFIRMATION_ALERT_MESSAGE" = "Non potrà essere annullato.";
/* Title for the 'conversation delete confirmation' alert. */
"CONVERSATION_DELETE_CONFIRMATION_ALERT_TITLE" = "Elimina conversazione?";
/* keyboard toolbar label when starting to search with no current results */
"CONVERSATION_SEARCH_SEARCHING" = "Searching...";
/* keyboard toolbar label when no messages match the search string */
@ -609,3 +601,15 @@
"context_menu_resync" = "Resync";
"GIPHY_PERMISSION_TITLE" = "Search GIFs?";
"GIPHY_PERMISSION_MESSAGE" = "Session will connect to Giphy to provide search results. You will not have full metadata protection when sending GIFs.";
"mute_button_text" = "Mute";
"unmute_button_text" = "Unmute";
"mark_read_button_text" = "Mark read";
"mark_unread_button_text" = "Mark unread";
"leave_group_confirmation_alert_title" = "Leave Group";
"leave_community_confirmation_alert_title" = "Leave Community";
"leave_community_confirmation_alert_message" = "Are you sure you want to leave %@?";
"group_you_leaving" = "Leaving...";
"group_leave_error" = "Failed to leave Group!";
"group_unable_to_leave" = "Unable to leave the Group, please try again";
"delete_conversation_confirmation_alert_message" = "Are you sure you want to delete your conversation with %@?";
"delete_conversation_confirmation_alert_title" = "Delete Conversation";

@ -60,14 +60,6 @@
"BUTTON_DONE" = "完了";
/* Button text to enable batch selection mode */
"BUTTON_SELECT" = "選択";
/* Alert body */
"CONFIRM_LEAVE_GROUP_DESCRIPTION" = "このグループとの会話が出来なくなりますがよろしいでしょうか。";
/* Alert title */
"CONFIRM_LEAVE_GROUP_TITLE" = "離脱してよろしいですか?";
/* Message for the 'conversation delete confirmation' alert. */
"CONVERSATION_DELETE_CONFIRMATION_ALERT_MESSAGE" = "消去すると元に戻せません";
/* Title for the 'conversation delete confirmation' alert. */
"CONVERSATION_DELETE_CONFIRMATION_ALERT_TITLE" = "消去しますか?";
/* keyboard toolbar label when starting to search with no current results */
"CONVERSATION_SEARCH_SEARCHING" = "Searching...";
/* keyboard toolbar label when no messages match the search string */
@ -609,3 +601,15 @@
"context_menu_resync" = "Resync";
"GIPHY_PERMISSION_TITLE" = "Search GIFs?";
"GIPHY_PERMISSION_MESSAGE" = "Session will connect to Giphy to provide search results. You will not have full metadata protection when sending GIFs.";
"mute_button_text" = "Mute";
"unmute_button_text" = "Unmute";
"mark_read_button_text" = "Mark read";
"mark_unread_button_text" = "Mark unread";
"leave_group_confirmation_alert_title" = "Leave Group";
"leave_community_confirmation_alert_title" = "Leave Community";
"leave_community_confirmation_alert_message" = "Are you sure you want to leave %@?";
"group_you_leaving" = "Leaving...";
"group_leave_error" = "Failed to leave Group!";
"group_unable_to_leave" = "Unable to leave the Group, please try again";
"delete_conversation_confirmation_alert_message" = "Are you sure you want to delete your conversation with %@?";
"delete_conversation_confirmation_alert_title" = "Delete Conversation";

@ -60,14 +60,6 @@
"BUTTON_DONE" = "Ok";
/* Button text to enable batch selection mode */
"BUTTON_SELECT" = "Selecteer";
/* Alert body */
"CONFIRM_LEAVE_GROUP_DESCRIPTION" = "Je kunt geen berichten meer versturen of ontvangen in deze groep.";
/* Alert title */
"CONFIRM_LEAVE_GROUP_TITLE" = "Wilt u echt deze groep verlaten?";
/* Message for the 'conversation delete confirmation' alert. */
"CONVERSATION_DELETE_CONFIRMATION_ALERT_MESSAGE" = "Dit kan niet ongedaan worden gemaakt.";
/* Title for the 'conversation delete confirmation' alert. */
"CONVERSATION_DELETE_CONFIRMATION_ALERT_TITLE" = "Gesprek verwijderen?";
/* keyboard toolbar label when starting to search with no current results */
"CONVERSATION_SEARCH_SEARCHING" = "Searching...";
/* keyboard toolbar label when no messages match the search string */
@ -609,3 +601,15 @@
"context_menu_resync" = "Resync";
"GIPHY_PERMISSION_TITLE" = "Search GIFs?";
"GIPHY_PERMISSION_MESSAGE" = "Session will connect to Giphy to provide search results. You will not have full metadata protection when sending GIFs.";
"mute_button_text" = "Mute";
"unmute_button_text" = "Unmute";
"mark_read_button_text" = "Mark read";
"mark_unread_button_text" = "Mark unread";
"leave_group_confirmation_alert_title" = "Leave Group";
"leave_community_confirmation_alert_title" = "Leave Community";
"leave_community_confirmation_alert_message" = "Are you sure you want to leave %@?";
"group_you_leaving" = "Leaving...";
"group_leave_error" = "Failed to leave Group!";
"group_unable_to_leave" = "Unable to leave the Group, please try again";
"delete_conversation_confirmation_alert_message" = "Are you sure you want to delete your conversation with %@?";
"delete_conversation_confirmation_alert_title" = "Delete Conversation";

@ -60,14 +60,6 @@
"BUTTON_DONE" = "Gotowe";
/* Button text to enable batch selection mode */
"BUTTON_SELECT" = "Zaznacz";
/* Alert body */
"CONFIRM_LEAVE_GROUP_DESCRIPTION" = "Nie będziesz już móc odbierać lub wysyłać wiadomości w tej grupie.";
/* Alert title */
"CONFIRM_LEAVE_GROUP_TITLE" = "Czy na pewno chcesz wyjść?";
/* Message for the 'conversation delete confirmation' alert. */
"CONVERSATION_DELETE_CONFIRMATION_ALERT_MESSAGE" = "Tego nie można cofnąć.";
/* Title for the 'conversation delete confirmation' alert. */
"CONVERSATION_DELETE_CONFIRMATION_ALERT_TITLE" = "Usunąć konwersację?";
/* keyboard toolbar label when starting to search with no current results */
"CONVERSATION_SEARCH_SEARCHING" = "Searching...";
/* keyboard toolbar label when no messages match the search string */
@ -609,3 +601,15 @@
"context_menu_resync" = "Resync";
"GIPHY_PERMISSION_TITLE" = "Search GIFs?";
"GIPHY_PERMISSION_MESSAGE" = "Session will connect to Giphy to provide search results. You will not have full metadata protection when sending GIFs.";
"mute_button_text" = "Mute";
"unmute_button_text" = "Unmute";
"mark_read_button_text" = "Mark read";
"mark_unread_button_text" = "Mark unread";
"leave_group_confirmation_alert_title" = "Leave Group";
"leave_community_confirmation_alert_title" = "Leave Community";
"leave_community_confirmation_alert_message" = "Are you sure you want to leave %@?";
"group_you_leaving" = "Leaving...";
"group_leave_error" = "Failed to leave Group!";
"group_unable_to_leave" = "Unable to leave the Group, please try again";
"delete_conversation_confirmation_alert_message" = "Are you sure you want to delete your conversation with %@?";
"delete_conversation_confirmation_alert_title" = "Delete Conversation";

@ -60,14 +60,6 @@
"BUTTON_DONE" = "Pronto";
/* Button text to enable batch selection mode */
"BUTTON_SELECT" = "Selecionar";
/* Alert body */
"CONFIRM_LEAVE_GROUP_DESCRIPTION" = "Você não poderá mais enviar nem receber mensagens neste grupo.";
/* Alert title */
"CONFIRM_LEAVE_GROUP_TITLE" = "Você tem certeza que deseja sair?";
/* Message for the 'conversation delete confirmation' alert. */
"CONVERSATION_DELETE_CONFIRMATION_ALERT_MESSAGE" = "Isso não pode ser desfeito.";
/* Title for the 'conversation delete confirmation' alert. */
"CONVERSATION_DELETE_CONFIRMATION_ALERT_TITLE" = "Excluir conversa?";
/* keyboard toolbar label when starting to search with no current results */
"CONVERSATION_SEARCH_SEARCHING" = "Searching...";
/* keyboard toolbar label when no messages match the search string */
@ -609,3 +601,15 @@
"context_menu_resync" = "Resync";
"GIPHY_PERMISSION_TITLE" = "Search GIFs?";
"GIPHY_PERMISSION_MESSAGE" = "Session will connect to Giphy to provide search results. You will not have full metadata protection when sending GIFs.";
"mute_button_text" = "Mute";
"unmute_button_text" = "Unmute";
"mark_read_button_text" = "Mark read";
"mark_unread_button_text" = "Mark unread";
"leave_group_confirmation_alert_title" = "Leave Group";
"leave_community_confirmation_alert_title" = "Leave Community";
"leave_community_confirmation_alert_message" = "Are you sure you want to leave %@?";
"group_you_leaving" = "Leaving...";
"group_leave_error" = "Failed to leave Group!";
"group_unable_to_leave" = "Unable to leave the Group, please try again";
"delete_conversation_confirmation_alert_message" = "Are you sure you want to delete your conversation with %@?";
"delete_conversation_confirmation_alert_title" = "Delete Conversation";

@ -60,14 +60,6 @@
"BUTTON_DONE" = "Готово";
/* Button text to enable batch selection mode */
"BUTTON_SELECT" = "Выбор";
/* Alert body */
"CONFIRM_LEAVE_GROUP_DESCRIPTION" = "Вы больше не сможете отправлять и получать сообщения в этой группе.";
/* Alert title */
"CONFIRM_LEAVE_GROUP_TITLE" = "Вы хотите покинуть группу?";
/* Message for the 'conversation delete confirmation' alert. */
"CONVERSATION_DELETE_CONFIRMATION_ALERT_MESSAGE" = "Это не может быть отменено.";
/* Title for the 'conversation delete confirmation' alert. */
"CONVERSATION_DELETE_CONFIRMATION_ALERT_TITLE" = "Удалить разговор?";
/* keyboard toolbar label when starting to search with no current results */
"CONVERSATION_SEARCH_SEARCHING" = "Searching...";
/* keyboard toolbar label when no messages match the search string */
@ -609,3 +601,15 @@
"context_menu_resync" = "Resync";
"GIPHY_PERMISSION_TITLE" = "Search GIFs?";
"GIPHY_PERMISSION_MESSAGE" = "Session will connect to Giphy to provide search results. You will not have full metadata protection when sending GIFs.";
"mute_button_text" = "Mute";
"unmute_button_text" = "Unmute";
"mark_read_button_text" = "Mark read";
"mark_unread_button_text" = "Mark unread";
"leave_group_confirmation_alert_title" = "Leave Group";
"leave_community_confirmation_alert_title" = "Leave Community";
"leave_community_confirmation_alert_message" = "Are you sure you want to leave %@?";
"group_you_leaving" = "Leaving...";
"group_leave_error" = "Failed to leave Group!";
"group_unable_to_leave" = "Unable to leave the Group, please try again";
"delete_conversation_confirmation_alert_message" = "Are you sure you want to delete your conversation with %@?";
"delete_conversation_confirmation_alert_title" = "Delete Conversation";

@ -60,14 +60,6 @@
"BUTTON_DONE" = "Done";
/* Button text to enable batch selection mode */
"BUTTON_SELECT" = "Select";
/* Alert body */
"CONFIRM_LEAVE_GROUP_DESCRIPTION" = "You will no longer be able to send or receive messages in this group.";
/* Alert title */
"CONFIRM_LEAVE_GROUP_TITLE" = "Do you really want to leave?";
/* Message for the 'conversation delete confirmation' alert. */
"CONVERSATION_DELETE_CONFIRMATION_ALERT_MESSAGE" = "This cannot be undone.";
/* Title for the 'conversation delete confirmation' alert. */
"CONVERSATION_DELETE_CONFIRMATION_ALERT_TITLE" = "Delete Conversation?";
/* keyboard toolbar label when starting to search with no current results */
"CONVERSATION_SEARCH_SEARCHING" = "Searching...";
/* keyboard toolbar label when no messages match the search string */
@ -609,3 +601,15 @@
"context_menu_resync" = "Resync";
"GIPHY_PERMISSION_TITLE" = "Search GIFs?";
"GIPHY_PERMISSION_MESSAGE" = "Session will connect to Giphy to provide search results. You will not have full metadata protection when sending GIFs.";
"mute_button_text" = "Mute";
"unmute_button_text" = "Unmute";
"mark_read_button_text" = "Mark read";
"mark_unread_button_text" = "Mark unread";
"leave_group_confirmation_alert_title" = "Leave Group";
"leave_community_confirmation_alert_title" = "Leave Community";
"leave_community_confirmation_alert_message" = "Are you sure you want to leave %@?";
"group_you_leaving" = "Leaving...";
"group_leave_error" = "Failed to leave Group!";
"group_unable_to_leave" = "Unable to leave the Group, please try again";
"delete_conversation_confirmation_alert_message" = "Are you sure you want to delete your conversation with %@?";
"delete_conversation_confirmation_alert_title" = "Delete Conversation";

@ -60,14 +60,6 @@
"BUTTON_DONE" = "Hotovo";
/* Button text to enable batch selection mode */
"BUTTON_SELECT" = "Vybrať";
/* Alert body */
"CONFIRM_LEAVE_GROUP_DESCRIPTION" = "Už nebudete môcť posielať a prijímať správy v tejto skupine.";
/* Alert title */
"CONFIRM_LEAVE_GROUP_TITLE" = "Ste si istý/á, že chcete odísť?";
/* Message for the 'conversation delete confirmation' alert. */
"CONVERSATION_DELETE_CONFIRMATION_ALERT_MESSAGE" = "Táto akcia sa nedá vrátiť.";
/* Title for the 'conversation delete confirmation' alert. */
"CONVERSATION_DELETE_CONFIRMATION_ALERT_TITLE" = "Zmazať konverzáciu?";
/* keyboard toolbar label when starting to search with no current results */
"CONVERSATION_SEARCH_SEARCHING" = "Searching...";
/* keyboard toolbar label when no messages match the search string */
@ -609,3 +601,15 @@
"context_menu_resync" = "Resync";
"GIPHY_PERMISSION_TITLE" = "Search GIFs?";
"GIPHY_PERMISSION_MESSAGE" = "Session will connect to Giphy to provide search results. You will not have full metadata protection when sending GIFs.";
"mute_button_text" = "Mute";
"unmute_button_text" = "Unmute";
"mark_read_button_text" = "Mark read";
"mark_unread_button_text" = "Mark unread";
"leave_group_confirmation_alert_title" = "Leave Group";
"leave_community_confirmation_alert_title" = "Leave Community";
"leave_community_confirmation_alert_message" = "Are you sure you want to leave %@?";
"group_you_leaving" = "Leaving...";
"group_leave_error" = "Failed to leave Group!";
"group_unable_to_leave" = "Unable to leave the Group, please try again";
"delete_conversation_confirmation_alert_message" = "Are you sure you want to delete your conversation with %@?";
"delete_conversation_confirmation_alert_title" = "Delete Conversation";

@ -60,14 +60,6 @@
"BUTTON_DONE" = "Klart";
/* Button text to enable batch selection mode */
"BUTTON_SELECT" = "Välj";
/* Alert body */
"CONFIRM_LEAVE_GROUP_DESCRIPTION" = "Du kommer inte längre att kunna skicka eller ta emot meddelanden i denna grupp.";
/* Alert title */
"CONFIRM_LEAVE_GROUP_TITLE" = "Vill du verkligen lämna?";
/* Message for the 'conversation delete confirmation' alert. */
"CONVERSATION_DELETE_CONFIRMATION_ALERT_MESSAGE" = "Detta kan inte ångras.";
/* Title for the 'conversation delete confirmation' alert. */
"CONVERSATION_DELETE_CONFIRMATION_ALERT_TITLE" = "Radera konversation?";
/* keyboard toolbar label when starting to search with no current results */
"CONVERSATION_SEARCH_SEARCHING" = "Searching...";
/* keyboard toolbar label when no messages match the search string */
@ -609,3 +601,15 @@
"context_menu_resync" = "Resync";
"GIPHY_PERMISSION_TITLE" = "Search GIFs?";
"GIPHY_PERMISSION_MESSAGE" = "Session will connect to Giphy to provide search results. You will not have full metadata protection when sending GIFs.";
"mute_button_text" = "Mute";
"unmute_button_text" = "Unmute";
"mark_read_button_text" = "Mark read";
"mark_unread_button_text" = "Mark unread";
"leave_group_confirmation_alert_title" = "Leave Group";
"leave_community_confirmation_alert_title" = "Leave Community";
"leave_community_confirmation_alert_message" = "Are you sure you want to leave %@?";
"group_you_leaving" = "Leaving...";
"group_leave_error" = "Failed to leave Group!";
"group_unable_to_leave" = "Unable to leave the Group, please try again";
"delete_conversation_confirmation_alert_message" = "Are you sure you want to delete your conversation with %@?";
"delete_conversation_confirmation_alert_title" = "Delete Conversation";

@ -60,14 +60,6 @@
"BUTTON_DONE" = "เสร็จ";
/* Button text to enable batch selection mode */
"BUTTON_SELECT" = "เลือก";
/* Alert body */
"CONFIRM_LEAVE_GROUP_DESCRIPTION" = "คุณจะไม่สามารถส่งและรับข้อความในกลุ่มนี้ได้อีกต่อไป";
/* Alert title */
"CONFIRM_LEAVE_GROUP_TITLE" = "แน่ใจออกจากไหม";
/* Message for the 'conversation delete confirmation' alert. */
"CONVERSATION_DELETE_CONFIRMATION_ALERT_MESSAGE" = "การกระทำนี้ไม่สามารถยกเลิกได้";
/* Title for the 'conversation delete confirmation' alert. */
"CONVERSATION_DELETE_CONFIRMATION_ALERT_TITLE" = "ลบการสนทนาไหม";
/* keyboard toolbar label when starting to search with no current results */
"CONVERSATION_SEARCH_SEARCHING" = "Searching...";
/* keyboard toolbar label when no messages match the search string */
@ -609,3 +601,15 @@
"context_menu_resync" = "Resync";
"GIPHY_PERMISSION_TITLE" = "Search GIFs?";
"GIPHY_PERMISSION_MESSAGE" = "Session will connect to Giphy to provide search results. You will not have full metadata protection when sending GIFs.";
"mute_button_text" = "Mute";
"unmute_button_text" = "Unmute";
"mark_read_button_text" = "Mark read";
"mark_unread_button_text" = "Mark unread";
"leave_group_confirmation_alert_title" = "Leave Group";
"leave_community_confirmation_alert_title" = "Leave Community";
"leave_community_confirmation_alert_message" = "Are you sure you want to leave %@?";
"group_you_leaving" = "Leaving...";
"group_leave_error" = "Failed to leave Group!";
"group_unable_to_leave" = "Unable to leave the Group, please try again";
"delete_conversation_confirmation_alert_message" = "Are you sure you want to delete your conversation with %@?";
"delete_conversation_confirmation_alert_title" = "Delete Conversation";

@ -60,14 +60,6 @@
"BUTTON_DONE" = "Xong";
/* Button text to enable batch selection mode */
"BUTTON_SELECT" = "Chọn";
/* Alert body */
"CONFIRM_LEAVE_GROUP_DESCRIPTION" = "Bạn sẽ không thể gửi hoặc nhận tin nhắn trong nhóm này nữa.";
/* Alert title */
"CONFIRM_LEAVE_GROUP_TITLE" = "Bạn thực sự muốn rời khỏi nhóm?";
/* Message for the 'conversation delete confirmation' alert. */
"CONVERSATION_DELETE_CONFIRMATION_ALERT_MESSAGE" = "Tác vụ này không thể hoàn tất.";
/* Title for the 'conversation delete confirmation' alert. */
"CONVERSATION_DELETE_CONFIRMATION_ALERT_TITLE" = "Xóa cuộc hội thoại?";
/* keyboard toolbar label when starting to search with no current results */
"CONVERSATION_SEARCH_SEARCHING" = "Searching...";
/* keyboard toolbar label when no messages match the search string */
@ -609,3 +601,15 @@
"context_menu_resync" = "Resync";
"GIPHY_PERMISSION_TITLE" = "Search GIFs?";
"GIPHY_PERMISSION_MESSAGE" = "Session will connect to Giphy to provide search results. You will not have full metadata protection when sending GIFs.";
"mute_button_text" = "Mute";
"unmute_button_text" = "Unmute";
"mark_read_button_text" = "Mark read";
"mark_unread_button_text" = "Mark unread";
"leave_group_confirmation_alert_title" = "Leave Group";
"leave_community_confirmation_alert_title" = "Leave Community";
"leave_community_confirmation_alert_message" = "Are you sure you want to leave %@?";
"group_you_leaving" = "Leaving...";
"group_leave_error" = "Failed to leave Group!";
"group_unable_to_leave" = "Unable to leave the Group, please try again";
"delete_conversation_confirmation_alert_message" = "Are you sure you want to delete your conversation with %@?";
"delete_conversation_confirmation_alert_title" = "Delete Conversation";

@ -60,14 +60,6 @@
"BUTTON_DONE" = "完成";
/* Button text to enable batch selection mode */
"BUTTON_SELECT" = "選擇";
/* Alert body */
"CONFIRM_LEAVE_GROUP_DESCRIPTION" = "您已經無法再於此群組傳送或接收訊息。";
/* Alert title */
"CONFIRM_LEAVE_GROUP_TITLE" = "確定要離開嗎?";
/* Message for the 'conversation delete confirmation' alert. */
"CONVERSATION_DELETE_CONFIRMATION_ALERT_MESSAGE" = "此操作無法復原。";
/* Title for the 'conversation delete confirmation' alert. */
"CONVERSATION_DELETE_CONFIRMATION_ALERT_TITLE" = "刪除對話?";
/* keyboard toolbar label when starting to search with no current results */
"CONVERSATION_SEARCH_SEARCHING" = "Searching...";
/* keyboard toolbar label when no messages match the search string */
@ -609,3 +601,15 @@
"context_menu_resync" = "Resync";
"GIPHY_PERMISSION_TITLE" = "Search GIFs?";
"GIPHY_PERMISSION_MESSAGE" = "Session will connect to Giphy to provide search results. You will not have full metadata protection when sending GIFs.";
"mute_button_text" = "Mute";
"unmute_button_text" = "Unmute";
"mark_read_button_text" = "Mark read";
"mark_unread_button_text" = "Mark unread";
"leave_group_confirmation_alert_title" = "Leave Group";
"leave_community_confirmation_alert_title" = "Leave Community";
"leave_community_confirmation_alert_message" = "Are you sure you want to leave %@?";
"group_you_leaving" = "Leaving...";
"group_leave_error" = "Failed to leave Group!";
"group_unable_to_leave" = "Unable to leave the Group, please try again";
"delete_conversation_confirmation_alert_message" = "Are you sure you want to delete your conversation with %@?";
"delete_conversation_confirmation_alert_title" = "Delete Conversation";

@ -60,14 +60,6 @@
"BUTTON_DONE" = "完成";
/* Button text to enable batch selection mode */
"BUTTON_SELECT" = "选择";
/* Alert body */
"CONFIRM_LEAVE_GROUP_DESCRIPTION" = "您将无法在此群组中继续发送或接收消息。";
/* Alert title */
"CONFIRM_LEAVE_GROUP_TITLE" = "确定离开群聊?";
/* Message for the 'conversation delete confirmation' alert. */
"CONVERSATION_DELETE_CONFIRMATION_ALERT_MESSAGE" = "该操作无法撤销。";
/* Title for the 'conversation delete confirmation' alert. */
"CONVERSATION_DELETE_CONFIRMATION_ALERT_TITLE" = "删除会话?";
/* keyboard toolbar label when starting to search with no current results */
"CONVERSATION_SEARCH_SEARCHING" = "Searching...";
/* keyboard toolbar label when no messages match the search string */
@ -609,3 +601,15 @@
"context_menu_resync" = "Resync";
"GIPHY_PERMISSION_TITLE" = "Search GIFs?";
"GIPHY_PERMISSION_MESSAGE" = "Session will connect to Giphy to provide search results. You will not have full metadata protection when sending GIFs.";
"mute_button_text" = "Mute";
"unmute_button_text" = "Unmute";
"mark_read_button_text" = "Mark read";
"mark_unread_button_text" = "Mark unread";
"leave_group_confirmation_alert_title" = "Leave Group";
"leave_community_confirmation_alert_title" = "Leave Community";
"leave_community_confirmation_alert_message" = "Are you sure you want to leave %@?";
"group_you_leaving" = "Leaving...";
"group_leave_error" = "Failed to leave Group!";
"group_unable_to_leave" = "Unable to leave the Group, please try again";
"delete_conversation_confirmation_alert_message" = "Are you sure you want to delete your conversation with %@?";
"delete_conversation_confirmation_alert_title" = "Delete Conversation";

@ -236,6 +236,7 @@ public class NotificationPresenter: NSObject, NotificationsProtocol {
let userPublicKey: String = getUserHexEncodedPublicKey(db)
let userBlindedKey: String? = SessionThread.getUserHexEncodedBlindedKey(
db,
threadId: thread.id,
threadVariant: thread.variant
)

@ -346,7 +346,7 @@ public final class FullConversationCell: UITableViewCell {
}
else {
accentLineView.themeBackgroundColor = .conversationButton_unreadStripBackground
accentLineView.alpha = (unreadCount > 0 ? 1 : 0.0001) // Setting the alpha to exactly 0 causes an issue on iOS 12
accentLineView.alpha = (unreadCount > 0 ? 1 : 0)
}
isPinnedIcon.isHidden = !cellViewModel.threadIsPinned
@ -384,12 +384,34 @@ public final class FullConversationCell: UITableViewCell {
typingIndicatorView.stopAnimation()
ThemeManager.onThemeChange(observer: snippetLabel) { [weak self, weak snippetLabel] theme, _ in
guard let textColor: UIColor = theme.color(for: .textPrimary) else { return }
snippetLabel?.attributedText = self?.getSnippet(
cellViewModel: cellViewModel,
textColor: textColor
)
if cellViewModel.interactionVariant == .infoClosedGroupCurrentUserLeaving {
guard let textColor: UIColor = theme.color(for: .textSecondary) else { return }
self?.displayNameLabel.themeTextColor = .textSecondary
snippetLabel?.attributedText = self?.getSnippet(
cellViewModel: cellViewModel,
textColor: textColor
)
} else if cellViewModel.interactionVariant == .infoClosedGroupCurrentUserErrorLeaving {
guard let textColor: UIColor = theme.color(for: .danger) else { return }
self?.displayNameLabel.themeTextColor = .textPrimary
snippetLabel?.attributedText = self?.getSnippet(
cellViewModel: cellViewModel,
textColor: textColor
)
} else {
guard let textColor: UIColor = theme.color(for: .textPrimary) else { return }
self?.displayNameLabel.themeTextColor = .textPrimary
snippetLabel?.attributedText = self?.getSnippet(
cellViewModel: cellViewModel,
textColor: textColor
)
}
}
}
@ -406,23 +428,33 @@ public final class FullConversationCell: UITableViewCell {
}
public func optimisticUpdate(
isBlocked: Bool? = nil,
isPinned: Bool? = nil
isMuted: Bool? = nil,
isPinned: Bool? = nil,
hasUnread: Bool? = nil
) {
if let isBlocked: Bool = isBlocked {
if isBlocked {
accentLineView.themeBackgroundColor = .danger
accentLineView.alpha = 1
}
else {
accentLineView.themeBackgroundColor = .conversationButton_unreadStripBackground
accentLineView.alpha = (!unreadCountView.isHidden ? 1 : 0.0001) // Setting the alpha to exactly 0 causes an issue on iOS 12
if let isMuted: Bool = isMuted {
if isMuted {
} else {
}
}
if let isPinned: Bool = isPinned {
isPinnedIcon.isHidden = !isPinned
}
if let hasUnread: Bool = hasUnread {
if hasUnread {
unreadCountView.isHidden = false
unreadCountLabel.text = "1"
unreadCountLabel.font = .boldSystemFont(ofSize: Values.verySmallFontSize)
accentLineView.alpha = 1
} else {
unreadCountView.isHidden = true
accentLineView.alpha = 0
}
}
}
// MARK: - Snippet generation
@ -461,7 +493,10 @@ public final class FullConversationCell: UITableViewCell {
))
}
if cellViewModel.threadVariant == .closedGroup || cellViewModel.threadVariant == .openGroup {
if
(cellViewModel.threadVariant == .closedGroup || cellViewModel.threadVariant == .openGroup) &&
(cellViewModel.interactionVariant?.isGroupControlMessage == false)
{
let authorName: String = cellViewModel.authorName(for: cellViewModel.threadVariant)
result.append(NSAttributedString(
@ -470,17 +505,22 @@ public final class FullConversationCell: UITableViewCell {
))
}
let previewText: String = {
if cellViewModel.interactionVariant == .infoClosedGroupCurrentUserErrorLeaving { return "group_leave_error".localized() }
return Interaction.previewText(
variant: (cellViewModel.interactionVariant ?? .standardIncoming),
body: cellViewModel.interactionBody,
threadContactDisplayName: cellViewModel.threadContactName(),
authorDisplayName: cellViewModel.authorName(for: cellViewModel.threadVariant),
attachmentDescriptionInfo: cellViewModel.interactionAttachmentDescriptionInfo,
attachmentCount: cellViewModel.interactionAttachmentCount,
isOpenGroupInvitation: (cellViewModel.interactionIsOpenGroupInvitation == true)
)
}()
result.append(NSAttributedString(
string: MentionUtilities.highlightMentionsNoAttributes(
in: Interaction.previewText(
variant: (cellViewModel.interactionVariant ?? .standardIncoming),
body: cellViewModel.interactionBody,
threadContactDisplayName: cellViewModel.threadContactName(),
authorDisplayName: cellViewModel.authorName(for: cellViewModel.threadVariant),
attachmentDescriptionInfo: cellViewModel.interactionAttachmentDescriptionInfo,
attachmentCount: cellViewModel.interactionAttachmentCount,
isOpenGroupInvitation: (cellViewModel.interactionIsOpenGroupInvitation == true)
),
in: previewText,
threadVariant: cellViewModel.threadVariant,
currentUserPublicKey: cellViewModel.currentUserPublicKey,
currentUserBlindedPublicKey: cellViewModel.currentUserBlindedPublicKey

@ -25,7 +25,8 @@ public enum SNMessagingKit { // Just to make the external API nice
_008_EmojiReacts.self,
_009_OpenGroupPermission.self,
_010_AddThreadIdToFTS.self,
_011_AddPendingReadReceipts.self
_011_AddPendingReadReceipts.self,
_012_AddFTSIfNeeded.self
]
]
)
@ -45,5 +46,6 @@ public enum SNMessagingKit { // Just to make the external API nice
JobRunner.add(executor: SendReadReceiptsJob.self, for: .sendReadReceipts)
JobRunner.add(executor: AttachmentDownloadJob.self, for: .attachmentDownload)
JobRunner.add(executor: AttachmentUploadJob.self, for: .attachmentUpload)
JobRunner.add(executor: GroupLeavingJob.self, for: .groupLeaving)
}
}

@ -13,13 +13,6 @@ enum _011_AddPendingReadReceipts: Migration {
static let minExpectedRunDuration: TimeInterval = 0.1
static func migrate(_ db: Database) throws {
// Can't actually alter a virtual table in SQLite so we need to drop and recreate it,
// luckily this is actually pretty quick
if try db.tableExists(Interaction.fullTextSearchTableName) {
try db.drop(table: Interaction.fullTextSearchTableName)
try db.dropFTS5SynchronizationTriggers(forTable: Interaction.fullTextSearchTableName)
}
try db.create(table: PendingReadReceipt.self) { t in
t.column(.threadId, .text)
.notNull()

@ -0,0 +1,29 @@
// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved.
import Foundation
import GRDB
import SessionUtilitiesKit
/// This migration adds the FTS table back for internal test users whose FTS table was removed unintentionally
enum _012_AddFTSIfNeeded: Migration {
static let target: TargetMigrations.Identifier = .messagingKit
static let identifier: String = "AddFTSIfNeeded"
static let needsConfigSync: Bool = false
static let minExpectedRunDuration: TimeInterval = 0.1
static func migrate(_ db: Database) throws {
// Fix an issue that the fullTextSearchTable was dropped unintentionally and global search won't work.
// This issue only happens to internal test users.
if try db.tableExists(Interaction.fullTextSearchTableName) == false {
try db.create(virtualTable: Interaction.fullTextSearchTableName, using: FTS5()) { t in
t.synchronize(withTable: Interaction.databaseTableName)
t.tokenizer = _001_InitialSetupMigration.fullTextSearchTokenizer
t.column(Interaction.Columns.body.name)
t.column(Interaction.Columns.threadId.name)
}
}
Storage.update(progress: 1, for: self, in: target) // In case this is the last migration
}
}

@ -883,9 +883,10 @@ extension Attachment {
let cloneId: String = UUID().uuidString
let thumbnailName: String = "quoted-thumbnail-\(sourceFilename ?? "null")"
guard self.isVisualMedia else { return nil }
guard
self.isValid,
self.isVisualMedia,
let thumbnailPath: String = Attachment.originalFilePath(
id: cloneId,
mimeType: OWSMimeTypeImageJpeg,

@ -151,7 +151,7 @@ internal extension ControlMessageProcessRecord {
.infoClosedGroupCreated:
return nil
case .infoClosedGroupUpdated, .infoClosedGroupCurrentUserLeft:
case .infoClosedGroupUpdated, .infoClosedGroupCurrentUserLeft, .infoClosedGroupCurrentUserLeaving, .infoClosedGroupCurrentUserErrorLeaving:
self.variant = .closedGroupControlMessage
case .infoDisappearingMessagesUpdate:

@ -73,6 +73,8 @@ public struct Interaction: Codable, Identifiable, Equatable, FetchableRecord, Mu
case infoClosedGroupCreated = 1000
case infoClosedGroupUpdated
case infoClosedGroupCurrentUserLeft
case infoClosedGroupCurrentUserErrorLeaving
case infoClosedGroupCurrentUserLeaving
case infoDisappearingMessagesUpdate = 2000
@ -87,7 +89,8 @@ public struct Interaction: Codable, Identifiable, Equatable, FetchableRecord, Mu
public var isInfoMessage: Bool {
switch self {
case .infoClosedGroupCreated, .infoClosedGroupUpdated, .infoClosedGroupCurrentUserLeft,
case .infoClosedGroupCreated, .infoClosedGroupUpdated,
.infoClosedGroupCurrentUserLeft, .infoClosedGroupCurrentUserLeaving, .infoClosedGroupCurrentUserErrorLeaving,
.infoDisappearingMessagesUpdate, .infoScreenshotNotification, .infoMediaSavedNotification,
.infoMessageRequestAccepted, .infoCall:
return true
@ -97,6 +100,25 @@ public struct Interaction: Codable, Identifiable, Equatable, FetchableRecord, Mu
}
}
public var isGroupControlMessage: Bool {
switch self {
case .infoClosedGroupCreated, .infoClosedGroupUpdated,
.infoClosedGroupCurrentUserLeft, .infoClosedGroupCurrentUserLeaving, .infoClosedGroupCurrentUserErrorLeaving:
return true
default:
return false
}
}
public var isGroupLeavingStatus: Bool {
switch self {
case .infoClosedGroupCurrentUserLeft, .infoClosedGroupCurrentUserLeaving, .infoClosedGroupCurrentUserErrorLeaving:
return true
default:
return false
}
}
/// This flag controls whether the `wasRead` flag is automatically set to true based on the message variant (as a result it they will
/// or won't affect the unread count)
fileprivate var canBeUnread: Bool {
@ -106,7 +128,8 @@ public struct Interaction: Codable, Identifiable, Equatable, FetchableRecord, Mu
case .standardOutgoing, .standardIncomingDeleted: return false
case .infoClosedGroupCreated, .infoClosedGroupUpdated, .infoClosedGroupCurrentUserLeft,
case .infoClosedGroupCreated, .infoClosedGroupUpdated,
.infoClosedGroupCurrentUserLeft, .infoClosedGroupCurrentUserLeaving, .infoClosedGroupCurrentUserErrorLeaving,
.infoDisappearingMessagesUpdate, .infoScreenshotNotification, .infoMediaSavedNotification,
.infoMessageRequestAccepted:
return false
@ -846,6 +869,8 @@ public extension Interaction {
case .infoClosedGroupCreated: return "GROUP_CREATED".localized()
case .infoClosedGroupCurrentUserLeft: return "GROUP_YOU_LEFT".localized()
case .infoClosedGroupCurrentUserLeaving: return "group_you_leaving".localized()
case .infoClosedGroupCurrentUserErrorLeaving: return "group_unable_to_leave".localized()
case .infoClosedGroupUpdated: return (body ?? "GROUP_UPDATED".localized())
case .infoMessageRequestAccepted: return (body ?? "MESSAGE_REQUESTS_ACCEPTED".localized())

@ -226,7 +226,7 @@ public extension SessionThread {
///
/// **Note:** In order to use this filter you **MUST** have a `joining(required/optional:)` to the
/// `SessionThread.contact` association or it won't work
static func isMessageRequest(userPublicKey: String, includeNonVisible: Bool = false) -> SQLSpecificExpressible {
static func isMessageRequest(userPublicKey: String, includeNonVisible: Bool = false) -> SQLExpression {
let thread: TypedTableAlias<SessionThread> = TypedTableAlias()
let contact: TypedTableAlias<Contact> = TypedTableAlias()
let shouldBeVisibleSQL: SQL = (includeNonVisible ?
@ -241,7 +241,7 @@ public extension SessionThread {
\(SQL("\(thread[.id]) != \(userPublicKey)")) AND
IFNULL(\(contact[.isApproved]), false) = false
"""
)
).sqlExpression
}
func isNoteToSelf(_ db: Database? = nil) -> Bool {
@ -317,42 +317,46 @@ public extension SessionThread {
}
static func getUserHexEncodedBlindedKey(
_ db: Database? = nil,
threadId: String,
threadVariant: Variant
) -> String? {
guard threadVariant == .openGroup else { return nil }
guard let db: Database = db else {
return Storage.shared.read { db in
getUserHexEncodedBlindedKey(db, threadId: threadId, threadVariant: threadVariant)
}
}
// Retrieve the relevant open group info
struct OpenGroupInfo: Decodable, FetchableRecord {
let publicKey: String
let server: String
}
guard
threadVariant == .openGroup,
let blindingInfo: (edkeyPair: Box.KeyPair?, publicKey: String?, capabilities: Set<Capability.Variant>) = Storage.shared.read({ db in
struct OpenGroupInfo: Decodable, FetchableRecord {
let publicKey: String?
let server: String?
}
let openGroupInfo: OpenGroupInfo? = try OpenGroup
.filter(id: threadId)
.select(.publicKey, .server)
.asRequest(of: OpenGroupInfo.self)
.fetchOne(db)
return (
Identity.fetchUserEd25519KeyPair(db),
openGroupInfo?.publicKey,
(try? Capability
.select(.variant)
.filter(Capability.Columns.openGroupServer == openGroupInfo?.server?.lowercased())
.asRequest(of: Capability.Variant.self)
.fetchSet(db))
.defaulting(to: [])
)
}),
let userEdKeyPair: Box.KeyPair = blindingInfo.edkeyPair,
let publicKey: String = blindingInfo.publicKey,
blindingInfo.capabilities.isEmpty || blindingInfo.capabilities.contains(.blind)
let userEdKeyPair: Box.KeyPair = Identity.fetchUserEd25519KeyPair(db),
let openGroupInfo: OpenGroupInfo = try? OpenGroup
.filter(id: threadId)
.select(.publicKey, .server)
.asRequest(of: OpenGroupInfo.self)
.fetchOne(db)
else { return nil }
// Check the capabilities to ensure the SOGS is blinded (or whether we have no capabilities)
let capabilities: Set<Capability.Variant> = (try? Capability
.select(.variant)
.filter(Capability.Columns.openGroupServer == openGroupInfo.server.lowercased())
.asRequest(of: Capability.Variant.self)
.fetchSet(db))
.defaulting(to: [])
guard capabilities.isEmpty || capabilities.contains(.blind) else { return nil }
let sodium: Sodium = Sodium()
let blindedKeyPair: Box.KeyPair? = sodium.blindedKeyPair(
serverPublicKey: publicKey,
serverPublicKey: openGroupInfo.publicKey,
edKeyPair: userEdKeyPair,
genericHash: sodium.getGenericHash()
)

@ -25,7 +25,7 @@ public enum AttachmentDownloadJob: JobExecutor {
let attachment: Attachment = Storage.shared
.read({ db in try Attachment.fetchOne(db, id: details.attachmentId) })
else {
failure(job, JobRunnerError.missingRequiredDetails, false)
failure(job, JobRunnerError.missingRequiredDetails, true)
return
}

@ -31,7 +31,7 @@ public enum AttachmentUploadJob: JobExecutor {
return (attachment, try OpenGroup.fetchOne(db, id: threadId))
})
else {
failure(job, JobRunnerError.missingRequiredDetails, false)
failure(job, JobRunnerError.missingRequiredDetails, true)
return
}

@ -0,0 +1,166 @@
// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved.
import Foundation
import GRDB
import PromiseKit
import SignalCoreKit
import SessionUtilitiesKit
import SessionSnodeKit
public enum GroupLeavingJob: JobExecutor {
public static var maxFailureCount: Int = 0
public static var requiresThreadId: Bool = true
public static var requiresInteractionId: Bool = true
public static func run(
_ job: SessionUtilitiesKit.Job,
queue: DispatchQueue,
success: @escaping (SessionUtilitiesKit.Job, Bool) -> (),
failure: @escaping (SessionUtilitiesKit.Job, Error?, Bool) -> (),
deferred: @escaping (SessionUtilitiesKit.Job) -> ())
{
guard
let detailsData: Data = job.details,
let details: Details = try? JSONDecoder().decode(Details.self, from: detailsData),
let interactionId: Int64 = job.interactionId
else {
failure(job, JobRunnerError.missingRequiredDetails, true)
return
}
guard let thread: SessionThread = Storage.shared.read({ db in try? SessionThread.fetchOne(db, id: details.groupPublicKey)}) else {
SNLog("Can't leave nonexistent closed group.")
failure(job, MessageSenderError.noThread, true)
return
}
guard let closedGroup: ClosedGroup = Storage.shared.read({ db in try? thread.closedGroup.fetchOne(db)}) else {
failure(job, MessageSenderError.invalidClosedGroupUpdate, true)
return
}
Storage.shared.writeAsync { db -> Promise<Void> in
try MessageSender.sendNonDurably(
db,
message: ClosedGroupControlMessage(
kind: .memberLeft
),
interactionId: interactionId,
in: thread
)
}
.done(on: queue) { _ in
// Remove the group from the database and unsubscribe from PNs
ClosedGroupPoller.shared.stopPolling(for: details.groupPublicKey)
Storage.shared.writeAsync { db in
let userPublicKey: String = getUserHexEncodedPublicKey(db)
try closedGroup
.keyPairs
.deleteAll(db)
let _ = PushNotificationAPI.performOperation(
.unsubscribe,
for: details.groupPublicKey,
publicKey: userPublicKey
)
try Interaction
.filter(id: interactionId)
.updateAll(
db,
[
Interaction.Columns.variant.set(to: Interaction.Variant.infoClosedGroupCurrentUserLeft),
Interaction.Columns.body.set(to: "GROUP_YOU_LEFT".localized())
]
)
// Update the group (if the admin leaves the group is disbanded)
let wasAdminUser: Bool = try GroupMember
.filter(GroupMember.Columns.groupId == thread.id)
.filter(GroupMember.Columns.profileId == userPublicKey)
.filter(GroupMember.Columns.role == GroupMember.Role.admin)
.isNotEmpty(db)
if wasAdminUser {
try GroupMember
.filter(GroupMember.Columns.groupId == thread.id)
.deleteAll(db)
}
else {
try GroupMember
.filter(GroupMember.Columns.groupId == thread.id)
.filter(GroupMember.Columns.profileId == userPublicKey)
.deleteAll(db)
}
if details.deleteThread {
_ = try SessionThread
.filter(id: thread.id)
.deleteAll(db)
}
}
success(job, false)
}
.catch(on: queue) { error in
Storage.shared.writeAsync { db in
try Interaction
.filter(id: job.interactionId)
.updateAll(
db,
[
Interaction.Columns.variant.set(to: Interaction.Variant.infoClosedGroupCurrentUserErrorLeaving),
Interaction.Columns.body.set(to: "group_unable_to_leave".localized())
]
)
}
success(job, false)
}
.retainUntilComplete()
}
}
// MARK: - GroupLeavingJob.Details
extension GroupLeavingJob {
public struct Details: Codable {
private enum CodingKeys: String, CodingKey {
case groupPublicKey
case deleteThread
}
public let groupPublicKey: String
public let deleteThread: Bool
// MARK: - Initialization
public init(
groupPublicKey: String,
deleteThread: Bool
) {
self.groupPublicKey = groupPublicKey
self.deleteThread = deleteThread
}
// MARK: - Codable
public init(from decoder: Decoder) throws {
let container: KeyedDecodingContainer<CodingKeys> = try decoder.container(keyedBy: CodingKeys.self)
self = Details(
groupPublicKey: try container.decode(String.self, forKey: .groupPublicKey),
deleteThread: try container.decode(Bool.self, forKey: .deleteThread)
)
}
public func encode(to encoder: Encoder) throws {
var container: KeyedEncodingContainer<CodingKeys> = encoder.container(keyedBy: CodingKeys.self)
try container.encode(groupPublicKey, forKey: .groupPublicKey)
try container.encode(deleteThread, forKey: .deleteThread)
}
}
}

@ -21,7 +21,7 @@ public enum MessageReceiveJob: JobExecutor {
let detailsData: Data = job.details,
let details: Details = try? JSONDecoder().decode(Details.self, from: detailsData)
else {
failure(job, JobRunnerError.missingRequiredDetails, false)
failure(job, JobRunnerError.missingRequiredDetails, true)
return
}

@ -23,7 +23,7 @@ public enum MessageSendJob: JobExecutor {
let detailsData: Data = job.details,
let details: Details = try? JSONDecoder().decode(Details.self, from: detailsData)
else {
failure(job, JobRunnerError.missingRequiredDetails, false)
failure(job, JobRunnerError.missingRequiredDetails, true)
return
}
@ -36,7 +36,7 @@ public enum MessageSendJob: JobExecutor {
let jobId: Int64 = job.id,
let interactionId: Int64 = job.interactionId
else {
failure(job, JobRunnerError.missingRequiredDetails, false)
failure(job, JobRunnerError.missingRequiredDetails, true)
return
}

@ -21,7 +21,7 @@ public enum NotifyPushServerJob: JobExecutor {
let detailsData: Data = job.details,
let details: Details = try? JSONDecoder().decode(Details.self, from: detailsData)
else {
failure(job, JobRunnerError.missingRequiredDetails, false)
failure(job, JobRunnerError.missingRequiredDetails, true)
return
}

@ -23,7 +23,7 @@ public enum SendReadReceiptsJob: JobExecutor {
let detailsData: Data = job.details,
let details: Details = try? JSONDecoder().decode(Details.self, from: detailsData)
else {
failure(job, JobRunnerError.missingRequiredDetails, false)
failure(job, JobRunnerError.missingRequiredDetails, true)
return
}

@ -27,8 +27,13 @@ public final class DataExtractionNotification: ControlMessage {
// MARK: - Initialization
public init(kind: Kind) {
super.init()
public init(
kind: Kind,
sentTimestamp: UInt64? = nil
) {
super.init(
sentTimestamp: sentTimestamp
)
self.kind = kind
}

@ -370,6 +370,7 @@ public extension Message {
let userPublicKey: String = getUserHexEncodedPublicKey(db)
let blindedUserPublicKey: String? = SessionThread
.getUserHexEncodedBlindedKey(
db,
threadId: openGroupId,
threadVariant: .openGroup
)

@ -478,13 +478,9 @@ extension MessageSender {
/// unregisters from push notifications.
///
/// The returned promise is fulfilled when the `MEMBER_LEFT` message has been sent to the group.
public static func leave(_ db: Database, groupPublicKey: String) throws -> Promise<Void> {
public static func leave(_ db: Database, groupPublicKey: String, deleteThread: Bool) throws {
guard let thread: SessionThread = try? SessionThread.fetchOne(db, id: groupPublicKey) else {
SNLog("Can't leave nonexistent closed group.")
return Promise(error: MessageSenderError.noThread)
}
guard let closedGroup: ClosedGroup = try? thread.closedGroup.fetchOne(db) else {
return Promise(error: MessageSenderError.invalidClosedGroupUpdate)
return
}
let userPublicKey: String = getUserHexEncodedPublicKey(db)
@ -493,66 +489,23 @@ extension MessageSender {
let interaction: Interaction = try Interaction(
threadId: thread.id,
authorId: userPublicKey,
variant: .infoClosedGroupCurrentUserLeft,
body: ClosedGroupControlMessage.Kind
.memberLeft
.infoMessage(db, sender: userPublicKey),
variant: .infoClosedGroupCurrentUserLeaving,
body: "group_you_leaving".localized(),
timestampMs: SnodeAPI.currentOffsetTimestampMs()
).inserted(db)
guard let interactionId: Int64 = interaction.id else {
throw StorageError.objectNotSaved
}
// Send the update to the group
let promise = try MessageSender
.sendNonDurably(
db,
message: ClosedGroupControlMessage(
kind: .memberLeft
),
interactionId: interactionId,
in: thread
JobRunner.upsert(
db,
job: Job(
variant: .groupLeaving,
threadId: thread.id,
interactionId: interaction.id,
details: GroupLeavingJob.Details(
groupPublicKey: groupPublicKey,
deleteThread: deleteThread
)
)
.done {
// Remove the group from the database and unsubscribe from PNs
ClosedGroupPoller.shared.stopPolling(for: groupPublicKey)
Storage.shared.write { db in
try closedGroup
.keyPairs
.deleteAll(db)
let _ = PushNotificationAPI.performOperation(
.unsubscribe,
for: groupPublicKey,
publicKey: userPublicKey
)
}
}
.map { _ in }
// Update the group (if the admin leaves the group is disbanded)
let wasAdminUser: Bool = try GroupMember
.filter(GroupMember.Columns.groupId == thread.id)
.filter(GroupMember.Columns.profileId == userPublicKey)
.filter(GroupMember.Columns.role == GroupMember.Role.admin)
.isNotEmpty(db)
if wasAdminUser {
try GroupMember
.filter(GroupMember.Columns.groupId == thread.id)
.deleteAll(db)
}
else {
try GroupMember
.filter(GroupMember.Columns.groupId == thread.id)
.filter(GroupMember.Columns.profileId == userPublicKey)
.deleteAll(db)
}
// Return
return promise
)
}
/*

@ -763,26 +763,3 @@ public final class MessageSender {
}
}
}
// MARK: - Objective-C Support
// FIXME: Remove when possible
@objc(SMKMessageSender)
public class SMKMessageSender: NSObject {
@objc(leaveClosedGroupWithPublicKey:)
public static func objc_leave(_ groupPublicKey: String) -> AnyPromise {
let promise = Storage.shared.writeAsync { db in
try MessageSender.leave(db, groupPublicKey: groupPublicKey)
}
return AnyPromise.from(promise)
}
@objc(forceSyncConfigurationNow)
public static func objc_forceSyncConfigurationNow() {
Storage.shared.write { db in
try MessageSender.syncConfiguration(db, forceSyncNow: true).retainUntilComplete()
}
}
}

@ -100,7 +100,7 @@ public struct SessionThreadViewModel: FetchableRecordWithRowId, Decodable, Equat
public var canWrite: Bool {
switch threadVariant {
case .contact: return true
case .closedGroup: return currentUserIsClosedGroupMember == true
case .closedGroup: return (currentUserIsClosedGroupMember == true) && (interactionVariant?.isGroupLeavingStatus != true)
case .openGroup: return openGroupPermissions?.contains(.write) ?? false
}
}
@ -360,6 +360,7 @@ public extension SessionThreadViewModel {
}
func populatingCurrentUserBlindedKey(
_ db: Database? = nil,
currentUserBlindedPublicKeyForThisThread: String? = nil
) -> SessionThreadViewModel {
return SessionThreadViewModel(
@ -411,6 +412,7 @@ public extension SessionThreadViewModel {
currentUserBlindedPublicKey: (
currentUserBlindedPublicKeyForThisThread ??
SessionThread.getUserHexEncodedBlindedKey(
db,
threadId: self.threadId,
threadVariant: self.threadVariant
)
@ -457,6 +459,9 @@ public extension SessionThreadViewModel {
let interactionAttachmentAttachmentIdColumnLiteral: SQL = SQL(stringLiteral: InteractionAttachment.Columns.attachmentId.name)
let interactionAttachmentInteractionIdColumnLiteral: SQL = SQL(stringLiteral: InteractionAttachment.Columns.interactionId.name)
let interactionAttachmentAlbumIndexColumnLiteral: SQL = SQL(stringLiteral: InteractionAttachment.Columns.albumIndex.name)
let groupMemberProfileIdColumnLiteral: SQL = SQL(stringLiteral: GroupMember.Columns.profileId.name)
let groupMemberRoleColumnLiteral: SQL = SQL(stringLiteral: GroupMember.Columns.role.name)
let groupMemberGroupIdColumnLiteral: SQL = SQL(stringLiteral: GroupMember.Columns.groupId.name)
/// **Note:** The `numColumnsBeforeProfiles` value **MUST** match the number of fields before
/// the `ViewModel.contactProfileKey` entry below otherwise the query will fail to
@ -464,7 +469,7 @@ public extension SessionThreadViewModel {
///
/// Explicitly set default values for the fields ignored for search results
let numColumnsBeforeProfiles: Int = 12
let numColumnsBetweenProfilesAndAttachmentInfo: Int = 11 // The attachment info columns will be combined
let numColumnsBetweenProfilesAndAttachmentInfo: Int = 12 // The attachment info columns will be combined
let request: SQLRequest<ViewModel> = """
SELECT
@ -488,7 +493,8 @@ public extension SessionThreadViewModel {
\(ViewModel.closedGroupProfileBackKey).*,
\(ViewModel.closedGroupProfileBackFallbackKey).*,
\(closedGroup[.name]) AS \(ViewModel.closedGroupNameKey),
(\(groupMember[.profileId]) IS NOT NULL) AS \(ViewModel.currentUserIsClosedGroupAdminKey),
(\(ViewModel.currentUserIsClosedGroupMemberKey).profileId IS NOT NULL) AS \(ViewModel.currentUserIsClosedGroupMemberKey),
(\(ViewModel.currentUserIsClosedGroupAdminKey).profileId IS NOT NULL) AS \(ViewModel.currentUserIsClosedGroupAdminKey),
\(openGroup[.name]) AS \(ViewModel.openGroupNameKey),
\(openGroup[.imageData]) AS \(ViewModel.openGroupProfilePictureDataKey),
@ -563,10 +569,15 @@ public extension SessionThreadViewModel {
LEFT JOIN \(Profile.self) AS \(ViewModel.contactProfileKey) ON \(ViewModel.contactProfileKey).\(profileIdColumnLiteral) = \(thread[.id])
LEFT JOIN \(OpenGroup.self) ON \(openGroup[.threadId]) = \(thread[.id])
LEFT JOIN \(ClosedGroup.self) ON \(closedGroup[.threadId]) = \(thread[.id])
LEFT JOIN \(GroupMember.self) ON (
\(SQL("\(groupMember[.role]) = \(GroupMember.Role.admin)")) AND
\(groupMember[.groupId]) = \(closedGroup[.threadId]) AND
\(SQL("\(groupMember[.profileId]) = \(userPublicKey)"))
LEFT JOIN \(GroupMember.self) AS \(ViewModel.currentUserIsClosedGroupMemberKey) ON (
\(SQL("\(ViewModel.currentUserIsClosedGroupMemberKey).\(groupMemberRoleColumnLiteral) != \(GroupMember.Role.zombie)")) AND
\(ViewModel.currentUserIsClosedGroupMemberKey).\(groupMemberGroupIdColumnLiteral) = \(closedGroup[.threadId]) AND
\(SQL("\(ViewModel.currentUserIsClosedGroupMemberKey).\(groupMemberProfileIdColumnLiteral) = \(userPublicKey)"))
)
LEFT JOIN \(GroupMember.self) AS \(ViewModel.currentUserIsClosedGroupAdminKey) ON (
\(SQL("\(ViewModel.currentUserIsClosedGroupAdminKey).\(groupMemberRoleColumnLiteral) = \(GroupMember.Role.admin)")) AND
\(ViewModel.currentUserIsClosedGroupAdminKey).\(groupMemberGroupIdColumnLiteral) = \(closedGroup[.threadId]) AND
\(SQL("\(ViewModel.currentUserIsClosedGroupAdminKey).\(groupMemberProfileIdColumnLiteral) = \(userPublicKey)"))
)
LEFT JOIN \(Profile.self) AS \(ViewModel.closedGroupProfileFrontKey) ON (

@ -55,6 +55,10 @@ public class TappableLabel: UILabel {
}
}
public var containsLinks: Bool {
return !links.isEmpty
}
// MARK: - Initialization
public override init(frame: CGRect) {

@ -0,0 +1,182 @@
// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved.
import UIKit
import SessionUtilitiesKit
public extension UIContextualAction {
private static var lookupMap: Atomic<[Int: [String: [Int: ThemeValue]]]> = Atomic([:])
enum Side: Int {
case leading
case trailing
func key(for indexPath: IndexPath) -> String {
return "\(indexPath.section)-\(indexPath.row)-\(rawValue)"
}
init?(for view: UIView) {
guard view.frame.minX == 0 else {
self = .trailing
return
}
self = .leading
}
}
convenience init(
title: String? = nil,
icon: UIImage? = nil,
iconHeight: CGFloat = Values.mediumFontSize,
themeTintColor: ThemeValue = .white,
themeBackgroundColor: ThemeValue,
side: Side,
actionIndex: Int,
indexPath: IndexPath,
tableView: UITableView,
handler: @escaping UIContextualAction.Handler
) {
self.init(style: .normal, title: title, handler: handler)
self.image = UIContextualAction
.imageWith(
title: title,
icon: icon,
iconHeight: iconHeight,
themeTintColor: themeTintColor
)?
.withRenderingMode(.alwaysTemplate)
self.themeBackgroundColor = themeBackgroundColor
UIContextualAction.lookupMap.mutate {
$0[tableView.hashValue] = ($0[tableView.hashValue] ?? [:])
.setting(
side.key(for: indexPath),
(($0[tableView.hashValue] ?? [:])[side.key(for: indexPath)] ?? [:])
.setting(actionIndex, themeTintColor)
)
}
}
private static func imageWith(
title: String?,
icon: UIImage?,
iconHeight: CGFloat,
themeTintColor: ThemeValue
) -> UIImage? {
let stackView: UIStackView = UIStackView()
stackView.axis = .vertical
stackView.alignment = .center
stackView.spacing = 4
if let icon: UIImage = icon {
let scale: Double = iconHeight / icon.size.height
let aspectRatio: CGFloat = (icon.size.width / icon.size.height)
let imageView: UIImageView = UIImageView(image: icon)
imageView.frame = CGRect(x: 0, y: 0, width: iconHeight * aspectRatio, height: iconHeight)
imageView.contentMode = .scaleAspectFit
imageView.themeTintColor = themeTintColor
stackView.addArrangedSubview(imageView)
}
if let title: String = title {
let label: UILabel = UILabel()
label.font = .systemFont(ofSize: Values.verySmallFontSize)
label.text = title
label.textAlignment = .center
label.themeTextColor = themeTintColor
label.minimumScaleFactor = 0.75
label.numberOfLines = (title.components(separatedBy: " ").count > 1 ? 2 : 1)
label.frame = CGRect(
origin: .zero,
// Note: It looks like there is a semi-max width of 68px for images in the swipe actions
// if the image ends up larger then there an odd behaviour can occur where 8/10 times the
// image is scaled down to fit, but ocassionally (primarily if you hide the action and
// immediately swipe to show it again once the cell hits the edge of the screen) the image
// won't be scaled down but will be full size - appearing as if two different images are used
size: label.sizeThatFits(CGSize(width: 68, height: 999))
)
label.set(.width, to: label.frame.width)
stackView.addArrangedSubview(label)
}
stackView.frame = CGRect(
origin: .zero,
size: stackView.systemLayoutSizeFitting(CGSize(width: 999, height: 999))
)
// Based on https://stackoverflow.com/a/41288197/1118398
let renderFormat: UIGraphicsImageRendererFormat = UIGraphicsImageRendererFormat()
renderFormat.scale = UIScreen.main.scale
let renderer: UIGraphicsImageRenderer = UIGraphicsImageRenderer(
size: stackView.bounds.size,
format: renderFormat
)
return renderer.image { rendererContext in
stackView.layer.render(in: rendererContext.cgContext)
}
}
private static func firstSubviewOfType<T>(in superview: UIView) -> T? {
guard !(superview is T) else { return superview as? T }
guard !superview.subviews.isEmpty else { return nil }
for subview in superview.subviews {
if let result: T = firstSubviewOfType(in: subview) {
return result
}
}
return nil
}
static func willBeginEditing(indexPath: IndexPath, tableView: UITableView) {
guard
let targetCell: UITableViewCell = tableView.cellForRow(at: indexPath),
targetCell.superview != tableView,
let targetSuperview: UIView = targetCell.superview?
.subviews
.filter({ $0 != targetCell })
.first,
let side: Side = Side(for: targetSuperview),
let themeMap: [Int: ThemeValue] = UIContextualAction.lookupMap.wrappedValue
.getting(tableView.hashValue)?
.getting(side.key(for: indexPath)),
targetSuperview.subviews.count == themeMap.count
else { return }
let targetViews: [UIImageView] = targetSuperview.subviews
.compactMap { subview in firstSubviewOfType(in: subview) }
guard targetViews.count == themeMap.count else { return }
// Set the imageView and background colours (so they change correctly when the theme changes)
targetViews.enumerated().forEach { index, targetView in
guard let themeTintColor: ThemeValue = themeMap[index] else { return }
targetView.themeTintColor = themeTintColor
}
}
static func didEndEditing(indexPath: IndexPath?, tableView: UITableView) {
guard let indexPath: IndexPath = indexPath else { return }
let leadingKey: String = Side.leading.key(for: indexPath)
let trailingKey: String = Side.trailing.key(for: indexPath)
guard
UIContextualAction.lookupMap.wrappedValue[tableView.hashValue]?[leadingKey] != nil ||
UIContextualAction.lookupMap.wrappedValue[tableView.hashValue]?[trailingKey] != nil
else { return }
UIContextualAction.lookupMap.mutate {
$0[tableView.hashValue]?[leadingKey] = nil
$0[tableView.hashValue]?[trailingKey] = nil
if $0[tableView.hashValue]?.isEmpty == true {
$0[tableView.hashValue] = nil
}
}
}
}

@ -3,7 +3,7 @@
import Foundation
import GRDB
public struct Job: Codable, Equatable, Identifiable, FetchableRecord, MutablePersistableRecord, TableRecord, ColumnExpressible {
public struct Job: Codable, Equatable, Hashable, Identifiable, FetchableRecord, MutablePersistableRecord, TableRecord, ColumnExpressible {
public static var databaseTableName: String { "job" }
internal static let dependencyForeignKey = ForeignKey([Columns.id], to: [JobDependencies.Columns.dependantId])
public static let dependantJobDependency = hasMany(
@ -102,6 +102,10 @@ public struct Job: Codable, Equatable, Identifiable, FetchableRecord, MutablePer
/// This is a job that runs once whenever an attachment is downloaded to attempt to decode and properly
/// download the attachment
case attachmentDownload
/// This is a job that runs once whenever the user leaves a group to send a group leaving message, remove group
/// record and group member record
case groupLeaving
}
public enum Behaviour: Int, Codable, DatabaseValueConvertible, CaseIterable {

@ -3,7 +3,7 @@
import Foundation
import GRDB
public struct JobDependencies: Codable, FetchableRecord, PersistableRecord, TableRecord, ColumnExpressible {
public struct JobDependencies: Codable, Equatable, Hashable, FetchableRecord, PersistableRecord, TableRecord, ColumnExpressible {
public static var databaseTableName: String { "jobDependencies" }
internal static let jobForeignKey = ForeignKey([Columns.jobId], to: [Job.Columns.id])
internal static let dependantForeignKey = ForeignKey([Columns.dependantId], to: [Job.Columns.id])

@ -45,6 +45,14 @@ public extension Array {
return updatedArray
}
func inserting(contentsOf other: [Element]?, at index: Int) -> [Element] {
guard let other: [Element] = other else { return self }
var updatedArray: [Element] = self
updatedArray.insert(contentsOf: other, at: 0)
return updatedArray
}
func grouped<Key: Hashable>(by keyForValue: (Element) throws -> Key) -> [Key: [Element]] {
return ((try? Dictionary(grouping: self, by: keyForValue)) ?? [:])
}

@ -34,6 +34,12 @@ public extension Dictionary {
return self[key]
}
func getting(_ key: Key?) -> Value? {
guard let key: Key = key else { return nil }
return self[key]
}
func setting(_ key: Key?, _ value: Value?) -> [Key: Value] {
guard let key: Key = key else { return self }

@ -66,7 +66,8 @@ public final class JobRunner {
jobVariants.remove(.attachmentUpload),
jobVariants.remove(.messageSend),
jobVariants.remove(.notifyPushServer),
jobVariants.remove(.sendReadReceipts)
jobVariants.remove(.sendReadReceipts),
jobVariants.remove(.groupLeaving)
].compactMap { $0 }
)
let messageReceiveQueue: JobQueue = JobQueue(
@ -626,9 +627,13 @@ private final class JobQueue {
}
fileprivate func hasPendingOrRunningJob(with detailsData: Data?) -> Bool {
guard let detailsData: Data = detailsData else { return false }
let pendingJobs: [Job] = queue.wrappedValue
return pendingJobs.contains { job in job.details == detailsData }
guard !pendingJobs.contains(where: { job in job.details == detailsData }) else { return true }
return detailsForCurrentlyRunningJobs.wrappedValue.values.contains(detailsData)
}
fileprivate func removePendingJob(_ jobId: Int64) {
@ -759,13 +764,15 @@ private final class JobQueue {
}
// Check if the next job has any dependencies
let dependencyInfo: (expectedCount: Int, jobs: [Job]) = Storage.shared.read { db in
let numExpectedDependencies: Int = try JobDependencies
let dependencyInfo: (expectedCount: Int, jobs: Set<Job>) = Storage.shared.read { db in
let expectedDependencies: Set<JobDependencies> = try JobDependencies
.filter(JobDependencies.Columns.jobId == nextJob.id)
.fetchCount(db)
let jobDependencies: [Job] = try nextJob.dependencies.fetchAll(db)
.fetchSet(db)
let jobDependencies: Set<Job> = try Job
.filter(ids: expectedDependencies.compactMap { $0.dependantId })
.fetchSet(db)
return (numExpectedDependencies, jobDependencies)
return (expectedDependencies.count, jobDependencies)
}
.defaulting(to: (0, []))
@ -777,39 +784,15 @@ private final class JobQueue {
guard dependencyInfo.jobs.isEmpty else {
SNLog("[JobRunner] \(queueContext) found job with \(dependencyInfo.jobs.count) dependencies, running those first")
let jobDependencyIds: [Int64] = dependencyInfo.jobs
.compactMap { $0.id }
let jobIdsNotInQueue: Set<Int64> = jobDependencyIds
.asSet()
.subtracting(queue.wrappedValue.compactMap { $0.id })
// If there are dependencies which aren't in the queue we should just append them
guard !jobIdsNotInQueue.isEmpty else {
queue.mutate { queue in
queue.append(
contentsOf: dependencyInfo.jobs
.filter { jobIdsNotInQueue.contains($0.id ?? -1) }
)
queue.append(nextJob)
}
handleJobDeferred(nextJob)
return
}
// Otherwise re-add the current job after it's dependencies (if this isn't a concurrent
// queue - don't want to immediately try to start the job again only for it to end up back
// in here)
if executionType != .concurrent {
queue.mutate { queue in
guard let lastDependencyIndex: Int = queue.lastIndex(where: { jobDependencyIds.contains($0.id ?? -1) }) else {
queue.append(nextJob)
return
}
queue.insert(nextJob, at: lastDependencyIndex + 1)
}
/// Remove all jobs this one is dependant on from the queue and re-insert them at the start of the queue
///
/// **Note:** We don't add the current job back the the queue because it should only be re-added if it's dependencies
/// are successfully completed
queue.mutate { queue in
queue = queue
.filter { !dependencyInfo.jobs.contains($0) }
.inserting(contentsOf: Array(dependencyInfo.jobs), at: 0)
}
handleJobDeferred(nextJob)
return
}
@ -909,6 +892,12 @@ private final class JobQueue {
/// This function is called when a job succeeds
private func handleJobSucceeded(_ job: Job, shouldStop: Bool) {
/// Retrieve the dependant jobs first (the `JobDependecies` table has cascading deletion when the original `Job` is
/// removed so we need to retrieve these records before that happens)
let dependantJobs: [Job] = Storage.shared
.read { db in try job.dependantJobs.fetchAll(db) }
.defaulting(to: [])
switch job.behaviour {
case .runOnce, .runOnceNextLaunch:
Storage.shared.write { db in
@ -971,26 +960,17 @@ private final class JobQueue {
default: break
}
// For concurrent queues retrieve any 'dependant' jobs and re-add them here (if they have other
// dependencies they will be removed again when they try to execute)
if executionType == .concurrent {
let dependantJobs: [Job] = Storage.shared
.read { db in try job.dependantJobs.fetchAll(db) }
.defaulting(to: [])
let dependantJobIds: [Int64] = dependantJobs
.compactMap { $0.id }
let jobIdsNotInQueue: Set<Int64> = dependantJobIds
.asSet()
.subtracting(queue.wrappedValue.compactMap { $0.id })
// If there are dependant jobs which aren't in the queue we should just append them
if !jobIdsNotInQueue.isEmpty {
queue.mutate { queue in
queue.append(
contentsOf: dependantJobs
.filter { jobIdsNotInQueue.contains($0.id ?? -1) }
)
}
/// Now that the job has been completed we want to insert any jobs that were dependant on it to the start of the queue (the
/// most likely case is that we want an entire job chain to be completed at the same time rather than being blocked by other
/// unrelated jobs)
///
/// **Note:** If any of these `dependantJobs` have other dependencies then when they attempt to start they will be
/// removed from the queue, replaced by their dependencies
if !dependantJobs.isEmpty {
queue.mutate { queue in
queue = queue
.filter { !dependantJobs.contains($0) }
.inserting(contentsOf: dependantJobs, at: 0)
}
}
@ -1050,19 +1030,30 @@ private final class JobQueue {
let nextRunTimestamp: TimeInterval = (Date().timeIntervalSince1970 + JobRunner.getRetryInterval(for: job))
Storage.shared.write { db in
/// Remove any dependant jobs from the queue (shouldn't be in there but filter the queue just in case so we don't try
/// to run a deleted job or get stuck in a loop of trying to run dependencies indefinitely)
let dependantJobIds: [Int64] = try job.dependantJobs
.select(.id)
.asRequest(of: Int64.self)
.fetchAll(db)
if !dependantJobIds.isEmpty {
queue.mutate { queue in
queue = queue.filter { !dependantJobIds.contains($0.id ?? -1) }
}
}
/// Delete/update the failed jobs and any dependencies
let updatedFailureCount: UInt = (job.failureCount + 1)
guard
!permanentFailure && (
maxFailureCount < 0 ||
job.failureCount + 1 < maxFailureCount
updatedFailureCount <= maxFailureCount
)
else {
SNLog("[JobRunner] \(queueContext) \(job.variant) failed permanently\(maxFailureCount >= 0 ? "; too many retries" : "")")
let dependantJobIds: [Int64] = try job.dependantJobs
.select(.id)
.asRequest(of: Int64.self)
.fetchAll(db)
// If the job permanently failed or we have performed all of our retry attempts
// then delete the job and all of it's dependant jobs (it'll probably never succeed)
_ = try job.dependantJobs
@ -1070,13 +1061,6 @@ private final class JobQueue {
_ = try job.delete(db)
// Remove the dependant jobs from the queue (so we don't try to run a deleted job)
if !dependantJobIds.isEmpty {
queue.mutate { queue in
queue = queue.filter { !dependantJobIds.contains($0.id ?? -1) }
}
}
performCleanUp(for: job, result: .failed)
return
}
@ -1085,7 +1069,7 @@ private final class JobQueue {
_ = try job
.with(
failureCount: (job.failureCount + 1),
failureCount: updatedFailureCount,
nextRunTimestamp: nextRunTimestamp
)
.saved(db)
@ -1096,22 +1080,9 @@ private final class JobQueue {
try job.dependantJobs
.updateAll(
db,
Job.Columns.failureCount.set(to: (job.failureCount + 1)),
Job.Columns.failureCount.set(to: updatedFailureCount),
Job.Columns.nextRunTimestamp.set(to: (nextRunTimestamp + (1 / 1000)))
)
let dependantJobIds: [Int64] = try job.dependantJobs
.select(.id)
.asRequest(of: Int64.self)
.fetchAll(db)
// Remove the dependant jobs from the queue (so we don't get stuck in a loop of trying
// to run dependecies indefinitely)
if !dependantJobIds.isEmpty {
queue.mutate { queue in
queue = queue.filter { !dependantJobIds.contains($0.id ?? -1) }
}
}
}
performCleanUp(for: job, result: .failed)

@ -41,8 +41,25 @@ public enum HTTP {
guard SecTrustSetAnchorCertificates(trust, certificates as CFArray) == errSecSuccess else {
return completionHandler(.cancelAuthenticationChallenge, nil)
}
// We want to make sure that the pinned certification was valid during it's validity
// period (which has now expired) so set the date to validate against to be within the
// valid period
let dateFormatter: DateFormatter = DateFormatter()
dateFormatter.dateFormat = "dd/MM/yyyy HH:mm:ss"
if let validDate: Date = dateFormatter.date(from: "01/01/2022 12:00:00") {
if SecTrustSetVerifyDate(trust, validDate as CFDate) != errSecSuccess {
SNLog("Unable to set date for seed node certificate validation.")
}
}
else {
SNLog("Unable to set date for seed node certificate validation.")
}
// Check that the presented certificate is one of the seed node certificates
var result: SecTrustResultType = .invalid
guard SecTrustEvaluate(trust, &result) == errSecSuccess else {
return completionHandler(.cancelAuthenticationChallenge, nil)
}

Loading…
Cancel
Save