Merge remote-tracking branch 'upstream/dev' into feature/updated-user-config-handling
# Conflicts: # Podfile.lock # Session.xcodeproj/project.pbxproj # Session/Closed Groups/EditClosedGroupVC.swift # Session/Conversations/Settings/ThreadSettingsViewModel.swift # Session/Home/HomeVC.swift # Session/Home/HomeViewModel.swift # Session/Meta/Translations/de.lproj/Localizable.strings # Session/Meta/Translations/en.lproj/Localizable.strings # Session/Meta/Translations/es.lproj/Localizable.strings # Session/Meta/Translations/fa.lproj/Localizable.strings # Session/Meta/Translations/fi.lproj/Localizable.strings # Session/Meta/Translations/fr.lproj/Localizable.strings # Session/Meta/Translations/hi.lproj/Localizable.strings # Session/Meta/Translations/hr.lproj/Localizable.strings # Session/Meta/Translations/id-ID.lproj/Localizable.strings # Session/Meta/Translations/it.lproj/Localizable.strings # Session/Meta/Translations/ja.lproj/Localizable.strings # Session/Meta/Translations/nl.lproj/Localizable.strings # Session/Meta/Translations/pl.lproj/Localizable.strings # Session/Meta/Translations/pt_BR.lproj/Localizable.strings # Session/Meta/Translations/ru.lproj/Localizable.strings # Session/Meta/Translations/si.lproj/Localizable.strings # Session/Meta/Translations/sk.lproj/Localizable.strings # Session/Meta/Translations/sv.lproj/Localizable.strings # Session/Meta/Translations/th.lproj/Localizable.strings # Session/Meta/Translations/vi-VN.lproj/Localizable.strings # Session/Meta/Translations/zh-Hant.lproj/Localizable.strings # Session/Meta/Translations/zh_CN.lproj/Localizable.strings # Session/Shared/FullConversationCell.swift # SessionMessagingKit/Configuration.swift # SessionMessagingKit/Database/Models/SessionThread.swift # SessionMessagingKit/Sending & Receiving/Message Handling/MessageSender+ClosedGroups.swift # SessionMessagingKit/Shared Models/SessionThreadViewModel.swift # SessionUIKit/Utilities/UIContextualAction+Theming.swift # SessionUtilitiesKit/Database/Models/Job.swift # SessionUtilitiesKit/General/Dictionary+Utilities.swift # SessionUtilitiesKit/JobRunner/JobRunner.swift
This commit is contained in:
commit
f4d6babca2
14
Podfile
14
Podfile
|
@ -10,7 +10,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'
|
||||
|
@ -102,6 +102,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)
|
||||
|
@ -132,3 +135,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
|
||||
|
|
28
Podfile.lock
28
Podfile.lock
|
@ -1,7 +1,7 @@
|
|||
PODS:
|
||||
- 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
|
||||
|
@ -12,7 +12,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)
|
||||
|
@ -37,10 +37,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)
|
||||
|
@ -128,7 +128,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`)
|
||||
|
@ -173,7 +173,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
|
||||
|
@ -189,11 +189,11 @@ CHECKOUT OPTIONS:
|
|||
:git: https://github.com/signalapp/YYImage
|
||||
|
||||
SPEC CHECKSUMS:
|
||||
CocoaLumberjack: 543c79c114dadc3b1aba95641d8738b06b05b646
|
||||
CocoaLumberjack: 78abfb691154e2a9df8ded4350d504ee19d90732
|
||||
CryptoSwift: a532e74ed010f8c95f611d00b8bbae42e9fe7c17
|
||||
Curve25519Kit: e63f9859ede02438ae3defc5e1a87e09d1ec7ee6
|
||||
DifferenceKit: ab185c4d7f9cef8af3fcf593e5b387fb81e999ca
|
||||
GRDB.swift: 611778a5e113385373baeb3e2ce474887d1aadb7
|
||||
GRDB.swift: 1cc67278f1a9878d6eb1b849485518112b79cab7
|
||||
libwebp: 98a37e597e40bfdb4c911fc98f2c53d0b12d05fc
|
||||
Nimble: 5316ef81a170ce87baf72dd961f22f89a602ff84
|
||||
NVActivityIndicatorView: 1f6c5687f1171810aa27a3296814dc2d7dec3667
|
||||
|
@ -205,13 +205,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: 6ee08fc446436a2534ec34c041c3b9bc39801870
|
||||
PODFILE CHECKSUM: f461937f78a0482496fea6fc4b2bb5d1351fe044
|
||||
|
||||
COCOAPODS: 1.11.3
|
||||
|
|
|
@ -113,7 +113,7 @@
|
|||
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 */; };
|
||||
7B521E0829BFEAFF00C3C36A /* _012_AddFTSIfNeeded.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B521E0729BFEAFF00C3C36A /* _012_AddFTSIfNeeded.swift */; };
|
||||
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 */; };
|
||||
|
@ -431,7 +431,6 @@
|
|||
C3BBE0A82554D4DE0050F1E3 /* JSON.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3C2A5D92553860B00C340D1 /* JSON.swift */; };
|
||||
C3BBE0A92554D4DE0050F1E3 /* HTTP.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3C2A5BC255385EE00C340D1 /* HTTP.swift */; };
|
||||
C3BBE0AA2554D4DE0050F1E3 /* Dictionary+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3C2A5D52553860A00C340D1 /* Dictionary+Utilities.swift */; };
|
||||
C3BBE0B52554F0E10050F1E3 /* (null) in Sources */ = {isa = PBXBuildFile; };
|
||||
C3BBE0C72554F1570050F1E3 /* FixedWidthInteger+BigEndian.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3BBE0C62554F1570050F1E3 /* FixedWidthInteger+BigEndian.swift */; };
|
||||
C3C2A5A3255385C100C340D1 /* SessionSnodeKit.h in Headers */ = {isa = PBXBuildFile; fileRef = C3C2A5A1255385C100C340D1 /* SessionSnodeKit.h */; settings = {ATTRIBUTES = (Public, ); }; };
|
||||
C3C2A5A7255385C100C340D1 /* SessionSnodeKit.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = C3C2A59F255385C100C340D1 /* SessionSnodeKit.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
|
||||
|
@ -463,8 +462,6 @@
|
|||
C3D9E38A256760390040E4F3 /* OWSFileSystem.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDBAB255A581500E217F9 /* OWSFileSystem.h */; settings = {ATTRIBUTES = (Public, ); }; };
|
||||
C3D9E39B256763C20040E4F3 /* AppContext.m in Sources */ = {isa = PBXBuildFile; fileRef = C33FDB85255A581100E217F9 /* AppContext.m */; };
|
||||
C3D9E3A4256763DE0040E4F3 /* AppContext.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDB8A255A581200E217F9 /* AppContext.h */; settings = {ATTRIBUTES = (Public, ); }; };
|
||||
C3D9E3BE25676AD70040E4F3 /* (null) in Sources */ = {isa = PBXBuildFile; };
|
||||
C3D9E3BF25676AD70040E4F3 /* (null) in Sources */ = {isa = PBXBuildFile; };
|
||||
C3D9E4C02567767F0040E4F3 /* DataSource.m in Sources */ = {isa = PBXBuildFile; fileRef = C33FDBB6255A581600E217F9 /* DataSource.m */; };
|
||||
C3D9E4D12567777D0040E4F3 /* OWSMediaUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33FDB22255A580900E217F9 /* OWSMediaUtils.swift */; };
|
||||
C3D9E4DA256778410040E4F3 /* UIImage+OWS.m in Sources */ = {isa = PBXBuildFile; fileRef = C33FDB81255A581100E217F9 /* UIImage+OWS.m */; };
|
||||
|
@ -582,7 +579,6 @@
|
|||
FD245C6B2850667400B966DD /* VisibleMessage+Profile.swift in Sources */ = {isa = PBXBuildFile; fileRef = C300A5B12554AF9800555489 /* VisibleMessage+Profile.swift */; };
|
||||
FD245C6C2850669200B966DD /* MessageReceiveJob.swift in Sources */ = {isa = PBXBuildFile; fileRef = C352A31225574F5200338F3E /* MessageReceiveJob.swift */; };
|
||||
FD245C6D285066A400B966DD /* NotifyPushServerJob.swift in Sources */ = {isa = PBXBuildFile; fileRef = C352A32E2557549C00338F3E /* NotifyPushServerJob.swift */; };
|
||||
FD26FA512919F9CE005801D8 /* (null) in Sources */ = {isa = PBXBuildFile; };
|
||||
FD26FA5E291CAFF9005801D8 /* (null) in Sources */ = {isa = PBXBuildFile; };
|
||||
FD26FA6D291DADAE005801D8 /* (null) in Sources */ = {isa = PBXBuildFile; };
|
||||
FD26FA7B291DF8F3005801D8 /* (null) in Sources */ = {isa = PBXBuildFile; };
|
||||
|
@ -595,6 +591,8 @@
|
|||
FD2B4AFD294688D000AB4848 /* SessionUtil+Contacts.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD2B4AFC294688D000AB4848 /* SessionUtil+Contacts.swift */; };
|
||||
FD2B4AFF2946C93200AB4848 /* ConfigurationSyncJob.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD2B4AFE2946C93200AB4848 /* ConfigurationSyncJob.swift */; };
|
||||
FD2B4B042949887A00AB4848 /* QueryInterfaceRequest+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD2B4B032949887A00AB4848 /* QueryInterfaceRequest+Utilities.swift */; };
|
||||
FD368A6829DE8F9C000DBF1E /* _012_AddFTSIfNeeded.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD368A6729DE8F9B000DBF1E /* _012_AddFTSIfNeeded.swift */; };
|
||||
FD368A6A29DE9E30000DBF1E /* UIContextualAction+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD368A6929DE9E30000DBF1E /* UIContextualAction+Utilities.swift */; };
|
||||
FD37E9C328A1C6F3003AE748 /* ThemeManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD37E9C228A1C6F3003AE748 /* ThemeManager.swift */; };
|
||||
FD37E9C628A1D4EC003AE748 /* Theme+ClassicDark.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD37E9C528A1D4EC003AE748 /* Theme+ClassicDark.swift */; };
|
||||
FD37E9C828A1D73F003AE748 /* Theme+Colors.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD37E9C728A1D73F003AE748 /* Theme+Colors.swift */; };
|
||||
|
@ -796,7 +794,6 @@
|
|||
FDC4386927B4E6B800C60D73 /* String+Utlities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4386827B4E6B700C60D73 /* String+Utlities.swift */; };
|
||||
FDC4386C27B4E90300C60D73 /* SessionUtilitiesKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C3C2A679255388CC00C340D1 /* SessionUtilitiesKit.framework */; };
|
||||
FDC4387227B5BB3B00C60D73 /* FileUploadResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4387127B5BB3B00C60D73 /* FileUploadResponse.swift */; };
|
||||
FDC4387427B5BB9B00C60D73 /* (null) in Sources */ = {isa = PBXBuildFile; };
|
||||
FDC4387827B5C35400C60D73 /* SendMessageRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4387727B5C35400C60D73 /* SendMessageRequest.swift */; };
|
||||
FDC4389227B9FFC700C60D73 /* SessionMessagingKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C3C2A6F025539DE700C340D1 /* SessionMessagingKit.framework */; platformFilter = ios; };
|
||||
FDC4389A27BA002500C60D73 /* OpenGroupAPISpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4389927BA002500C60D73 /* OpenGroupAPISpec.swift */; };
|
||||
|
@ -1228,11 +1225,12 @@
|
|||
7B1D74A927BCC16E0030B423 /* NSENotificationPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSENotificationPresenter.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>"; };
|
||||
7B521E0729BFEAFF00C3C36A /* _012_AddFTSIfNeeded.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _012_AddFTSIfNeeded.swift; 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>"; };
|
||||
|
@ -1242,6 +1240,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>"; };
|
||||
|
@ -1731,6 +1730,8 @@
|
|||
FD2B4AFC294688D000AB4848 /* SessionUtil+Contacts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SessionUtil+Contacts.swift"; sourceTree = "<group>"; };
|
||||
FD2B4AFE2946C93200AB4848 /* ConfigurationSyncJob.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfigurationSyncJob.swift; sourceTree = "<group>"; };
|
||||
FD2B4B032949887A00AB4848 /* QueryInterfaceRequest+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "QueryInterfaceRequest+Utilities.swift"; sourceTree = "<group>"; };
|
||||
FD368A6729DE8F9B000DBF1E /* _012_AddFTSIfNeeded.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = _012_AddFTSIfNeeded.swift; sourceTree = "<group>"; };
|
||||
FD368A6929DE9E30000DBF1E /* UIContextualAction+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIContextualAction+Utilities.swift"; sourceTree = "<group>"; };
|
||||
FD37E9C228A1C6F3003AE748 /* ThemeManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThemeManager.swift; sourceTree = "<group>"; };
|
||||
FD37E9C528A1D4EC003AE748 /* Theme+ClassicDark.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Theme+ClassicDark.swift"; sourceTree = "<group>"; };
|
||||
FD37E9C728A1D73F003AE748 /* Theme+Colors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Theme+Colors.swift"; sourceTree = "<group>"; };
|
||||
|
@ -2326,6 +2327,7 @@
|
|||
FD848B8E283EF2A8000E298B /* UIScrollView+Utilities.swift */,
|
||||
C3548F0724456AB6009433A8 /* UIView+Wrapping.swift */,
|
||||
7BAADFCD27B215FE007BCF92 /* UIView+Draggable.swift */,
|
||||
FD368A6929DE9E30000DBF1E /* UIContextualAction+Utilities.swift */,
|
||||
7BFD1A892745C4F000FB91B9 /* Permissions.swift */,
|
||||
);
|
||||
path = Utilities;
|
||||
|
@ -3482,6 +3484,7 @@
|
|||
D221A08C169C9E5E00537ABF /* Frameworks */,
|
||||
D221A08A169C9E5E00537ABF /* Products */,
|
||||
2BADBA206E0B8D297E313FBA /* Pods */,
|
||||
FD368A6629DE86A9000DBF1E /* Recovered References */,
|
||||
);
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
|
@ -3636,7 +3639,7 @@
|
|||
7BAA7B6528D2DE4700AE1489 /* _009_OpenGroupPermission.swift */,
|
||||
FD7115F128C6CB3900B47552 /* _010_AddThreadIdToFTS.swift */,
|
||||
FD432431299C6933008A0213 /* _011_AddPendingReadReceipts.swift */,
|
||||
7B521E0729BFEAFF00C3C36A /* _012_AddFTSIfNeeded.swift */,
|
||||
FD368A6729DE8F9B000DBF1E /* _012_AddFTSIfNeeded.swift */,
|
||||
FD8ECF7C2934293A00C0D1BB /* _013_SessionUtilChanges.swift */,
|
||||
FD778B6329B189FF001BAC6B /* _014_GenerateInitialUserConfigDumps.swift */,
|
||||
);
|
||||
|
@ -3773,6 +3776,15 @@
|
|||
path = Database;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
FD368A6629DE86A9000DBF1E /* Recovered References */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
7B2E985729AC227C001792D7 /* UIContextualAction+Theming.swift */,
|
||||
7B89FF4529C016E300C4C708 /* _012_AddFTSIfNeeded.swift */,
|
||||
);
|
||||
name = "Recovered References";
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
FD37E9C428A1C701003AE748 /* Themes */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
|
@ -4289,6 +4301,7 @@
|
|||
FDF0B74E28079E5E004C14C5 /* SendReadReceiptsJob.swift */,
|
||||
C352A348255781F400338F3E /* AttachmentDownloadJob.swift */,
|
||||
C352A35A2557824E00338F3E /* AttachmentUploadJob.swift */,
|
||||
7B521E0929BFF84400C3C36A /* GroupLeavingJob.swift */,
|
||||
FD2B4AFE2946C93200AB4848 /* ConfigurationSyncJob.swift */,
|
||||
);
|
||||
path = Types;
|
||||
|
@ -5672,6 +5685,7 @@
|
|||
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 */,
|
||||
|
@ -5703,19 +5717,15 @@
|
|||
FD09797527FAB64300936362 /* ProfileManager.swift in Sources */,
|
||||
FD245C57285065F100B966DD /* Poller.swift in Sources */,
|
||||
FDA8EAFE280E8B78002B68E5 /* FailedMessageSendsJob.swift in Sources */,
|
||||
7B521E0829BFEAFF00C3C36A /* _012_AddFTSIfNeeded.swift in Sources */,
|
||||
FD245C6A2850666F00B966DD /* FileServerAPI.swift in Sources */,
|
||||
FDC4386927B4E6B800C60D73 /* String+Utlities.swift in Sources */,
|
||||
FD716E6628502EE200C96BF4 /* CurrentCallProtocol.swift in Sources */,
|
||||
FD26FA512919F9CE005801D8 /* (null) in Sources */,
|
||||
FD8ECF9029381FC200C0D1BB /* SessionUtil+UserProfile.swift in Sources */,
|
||||
FD09B7E5288670BB00ED0B66 /* _008_EmojiReacts.swift in Sources */,
|
||||
FDC4385F27B4C4A200C60D73 /* PinnedMessage.swift in Sources */,
|
||||
7BFD1A8C2747150E00FB91B9 /* TurnServerInfo.swift in Sources */,
|
||||
C3D9E3BF25676AD70040E4F3 /* (null) in Sources */,
|
||||
B8BF43BA26CC95FB007828D1 /* WebRTC+Utilities.swift in Sources */,
|
||||
7B8D5FC428332600008324D9 /* VisibleMessage+Reaction.swift in Sources */,
|
||||
C3BBE0B52554F0E10050F1E3 /* (null) in Sources */,
|
||||
FDC4386527B4DE7600C60D73 /* RoomPollInfo.swift in Sources */,
|
||||
FD245C6B2850667400B966DD /* VisibleMessage+Profile.swift in Sources */,
|
||||
FD37EA0F28AB3330003AE748 /* _006_FixHiddenModAdminSupport.swift in Sources */,
|
||||
|
@ -5737,7 +5747,6 @@
|
|||
FD09C5EC282B8F18000CE219 /* AttachmentError.swift in Sources */,
|
||||
FD17D79927F40AB800122BE0 /* _003_YDBToGRDBMigration.swift in Sources */,
|
||||
FDF0B7512807BA56004C14C5 /* NotificationsProtocol.swift in Sources */,
|
||||
FDC4387427B5BB9B00C60D73 /* (null) in Sources */,
|
||||
B8DE1FB426C22F2F0079C9CE /* WebRTCSession.swift in Sources */,
|
||||
FDC6D6F32860607300B04575 /* Environment.swift in Sources */,
|
||||
C3A3A171256E1D25004D228D /* SSKReachabilityManager.swift in Sources */,
|
||||
|
@ -5785,7 +5794,6 @@
|
|||
FDC4381527B329CE00C60D73 /* NonceGenerator.swift in Sources */,
|
||||
7B93D07027CF194000811CB6 /* ConfigurationMessage+Convenience.swift in Sources */,
|
||||
FD5C72FD284F0EC90029977D /* MessageReceiver+ExpirationTimers.swift in Sources */,
|
||||
C3D9E3BE25676AD70040E4F3 /* (null) in Sources */,
|
||||
C32C5A88256DBCF9003C73A2 /* MessageReceiver+ClosedGroups.swift in Sources */,
|
||||
B8D0A25925E367AC00C1835E /* Notification+MessageReceiver.swift in Sources */,
|
||||
FD245C53285065DB00B966DD /* ProximityMonitoringManager.swift in Sources */,
|
||||
|
@ -5815,6 +5823,7 @@
|
|||
B8856D11256F112A001CE70E /* OWSAudioSession.swift in Sources */,
|
||||
C3DB66C3260ACCE6001EFC55 /* OpenGroupPoller.swift in Sources */,
|
||||
FD716E722850647600C96BF4 /* Data+Utilities.swift in Sources */,
|
||||
FD368A6829DE8F9C000DBF1E /* _012_AddFTSIfNeeded.swift in Sources */,
|
||||
FD245C5C2850660A00B966DD /* ConfigurationMessage.swift in Sources */,
|
||||
FD5C7301284F0F7A0029977D /* MessageReceiver+UnsendRequests.swift in Sources */,
|
||||
C3C2A75F2553A3C500C340D1 /* VisibleMessage+LinkPreview.swift in Sources */,
|
||||
|
@ -6034,6 +6043,7 @@
|
|||
B894D0752339EDCF00B4D94D /* NukeDataModal.swift in Sources */,
|
||||
7B93D07727CF1A8A00811CB6 /* MockDataGenerator.swift in Sources */,
|
||||
7B1B52D828580C6D006069F2 /* EmojiPickerSheet.swift in Sources */,
|
||||
FD368A6A29DE9E30000DBF1E /* UIContextualAction+Utilities.swift in Sources */,
|
||||
7B4C75CD26BB92060000AC89 /* DeletedMessageView.swift in Sources */,
|
||||
FDD250722837234B00198BDA /* MediaGalleryNavigationController.swift in Sources */,
|
||||
FDD2506E283711D600198BDA /* DifferenceKit+Utilities.swift in Sources */,
|
||||
|
@ -6331,7 +6341,7 @@
|
|||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
CURRENT_PROJECT_VERSION = 398;
|
||||
CURRENT_PROJECT_VERSION = 399;
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
DEVELOPMENT_TEAM = SUQ8J2PCT7;
|
||||
FRAMEWORK_SEARCH_PATHS = "$(inherited)";
|
||||
|
@ -6355,7 +6365,7 @@
|
|||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 2.2.9;
|
||||
MARKETING_VERSION = 2.2.10;
|
||||
MTL_ENABLE_DEBUG_INFO = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger.ShareExtension";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
|
@ -6403,7 +6413,7 @@
|
|||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
CURRENT_PROJECT_VERSION = 398;
|
||||
CURRENT_PROJECT_VERSION = 399;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
DEVELOPMENT_TEAM = SUQ8J2PCT7;
|
||||
ENABLE_NS_ASSERTIONS = NO;
|
||||
|
@ -6432,7 +6442,7 @@
|
|||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 2.2.9;
|
||||
MARKETING_VERSION = 2.2.10;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger.ShareExtension";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
|
@ -6468,7 +6478,7 @@
|
|||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
CURRENT_PROJECT_VERSION = 398;
|
||||
CURRENT_PROJECT_VERSION = 399;
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
DEVELOPMENT_TEAM = SUQ8J2PCT7;
|
||||
FRAMEWORK_SEARCH_PATHS = "$(inherited)";
|
||||
|
@ -6491,7 +6501,7 @@
|
|||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 2.2.9;
|
||||
MARKETING_VERSION = 2.2.10;
|
||||
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
||||
MTL_FAST_MATH = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger.NotificationServiceExtension";
|
||||
|
@ -6542,7 +6552,7 @@
|
|||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
CURRENT_PROJECT_VERSION = 398;
|
||||
CURRENT_PROJECT_VERSION = 399;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
DEVELOPMENT_TEAM = SUQ8J2PCT7;
|
||||
ENABLE_NS_ASSERTIONS = NO;
|
||||
|
@ -6570,7 +6580,7 @@
|
|||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 2.2.9;
|
||||
MARKETING_VERSION = 2.2.10;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
MTL_FAST_MATH = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger.NotificationServiceExtension";
|
||||
|
@ -7454,7 +7464,7 @@
|
|||
CODE_SIGN_ENTITLEMENTS = Session/Meta/Signal.entitlements;
|
||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||
CURRENT_PROJECT_VERSION = 398;
|
||||
CURRENT_PROJECT_VERSION = 399;
|
||||
DEVELOPMENT_TEAM = SUQ8J2PCT7;
|
||||
FRAMEWORK_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
|
@ -7492,7 +7502,7 @@
|
|||
"$(SRCROOT)",
|
||||
);
|
||||
LLVM_LTO = NO;
|
||||
MARKETING_VERSION = 2.2.9;
|
||||
MARKETING_VERSION = 2.2.10;
|
||||
OTHER_LDFLAGS = "$(inherited)";
|
||||
OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\"";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger";
|
||||
|
@ -7525,7 +7535,7 @@
|
|||
CODE_SIGN_ENTITLEMENTS = Session/Meta/Signal.entitlements;
|
||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||
CURRENT_PROJECT_VERSION = 398;
|
||||
CURRENT_PROJECT_VERSION = 399;
|
||||
DEVELOPMENT_TEAM = SUQ8J2PCT7;
|
||||
FRAMEWORK_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
|
@ -7563,7 +7573,7 @@
|
|||
"$(SRCROOT)",
|
||||
);
|
||||
LLVM_LTO = NO;
|
||||
MARKETING_VERSION = 2.2.9;
|
||||
MARKETING_VERSION = 2.2.10;
|
||||
OTHER_LDFLAGS = "$(inherited)";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger";
|
||||
PRODUCT_NAME = Session;
|
||||
|
|
|
@ -18,6 +18,7 @@ final class EditClosedGroupVC: BaseVC, UITableViewDataSource, UITableViewDelegat
|
|||
}
|
||||
|
||||
private let threadId: String
|
||||
private let threadVariant: SessionThread.Variant
|
||||
private var originalName: String = ""
|
||||
private var originalMembersAndZombieIds: Set<String> = []
|
||||
private var name: String = ""
|
||||
|
@ -82,8 +83,9 @@ final class EditClosedGroupVC: BaseVC, UITableViewDataSource, UITableViewDelegat
|
|||
|
||||
// MARK: - Lifecycle
|
||||
|
||||
init(threadId: String) {
|
||||
init(threadId: String, threadVariant: SessionThread.Variant) {
|
||||
self.threadId = threadId
|
||||
self.threadVariant = threadVariant
|
||||
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
}
|
||||
|
@ -438,6 +440,7 @@ final class EditClosedGroupVC: BaseVC, UITableViewDataSource, UITableViewDelegat
|
|||
}
|
||||
|
||||
let threadId: String = self.threadId
|
||||
let threadVariant: SessionThread.Variant = self.threadVariant
|
||||
let updatedName: String = self.name
|
||||
let userPublicKey: String = self.userPublicKey
|
||||
let updatedMemberIds: Set<String> = self.membersAndZombies
|
||||
|
@ -464,7 +467,15 @@ final class EditClosedGroupVC: BaseVC, UITableViewDataSource, UITableViewDelegat
|
|||
Storage.shared
|
||||
.writePublisherFlatMap(receiveOn: DispatchQueue.main) { db -> AnyPublisher<Void, Error> in
|
||||
if !updatedMemberIds.contains(userPublicKey) {
|
||||
return MessageSender.leave(db, groupPublicKey: threadId)
|
||||
try MessageSender.leave(
|
||||
db,
|
||||
groupPublicKey: threadId,
|
||||
deleteThread: true
|
||||
)
|
||||
|
||||
return Just(())
|
||||
.setFailureType(to: Error.self)
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
return 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) ]
|
||||
|
|
|
@ -1742,7 +1742,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
|
||||
|
@ -2052,7 +2053,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,
|
||||
threadId: threadId,
|
||||
|
@ -2320,7 +2322,8 @@ extension ConversationVC:
|
|||
try MessageSender.send(
|
||||
db,
|
||||
message: DataExtractionNotification(
|
||||
kind: .screenshot
|
||||
kind: .screenshot,
|
||||
sentTimestamp: UInt64(SnodeAPI.currentOffsetTimestampMs())
|
||||
),
|
||||
interactionId: nil,
|
||||
threadId: threadId,
|
||||
|
@ -2435,31 +2438,49 @@ extension ConversationVC {
|
|||
}
|
||||
|
||||
@objc func deleteMessageRequest() {
|
||||
MessageRequestsViewModel.deleteMessageRequest(
|
||||
threadId: self.viewModel.threadData.threadId,
|
||||
threadVariant: self.viewModel.threadData.threadVariant,
|
||||
let actions: [UIContextualAction]? = UIContextualAction.generateSwipeActions(
|
||||
[.delete],
|
||||
for: .trailing,
|
||||
indexPath: IndexPath(row: 0, section: 0),
|
||||
tableView: self.tableView,
|
||||
threadViewModel: self.viewModel.threadData,
|
||||
viewController: self
|
||||
) { [weak self] in
|
||||
)
|
||||
|
||||
guard let action: UIContextualAction = actions?.first else { return }
|
||||
|
||||
action.handler(action, self.view, { [weak self] didConfirm in
|
||||
guard didConfirm else { return }
|
||||
|
||||
self?.stopObservingChanges()
|
||||
|
||||
DispatchQueue.main.async {
|
||||
self?.navigationController?.popViewController(animated: true)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@objc func blockMessageRequest() {
|
||||
MessageRequestsViewModel.blockMessageRequest(
|
||||
threadId: self.viewModel.threadData.threadId,
|
||||
threadVariant: self.viewModel.threadData.threadVariant,
|
||||
let actions: [UIContextualAction]? = UIContextualAction.generateSwipeActions(
|
||||
[.block],
|
||||
for: .trailing,
|
||||
indexPath: IndexPath(row: 0, section: 0),
|
||||
tableView: self.tableView,
|
||||
threadViewModel: self.viewModel.threadData,
|
||||
viewController: self
|
||||
) { [weak self] in
|
||||
)
|
||||
|
||||
guard let action: UIContextualAction = actions?.first else { return }
|
||||
|
||||
action.handler(action, self.view, { [weak self] didConfirm in
|
||||
guard didConfirm else { return }
|
||||
|
||||
self?.stopObservingChanges()
|
||||
|
||||
DispatchQueue.main.async {
|
||||
self?.navigationController?.popViewController(animated: true)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -168,6 +168,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
|
||||
|
|
|
@ -482,7 +482,9 @@ class ThreadSettingsViewModel: SessionTableViewModel<ThreadSettingsViewModel.Nav
|
|||
label: "Edit group"
|
||||
),
|
||||
onTap: { [weak self] in
|
||||
self?.transitionToScreen(EditClosedGroupVC(threadId: threadId))
|
||||
self?.transitionToScreen(
|
||||
EditClosedGroupVC(threadId: threadId, threadVariant: threadVariant)
|
||||
)
|
||||
}
|
||||
)
|
||||
),
|
||||
|
@ -500,21 +502,39 @@ class ThreadSettingsViewModel: SessionTableViewModel<ThreadSettingsViewModel.Nav
|
|||
label: "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
|
||||
.writePublisherFlatMap(receiveOn: DispatchQueue.global(qos: .userInitiated)) { db in
|
||||
MessageSender.leave(db, groupPublicKey: threadId)
|
||||
}
|
||||
.sinkUntilComplete()
|
||||
dependencies.storage.write { db in
|
||||
try SessionThread.deleteOrLeave(
|
||||
db,
|
||||
threadId: threadId,
|
||||
threadVariant: threadVariant,
|
||||
groupLeaveType: .standard,
|
||||
calledFromConfigHandling: false
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
),
|
||||
|
|
|
@ -217,7 +217,12 @@ class GlobalSearchViewController: BaseVC, SessionUtilRespondingViewController, U
|
|||
self?.termForCurrentSearchResultSet = searchText
|
||||
self?.searchResultSet = [
|
||||
(hasResults ? nil : [
|
||||
ArraySection(model: .noResults, elements: [SessionThreadViewModel()])
|
||||
ArraySection(
|
||||
model: .noResults,
|
||||
elements: [
|
||||
SessionThreadViewModel(threadId: SessionThreadViewModel.invalidId)
|
||||
]
|
||||
)
|
||||
]),
|
||||
(hasResults ? sections : nil)
|
||||
]
|
||||
|
|
|
@ -636,6 +636,7 @@ final class HomeVC: BaseVC, SessionUtilRespondingViewController, UITableViewData
|
|||
|
||||
func tableView(_ tableView: UITableView, leadingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
|
||||
let section: HomeViewModel.SectionModel = self.viewModel.threadData[indexPath.section]
|
||||
let threadViewModel: SessionThreadViewModel = section.elements[indexPath.row]
|
||||
|
||||
switch section.model {
|
||||
case .threads:
|
||||
|
@ -644,268 +645,86 @@ final class HomeVC: BaseVC, SessionUtilRespondingViewController, UITableViewData
|
|||
return nil
|
||||
}
|
||||
|
||||
return generateSwipeActions(
|
||||
[.toggleReadStatus],
|
||||
for: .leading,
|
||||
indexPath: indexPath,
|
||||
tableView: tableView
|
||||
return UIContextualAction.configuration(
|
||||
for: UIContextualAction.generateSwipeActions(
|
||||
[.toggleReadStatus],
|
||||
for: .leading,
|
||||
indexPath: indexPath,
|
||||
tableView: tableView,
|
||||
threadViewModel: threadViewModel,
|
||||
viewController: self
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
default: return nil
|
||||
}
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
|
||||
let section: HomeViewModel.SectionModel = self.viewModel.threadData[indexPath.section]
|
||||
let threadViewModel: SessionThreadViewModel = section.elements[indexPath.row]
|
||||
|
||||
switch section.model {
|
||||
case .messageRequests:
|
||||
return generateSwipeActions([.hide], for: .trailing, indexPath: indexPath, tableView: tableView)
|
||||
|
||||
case .threads:
|
||||
let threadViewModel: SessionThreadViewModel = section.elements[indexPath.row]
|
||||
let sessionIdPrefix: SessionId.Prefix? = SessionId(from: threadViewModel.threadId)?.prefix
|
||||
|
||||
// Cannot properly sync outgoing blinded message requests and can only block contact
|
||||
// threads so only provide these options if valid
|
||||
let shouldHaveBlockAction: Bool = (
|
||||
threadViewModel.threadVariant == .contact &&
|
||||
!threadViewModel.threadIsNoteToSelf &&
|
||||
sessionIdPrefix != .blinded
|
||||
return UIContextualAction.configuration(
|
||||
for: UIContextualAction.generateSwipeActions(
|
||||
[.hide],
|
||||
for: .trailing,
|
||||
indexPath: indexPath,
|
||||
tableView: tableView,
|
||||
threadViewModel: threadViewModel,
|
||||
viewController: self
|
||||
)
|
||||
)
|
||||
|
||||
return generateSwipeActions(
|
||||
[
|
||||
(sessionIdPrefix == .blinded ? nil : .pin),
|
||||
(!shouldHaveBlockAction ? nil : .block),
|
||||
.delete
|
||||
].compactMap { $0 },
|
||||
for: .trailing,
|
||||
indexPath: indexPath,
|
||||
tableView: tableView
|
||||
case .threads:
|
||||
let sessionIdPrefix: SessionId.Prefix? = SessionId(from: threadViewModel.threadId)?.prefix
|
||||
|
||||
// Cannot properly sync outgoing blinded message requests so only provide valid options
|
||||
let shouldHavePinAction: Bool = (
|
||||
sessionIdPrefix != .blinded
|
||||
)
|
||||
let shouldHaveMuteAction: Bool = {
|
||||
switch threadViewModel.threadVariant {
|
||||
case .contact: return (
|
||||
!threadViewModel.threadIsNoteToSelf &&
|
||||
sessionIdPrefix != .blinded
|
||||
)
|
||||
|
||||
case .legacyGroup, .group: return (
|
||||
threadViewModel.currentUserIsClosedGroupMember == true
|
||||
)
|
||||
|
||||
case .community: return true
|
||||
}
|
||||
}()
|
||||
let destructiveAction: UIContextualAction.SwipeAction = {
|
||||
switch (threadViewModel.threadVariant, threadViewModel.threadIsNoteToSelf, threadViewModel.currentUserIsClosedGroupMember) {
|
||||
case (.contact, true, _): return .hide
|
||||
case (.legacyGroup, _, true), (.group, _, true), (.community, _, _): return .leave
|
||||
default: return .delete
|
||||
}
|
||||
}()
|
||||
|
||||
return UIContextualAction.configuration(
|
||||
for: UIContextualAction.generateSwipeActions(
|
||||
[
|
||||
(!shouldHavePinAction ? nil : .pin),
|
||||
(!shouldHaveMuteAction ? nil : .mute),
|
||||
destructiveAction
|
||||
].compactMap { $0 },
|
||||
for: .trailing,
|
||||
indexPath: indexPath,
|
||||
tableView: tableView,
|
||||
threadViewModel: threadViewModel,
|
||||
viewController: self
|
||||
)
|
||||
)
|
||||
|
||||
default: return nil
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Swipe action generation
|
||||
|
||||
private enum SwipeAction {
|
||||
case toggleReadStatus
|
||||
case hide
|
||||
case pin
|
||||
case block
|
||||
case delete
|
||||
}
|
||||
|
||||
private func generateSwipeActions(
|
||||
_ actions: [SwipeAction],
|
||||
for side: UIContextualAction.Side,
|
||||
indexPath: IndexPath,
|
||||
tableView: UITableView
|
||||
) -> UISwipeActionsConfiguration? {
|
||||
guard !actions.isEmpty else { return nil }
|
||||
|
||||
let section: HomeViewModel.SectionModel = self.viewModel.threadData[indexPath.section]
|
||||
let threadViewModel: SessionThreadViewModel = section.elements[indexPath.row]
|
||||
let unswipeAnimationDelay: DispatchTimeInterval = .milliseconds(500)
|
||||
|
||||
// Note: for some reason the `UISwipeActionsConfiguration` expects actions to be left-to-right
|
||||
// for leading actions, but right-to-left for trailing actions...
|
||||
let targetActions: [SwipeAction] = (side == .trailing ? actions.reversed() : actions)
|
||||
|
||||
return UISwipeActionsConfiguration(
|
||||
actions: targetActions
|
||||
.enumerated()
|
||||
.map { index, action -> UIContextualAction in
|
||||
// Even though we have to reverse the actions above, the indexes in the view hierarchy
|
||||
// are in the expected order
|
||||
let targetIndex: Int = (side == .trailing ? (targetActions.count - index) : index)
|
||||
|
||||
switch action {
|
||||
// MARK: -- toggleReadStatus
|
||||
|
||||
case .toggleReadStatus:
|
||||
let isUnread: Bool = (
|
||||
threadViewModel.threadWasMarkedUnread == true ||
|
||||
(threadViewModel.threadUnreadCount ?? 0) > 0
|
||||
)
|
||||
|
||||
return UIContextualAction(
|
||||
title: (isUnread ?
|
||||
"MARK_AS_READ".localized() :
|
||||
"MARK_AS_UNREAD".localized()
|
||||
),
|
||||
icon: (isUnread ?
|
||||
UIImage(systemName: "envelope.open") :
|
||||
UIImage(systemName: "envelope.badge")
|
||||
),
|
||||
themeTintColor: .white,
|
||||
themeBackgroundColor: .conversationButton_swipeRead,
|
||||
side: side,
|
||||
actionIndex: targetIndex,
|
||||
indexPath: indexPath,
|
||||
tableView: tableView
|
||||
) { [weak self] _, _, completionHandler in
|
||||
// Delay the change to give the cell "unswipe" animation some time to complete
|
||||
DispatchQueue.global(qos: .default).asyncAfter(deadline: .now() + unswipeAnimationDelay) {
|
||||
switch isUnread {
|
||||
case true:
|
||||
self?.viewModel.markAsRead(
|
||||
threadViewModel: threadViewModel,
|
||||
target: .threadAndInteractions(
|
||||
interactionsBeforeInclusive: threadViewModel.interactionId
|
||||
)
|
||||
)
|
||||
|
||||
case false:
|
||||
self?.viewModel.markAsUnread(threadViewModel: threadViewModel)
|
||||
}
|
||||
}
|
||||
completionHandler(true)
|
||||
}
|
||||
|
||||
// MARK: -- hide
|
||||
|
||||
case .hide:
|
||||
return UIContextualAction(
|
||||
title: "TXT_HIDE_TITLE".localized(),
|
||||
icon: UIImage(systemName: "eye.slash"),
|
||||
themeTintColor: .white,
|
||||
themeBackgroundColor: .conversationButton_swipeDestructive,
|
||||
side: side,
|
||||
actionIndex: targetIndex,
|
||||
indexPath: indexPath,
|
||||
tableView: tableView
|
||||
) { _, _, completionHandler in
|
||||
Storage.shared.write { db in db[.hasHiddenMessageRequests] = true }
|
||||
completionHandler(true)
|
||||
}
|
||||
|
||||
// MARK: -- pin
|
||||
|
||||
case .pin:
|
||||
return UIContextualAction(
|
||||
title: (threadViewModel.threadPinnedPriority > 0 ?
|
||||
"UNPIN_BUTTON_TEXT".localized() :
|
||||
"PIN_BUTTON_TEXT".localized()
|
||||
),
|
||||
icon: (threadViewModel.threadPinnedPriority > 0 ?
|
||||
UIImage(systemName: "pin.slash") :
|
||||
UIImage(systemName: "pin")
|
||||
),
|
||||
themeTintColor: .white,
|
||||
themeBackgroundColor: .conversationButton_swipeTertiary,
|
||||
side: side,
|
||||
actionIndex: targetIndex,
|
||||
indexPath: indexPath,
|
||||
tableView: tableView
|
||||
) { _, _, completionHandler in
|
||||
(tableView.cellForRow(at: indexPath) as? FullConversationCell)?.optimisticUpdate(
|
||||
isPinned: !(threadViewModel.threadPinnedPriority > 0)
|
||||
)
|
||||
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 SessionThread
|
||||
.filter(id: threadViewModel.threadId)
|
||||
.updateAllAndConfig(
|
||||
db,
|
||||
SessionThread.Columns.pinnedPriority
|
||||
.set(to: (threadViewModel.threadPinnedPriority == 0 ? 1 : 0))
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: -- block
|
||||
|
||||
case .block:
|
||||
return UIContextualAction(
|
||||
title: (threadViewModel.threadIsBlocked == true ?
|
||||
"BLOCK_LIST_UNBLOCK_BUTTON".localized() :
|
||||
"BLOCK_LIST_BLOCK_BUTTON".localized()
|
||||
),
|
||||
icon: UIImage(named: "table_ic_block"),
|
||||
themeTintColor: .white,
|
||||
themeBackgroundColor: .conversationButton_swipeSecondary,
|
||||
side: .trailing,
|
||||
actionIndex: 1,
|
||||
indexPath: indexPath,
|
||||
tableView: tableView
|
||||
) { _, _, completionHandler in
|
||||
(tableView.cellForRow(at: indexPath) as? FullConversationCell)?.optimisticUpdate(
|
||||
isBlocked: (threadViewModel.threadIsBlocked == false)
|
||||
)
|
||||
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
|
||||
.writePublisher(receiveOn: DispatchQueue.global(qos: .userInitiated)) { db in
|
||||
try Contact
|
||||
.filter(id: threadViewModel.threadId)
|
||||
.updateAllAndConfig(
|
||||
db,
|
||||
Contact.Columns.isBlocked.set(
|
||||
to: (threadViewModel.threadIsBlocked == false ?
|
||||
true:
|
||||
false
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
.sinkUntilComplete()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: -- delete
|
||||
|
||||
case .delete:
|
||||
return UIContextualAction(
|
||||
title: "TXT_DELETE_TITLE".localized(),
|
||||
icon: UIImage(named: "icon_bin"),
|
||||
themeTintColor: .white,
|
||||
themeBackgroundColor: .conversationButton_swipeDestructive,
|
||||
side: side,
|
||||
actionIndex: targetIndex,
|
||||
indexPath: indexPath,
|
||||
tableView: tableView
|
||||
) { [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.deleteOrLeave(
|
||||
threadId: threadViewModel.threadId,
|
||||
threadVariant: threadViewModel.threadVariant
|
||||
)
|
||||
self?.dismiss(animated: true, completion: nil)
|
||||
|
||||
completionHandler(true)
|
||||
},
|
||||
afterClosed: { completionHandler(false) }
|
||||
)
|
||||
)
|
||||
|
||||
self?.present(confirmationModal, animated: true, completion: nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// MARK: - Interaction
|
||||
|
||||
func handleContinueButtonTapped(from seedReminderView: SeedReminderView) {
|
||||
|
|
|
@ -271,7 +271,11 @@ public class HomeViewModel {
|
|||
|
||||
PagedData.processAndTriggerUpdates(
|
||||
updatedData: updatedThreadData,
|
||||
currentDataRetriever: { [weak self] in (self?.unobservedThreadDataChanges?.0 ?? self?.threadData) },
|
||||
currentDataRetriever: { [weak self] in
|
||||
guard self?.hasProcessedInitialThreadData == true else { return nil }
|
||||
|
||||
return (self?.unobservedThreadDataChanges?.0 ?? self?.threadData)
|
||||
},
|
||||
onDataChange: onThreadChange,
|
||||
onUnobservedDataChange: { [weak self] updatedData, changeset in
|
||||
self?.unobservedThreadDataChanges = (changeset.isEmpty ?
|
||||
|
@ -284,12 +288,15 @@ public class HomeViewModel {
|
|||
|
||||
// MARK: - Thread Data
|
||||
|
||||
private var hasProcessedInitialThreadData: Bool = false
|
||||
public private(set) var unobservedThreadDataChanges: ([SectionModel], StagedChangeset<[SectionModel]>)?
|
||||
public private(set) var threadData: [SectionModel] = []
|
||||
public private(set) var pagedDataObserver: PagedDatabaseObserver<SessionThread, SessionThreadViewModel>?
|
||||
|
||||
public var onThreadChange: (([SectionModel], StagedChangeset<[SectionModel]>) -> ())? {
|
||||
didSet {
|
||||
self.hasProcessedInitialThreadData = (onThreadChange != nil || hasProcessedInitialThreadData)
|
||||
|
||||
// When starting to observe interaction changes we want to trigger a UI update just in case the
|
||||
// data was changed while we weren't observing
|
||||
if let unobservedThreadDataChanges: ([SectionModel], StagedChangeset<[SectionModel]>) = self.unobservedThreadDataChanges {
|
||||
|
@ -317,7 +324,10 @@ public class HomeViewModel {
|
|||
[SectionModel(
|
||||
section: .messageRequests,
|
||||
elements: [
|
||||
SessionThreadViewModel(unreadCount: UInt(finalUnreadMessageRequestCount))
|
||||
SessionThreadViewModel(
|
||||
threadId: SessionThreadViewModel.messageRequestsSectionId,
|
||||
unreadCount: UInt(finalUnreadMessageRequestCount)
|
||||
)
|
||||
]
|
||||
)]
|
||||
),
|
||||
|
@ -352,29 +362,4 @@ public class HomeViewModel {
|
|||
public func updateThreadData(_ updatedData: [SectionModel]) {
|
||||
self.threadData = updatedData
|
||||
}
|
||||
|
||||
// MARK: - Functions
|
||||
|
||||
public func markAsRead(
|
||||
threadViewModel: SessionThreadViewModel,
|
||||
target: SessionThreadViewModel.ReadTarget
|
||||
) {
|
||||
threadViewModel.markAsRead(target: target)
|
||||
}
|
||||
|
||||
public func markAsUnread(threadViewModel: SessionThreadViewModel) {
|
||||
threadViewModel.markAsUnread()
|
||||
}
|
||||
|
||||
public func deleteOrLeave(threadId: String, threadVariant: SessionThread.Variant) {
|
||||
Storage.shared.writeAsync { db in
|
||||
try SessionThread.deleteOrLeave(
|
||||
db,
|
||||
threadId: threadId,
|
||||
threadVariant: threadVariant,
|
||||
shouldSendLeaveMessageForGroups: true,
|
||||
calledFromConfigHandling: false
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -404,54 +404,23 @@ class MessageRequestsViewController: BaseVC, SessionUtilRespondingViewController
|
|||
|
||||
func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
|
||||
let section: MessageRequestsViewModel.SectionModel = self.viewModel.threadData[indexPath.section]
|
||||
let threadViewModel: SessionThreadViewModel = section.elements[indexPath.row]
|
||||
|
||||
switch section.model {
|
||||
case .threads:
|
||||
let threadId: String = section.elements[indexPath.row].threadId
|
||||
let threadVariant: SessionThread.Variant = section.elements[indexPath.row].threadVariant
|
||||
let delete: UIContextualAction = UIContextualAction(
|
||||
title: "TXT_DELETE_TITLE".localized(),
|
||||
icon: UIImage(named: "icon_bin"),
|
||||
themeTintColor: .white,
|
||||
themeBackgroundColor: .conversationButton_swipeDestructive,
|
||||
side: .trailing,
|
||||
actionIndex: (threadVariant == .contact ? 1 : 0),
|
||||
indexPath: indexPath,
|
||||
tableView: tableView
|
||||
) { [weak self] _, _, completionHandler in
|
||||
MessageRequestsViewModel.deleteMessageRequest(
|
||||
threadId: threadId,
|
||||
threadVariant: threadVariant,
|
||||
return UIContextualAction.configuration(
|
||||
for: UIContextualAction.generateSwipeActions(
|
||||
[
|
||||
(threadViewModel.threadVariant != .contact ? nil : .block),
|
||||
.delete
|
||||
].compactMap { $0 },
|
||||
for: .trailing,
|
||||
indexPath: indexPath,
|
||||
tableView: tableView,
|
||||
threadViewModel: threadViewModel,
|
||||
viewController: self
|
||||
)
|
||||
completionHandler(true)
|
||||
}
|
||||
|
||||
switch threadVariant {
|
||||
case .contact:
|
||||
let block: UIContextualAction = UIContextualAction(
|
||||
title: "BLOCK_LIST_BLOCK_BUTTON".localized(),
|
||||
icon: UIImage(named: "table_ic_block"),
|
||||
themeTintColor: .white,
|
||||
themeBackgroundColor: .conversationButton_swipeSecondary,
|
||||
side: .trailing,
|
||||
actionIndex: 0,
|
||||
indexPath: indexPath,
|
||||
tableView: tableView
|
||||
) { [weak self] _, _, completionHandler in
|
||||
MessageRequestsViewModel.blockMessageRequest(
|
||||
threadId: threadId,
|
||||
threadVariant: threadVariant,
|
||||
viewController: self
|
||||
)
|
||||
completionHandler(true)
|
||||
}
|
||||
|
||||
return UISwipeActionsConfiguration(actions: [ delete, block ])
|
||||
|
||||
case .legacyGroup, .group, .community:
|
||||
return UISwipeActionsConfiguration(actions: [ delete ])
|
||||
}
|
||||
)
|
||||
|
||||
default: return nil
|
||||
}
|
||||
|
|
|
@ -171,86 +171,6 @@ public class MessageRequestsViewModel {
|
|||
|
||||
// MARK: - Functions
|
||||
|
||||
static func deleteMessageRequest(
|
||||
threadId: String,
|
||||
threadVariant: SessionThread.Variant,
|
||||
viewController: UIViewController?,
|
||||
completion: (() -> Void)? = nil
|
||||
) {
|
||||
let modal: ConfirmationModal = ConfirmationModal(
|
||||
info: ConfirmationModal.Info(
|
||||
title: "MESSAGE_REQUESTS_DELETE_CONFIRMATION_ACTON".localized(),
|
||||
confirmTitle: "TXT_DELETE_TITLE".localized(),
|
||||
confirmStyle: .danger,
|
||||
cancelStyle: .alert_text
|
||||
) { _ in
|
||||
Storage.shared.write { db in
|
||||
try SessionThread.deleteOrLeave(
|
||||
db,
|
||||
threadId: threadId,
|
||||
threadVariant: threadVariant,
|
||||
shouldSendLeaveMessageForGroups: false,
|
||||
calledFromConfigHandling: false
|
||||
)
|
||||
}
|
||||
|
||||
completion?()
|
||||
}
|
||||
)
|
||||
|
||||
viewController?.present(modal, animated: true, completion: nil)
|
||||
}
|
||||
|
||||
static func blockMessageRequest(
|
||||
threadId: String,
|
||||
threadVariant: SessionThread.Variant,
|
||||
viewController: UIViewController?,
|
||||
completion: (() -> Void)? = nil
|
||||
) {
|
||||
guard threadVariant == .contact else { return }
|
||||
|
||||
let modal: ConfirmationModal = ConfirmationModal(
|
||||
info: ConfirmationModal.Info(
|
||||
title: "MESSAGE_REQUESTS_BLOCK_CONFIRMATION_ACTON".localized(),
|
||||
confirmTitle: "BLOCK_LIST_BLOCK_BUTTON".localized(),
|
||||
confirmStyle: .danger,
|
||||
cancelStyle: .alert_text
|
||||
) { _ in
|
||||
Storage.shared.writeAsync(
|
||||
updates: { db in
|
||||
// Update the contact
|
||||
try Contact
|
||||
.fetchOrCreate(db, id: threadId)
|
||||
.save(db)
|
||||
try Contact
|
||||
.filter(id: threadId)
|
||||
.updateAllAndConfig(
|
||||
db,
|
||||
Contact.Columns.isApproved.set(to: false),
|
||||
Contact.Columns.isBlocked.set(to: true),
|
||||
|
||||
// Note: We set this to true so the current user will be able to send a
|
||||
// message to the person who originally sent them the message request in
|
||||
// the future if they unblock them
|
||||
Contact.Columns.didApproveMe.set(to: true)
|
||||
)
|
||||
|
||||
try SessionThread.deleteOrLeave(
|
||||
db,
|
||||
threadId: threadId,
|
||||
threadVariant: .contact,
|
||||
shouldSendLeaveMessageForGroups: false,
|
||||
calledFromConfigHandling: false
|
||||
)
|
||||
},
|
||||
completion: { _, _ in completion?() }
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
viewController?.present(modal, animated: true, completion: nil)
|
||||
}
|
||||
|
||||
static func clearAllRequests(
|
||||
contactThreadIds: [String],
|
||||
groupThreadIds: [String]
|
||||
|
@ -262,7 +182,7 @@ public class MessageRequestsViewModel {
|
|||
db,
|
||||
threadIds: contactThreadIds,
|
||||
threadVariant: .contact,
|
||||
shouldSendLeaveMessageForGroups: false,
|
||||
groupLeaveType: .silent,
|
||||
calledFromConfigHandling: false
|
||||
)
|
||||
|
||||
|
@ -271,7 +191,7 @@ public class MessageRequestsViewModel {
|
|||
db,
|
||||
threadIds: groupThreadIds,
|
||||
threadVariant: .group,
|
||||
shouldSendLeaveMessageForGroups: false,
|
||||
groupLeaveType: .silent,
|
||||
calledFromConfigHandling: false
|
||||
)
|
||||
}
|
||||
|
|
|
@ -539,7 +539,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
|
||||
threadId: threadId,
|
||||
|
|
|
@ -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,6 +601,22 @@
|
|||
"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_group_confirmation_alert_title" = "Delete Group";
|
||||
"delete_group_confirmation_alert_message" = "Are you sure you want to delete %@?";
|
||||
"delete_conversation_confirmation_alert_title" = "Delete Conversation";
|
||||
"delete_conversation_confirmation_alert_message" = "Are you sure you want to delete your conversation with %@?";
|
||||
"hide_note_to_self_confirmation_alert_title" = "Hide Note to Self";
|
||||
"hide_note_to_self_confirmation_alert_message" = "Are you sure you want to hide %@?";
|
||||
"REMOVE_AVATAR" = "Remove";
|
||||
"CONTACT_NICKNAME_PLACEHOLDER" = "Enter a name";
|
||||
"MARK_AS_READ" = "Mark Read";
|
||||
|
|
|
@ -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,6 +601,22 @@
|
|||
"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_group_confirmation_alert_title" = "Delete Group";
|
||||
"delete_group_confirmation_alert_message" = "Are you sure you want to delete %@?";
|
||||
"delete_conversation_confirmation_alert_title" = "Delete Conversation";
|
||||
"delete_conversation_confirmation_alert_message" = "Are you sure you want to delete your conversation with %@?";
|
||||
"hide_note_to_self_confirmation_alert_title" = "Hide Note to Self";
|
||||
"hide_note_to_self_confirmation_alert_message" = "Are you sure you want to hide %@?";
|
||||
"REMOVE_AVATAR" = "Remove";
|
||||
"CONTACT_NICKNAME_PLACEHOLDER" = "Enter a name";
|
||||
"MARK_AS_READ" = "Mark Read";
|
||||
|
|
|
@ -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,6 +601,22 @@
|
|||
"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_group_confirmation_alert_title" = "Delete Group";
|
||||
"delete_group_confirmation_alert_message" = "Are you sure you want to delete %@?";
|
||||
"delete_conversation_confirmation_alert_title" = "Delete Conversation";
|
||||
"delete_conversation_confirmation_alert_message" = "Are you sure you want to delete your conversation with %@?";
|
||||
"hide_note_to_self_confirmation_alert_title" = "Hide Note to Self";
|
||||
"hide_note_to_self_confirmation_alert_message" = "Are you sure you want to hide %@?";
|
||||
"REMOVE_AVATAR" = "Remove";
|
||||
"CONTACT_NICKNAME_PLACEHOLDER" = "Enter a name";
|
||||
"MARK_AS_READ" = "Mark Read";
|
||||
|
|
|
@ -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,6 +601,22 @@
|
|||
"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_group_confirmation_alert_title" = "Delete Group";
|
||||
"delete_group_confirmation_alert_message" = "Are you sure you want to delete %@?";
|
||||
"delete_conversation_confirmation_alert_title" = "Delete Conversation";
|
||||
"delete_conversation_confirmation_alert_message" = "Are you sure you want to delete your conversation with %@?";
|
||||
"hide_note_to_self_confirmation_alert_title" = "Hide Note to Self";
|
||||
"hide_note_to_self_confirmation_alert_message" = "Are you sure you want to hide %@?";
|
||||
"REMOVE_AVATAR" = "Remove";
|
||||
"CONTACT_NICKNAME_PLACEHOLDER" = "Enter a name";
|
||||
"MARK_AS_READ" = "Mark Read";
|
||||
|
|
|
@ -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,6 +601,22 @@
|
|||
"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_group_confirmation_alert_title" = "Delete Group";
|
||||
"delete_group_confirmation_alert_message" = "Are you sure you want to delete %@?";
|
||||
"delete_conversation_confirmation_alert_title" = "Delete Conversation";
|
||||
"delete_conversation_confirmation_alert_message" = "Are you sure you want to delete your conversation with %@?";
|
||||
"hide_note_to_self_confirmation_alert_title" = "Hide Note to Self";
|
||||
"hide_note_to_self_confirmation_alert_message" = "Are you sure you want to hide %@?";
|
||||
"REMOVE_AVATAR" = "Remove";
|
||||
"CONTACT_NICKNAME_PLACEHOLDER" = "Enter a name";
|
||||
"MARK_AS_READ" = "Mark Read";
|
||||
|
|
|
@ -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,6 +601,22 @@
|
|||
"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_group_confirmation_alert_title" = "Delete Group";
|
||||
"delete_group_confirmation_alert_message" = "Are you sure you want to delete %@?";
|
||||
"delete_conversation_confirmation_alert_title" = "Delete Conversation";
|
||||
"delete_conversation_confirmation_alert_message" = "Are you sure you want to delete your conversation with %@?";
|
||||
"hide_note_to_self_confirmation_alert_title" = "Hide Note to Self";
|
||||
"hide_note_to_self_confirmation_alert_message" = "Are you sure you want to hide %@?";
|
||||
"REMOVE_AVATAR" = "Remove";
|
||||
"CONTACT_NICKNAME_PLACEHOLDER" = "Enter a name";
|
||||
"MARK_AS_READ" = "Mark Read";
|
||||
|
|
|
@ -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,6 +601,22 @@
|
|||
"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_group_confirmation_alert_title" = "Delete Group";
|
||||
"delete_group_confirmation_alert_message" = "Are you sure you want to delete %@?";
|
||||
"delete_conversation_confirmation_alert_title" = "Delete Conversation";
|
||||
"delete_conversation_confirmation_alert_message" = "Are you sure you want to delete your conversation with %@?";
|
||||
"hide_note_to_self_confirmation_alert_title" = "Hide Note to Self";
|
||||
"hide_note_to_self_confirmation_alert_message" = "Are you sure you want to hide %@?";
|
||||
"REMOVE_AVATAR" = "Remove";
|
||||
"CONTACT_NICKNAME_PLACEHOLDER" = "Enter a name";
|
||||
"MARK_AS_READ" = "Mark Read";
|
||||
|
|
|
@ -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,6 +601,22 @@
|
|||
"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_group_confirmation_alert_title" = "Delete Group";
|
||||
"delete_group_confirmation_alert_message" = "Are you sure you want to delete %@?";
|
||||
"delete_conversation_confirmation_alert_title" = "Delete Conversation";
|
||||
"delete_conversation_confirmation_alert_message" = "Are you sure you want to delete your conversation with %@?";
|
||||
"hide_note_to_self_confirmation_alert_title" = "Hide Note to Self";
|
||||
"hide_note_to_self_confirmation_alert_message" = "Are you sure you want to hide %@?";
|
||||
"REMOVE_AVATAR" = "Remove";
|
||||
"CONTACT_NICKNAME_PLACEHOLDER" = "Enter a name";
|
||||
"MARK_AS_READ" = "Mark Read";
|
||||
|
|
|
@ -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,6 +601,22 @@
|
|||
"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_group_confirmation_alert_title" = "Delete Group";
|
||||
"delete_group_confirmation_alert_message" = "Are you sure you want to delete %@?";
|
||||
"delete_conversation_confirmation_alert_title" = "Delete Conversation";
|
||||
"delete_conversation_confirmation_alert_message" = "Are you sure you want to delete your conversation with %@?";
|
||||
"hide_note_to_self_confirmation_alert_title" = "Hide Note to Self";
|
||||
"hide_note_to_self_confirmation_alert_message" = "Are you sure you want to hide %@?";
|
||||
"REMOVE_AVATAR" = "Remove";
|
||||
"CONTACT_NICKNAME_PLACEHOLDER" = "Enter a name";
|
||||
"MARK_AS_READ" = "Mark Read";
|
||||
|
|
|
@ -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,6 +601,22 @@
|
|||
"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_group_confirmation_alert_title" = "Delete Group";
|
||||
"delete_group_confirmation_alert_message" = "Are you sure you want to delete %@?";
|
||||
"delete_conversation_confirmation_alert_title" = "Delete Conversation";
|
||||
"delete_conversation_confirmation_alert_message" = "Are you sure you want to delete your conversation with %@?";
|
||||
"hide_note_to_self_confirmation_alert_title" = "Hide Note to Self";
|
||||
"hide_note_to_self_confirmation_alert_message" = "Are you sure you want to hide %@?";
|
||||
"REMOVE_AVATAR" = "Remove";
|
||||
"CONTACT_NICKNAME_PLACEHOLDER" = "Enter a name";
|
||||
"MARK_AS_READ" = "Mark Read";
|
||||
|
|
|
@ -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,6 +601,22 @@
|
|||
"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_group_confirmation_alert_title" = "Delete Group";
|
||||
"delete_group_confirmation_alert_message" = "Are you sure you want to delete %@?";
|
||||
"delete_conversation_confirmation_alert_title" = "Delete Conversation";
|
||||
"delete_conversation_confirmation_alert_message" = "Are you sure you want to delete your conversation with %@?";
|
||||
"hide_note_to_self_confirmation_alert_title" = "Hide Note to Self";
|
||||
"hide_note_to_self_confirmation_alert_message" = "Are you sure you want to hide %@?";
|
||||
"REMOVE_AVATAR" = "Remove";
|
||||
"CONTACT_NICKNAME_PLACEHOLDER" = "Enter a name";
|
||||
"MARK_AS_READ" = "Mark Read";
|
||||
|
|
|
@ -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,6 +601,22 @@
|
|||
"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_group_confirmation_alert_title" = "Delete Group";
|
||||
"delete_group_confirmation_alert_message" = "Are you sure you want to delete %@?";
|
||||
"delete_conversation_confirmation_alert_title" = "Delete Conversation";
|
||||
"delete_conversation_confirmation_alert_message" = "Are you sure you want to delete your conversation with %@?";
|
||||
"hide_note_to_self_confirmation_alert_title" = "Hide Note to Self";
|
||||
"hide_note_to_self_confirmation_alert_message" = "Are you sure you want to hide %@?";
|
||||
"REMOVE_AVATAR" = "Remove";
|
||||
"CONTACT_NICKNAME_PLACEHOLDER" = "Enter a name";
|
||||
"MARK_AS_READ" = "Mark Read";
|
||||
|
|
|
@ -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,6 +601,22 @@
|
|||
"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_group_confirmation_alert_title" = "Delete Group";
|
||||
"delete_group_confirmation_alert_message" = "Are you sure you want to delete %@?";
|
||||
"delete_conversation_confirmation_alert_title" = "Delete Conversation";
|
||||
"delete_conversation_confirmation_alert_message" = "Are you sure you want to delete your conversation with %@?";
|
||||
"hide_note_to_self_confirmation_alert_title" = "Hide Note to Self";
|
||||
"hide_note_to_self_confirmation_alert_message" = "Are you sure you want to hide %@?";
|
||||
"REMOVE_AVATAR" = "Remove";
|
||||
"CONTACT_NICKNAME_PLACEHOLDER" = "Enter a name";
|
||||
"MARK_AS_READ" = "Mark Read";
|
||||
|
|
|
@ -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,6 +601,22 @@
|
|||
"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_group_confirmation_alert_title" = "Delete Group";
|
||||
"delete_group_confirmation_alert_message" = "Are you sure you want to delete %@?";
|
||||
"delete_conversation_confirmation_alert_title" = "Delete Conversation";
|
||||
"delete_conversation_confirmation_alert_message" = "Are you sure you want to delete your conversation with %@?";
|
||||
"hide_note_to_self_confirmation_alert_title" = "Hide Note to Self";
|
||||
"hide_note_to_self_confirmation_alert_message" = "Are you sure you want to hide %@?";
|
||||
"REMOVE_AVATAR" = "Remove";
|
||||
"CONTACT_NICKNAME_PLACEHOLDER" = "Enter a name";
|
||||
"MARK_AS_READ" = "Mark Read";
|
||||
|
|
|
@ -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,6 +601,22 @@
|
|||
"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_group_confirmation_alert_title" = "Delete Group";
|
||||
"delete_group_confirmation_alert_message" = "Are you sure you want to delete %@?";
|
||||
"delete_conversation_confirmation_alert_title" = "Delete Conversation";
|
||||
"delete_conversation_confirmation_alert_message" = "Are you sure you want to delete your conversation with %@?";
|
||||
"hide_note_to_self_confirmation_alert_title" = "Hide Note to Self";
|
||||
"hide_note_to_self_confirmation_alert_message" = "Are you sure you want to hide %@?";
|
||||
"REMOVE_AVATAR" = "Remove";
|
||||
"CONTACT_NICKNAME_PLACEHOLDER" = "Enter a name";
|
||||
"MARK_AS_READ" = "Mark Read";
|
||||
|
|
|
@ -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,6 +601,22 @@
|
|||
"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_group_confirmation_alert_title" = "Delete Group";
|
||||
"delete_group_confirmation_alert_message" = "Are you sure you want to delete %@?";
|
||||
"delete_conversation_confirmation_alert_title" = "Delete Conversation";
|
||||
"delete_conversation_confirmation_alert_message" = "Are you sure you want to delete your conversation with %@?";
|
||||
"hide_note_to_self_confirmation_alert_title" = "Hide Note to Self";
|
||||
"hide_note_to_self_confirmation_alert_message" = "Are you sure you want to hide %@?";
|
||||
"REMOVE_AVATAR" = "Remove";
|
||||
"CONTACT_NICKNAME_PLACEHOLDER" = "Enter a name";
|
||||
"MARK_AS_READ" = "Mark Read";
|
||||
|
|
|
@ -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,6 +601,22 @@
|
|||
"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_group_confirmation_alert_title" = "Delete Group";
|
||||
"delete_group_confirmation_alert_message" = "Are you sure you want to delete %@?";
|
||||
"delete_conversation_confirmation_alert_title" = "Delete Conversation";
|
||||
"delete_conversation_confirmation_alert_message" = "Are you sure you want to delete your conversation with %@?";
|
||||
"hide_note_to_self_confirmation_alert_title" = "Hide Note to Self";
|
||||
"hide_note_to_self_confirmation_alert_message" = "Are you sure you want to hide %@?";
|
||||
"REMOVE_AVATAR" = "Remove";
|
||||
"CONTACT_NICKNAME_PLACEHOLDER" = "Enter a name";
|
||||
"MARK_AS_READ" = "Mark Read";
|
||||
|
|
|
@ -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,6 +601,22 @@
|
|||
"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_group_confirmation_alert_title" = "Delete Group";
|
||||
"delete_group_confirmation_alert_message" = "Are you sure you want to delete %@?";
|
||||
"delete_conversation_confirmation_alert_title" = "Delete Conversation";
|
||||
"delete_conversation_confirmation_alert_message" = "Are you sure you want to delete your conversation with %@?";
|
||||
"hide_note_to_self_confirmation_alert_title" = "Hide Note to Self";
|
||||
"hide_note_to_self_confirmation_alert_message" = "Are you sure you want to hide %@?";
|
||||
"REMOVE_AVATAR" = "Remove";
|
||||
"CONTACT_NICKNAME_PLACEHOLDER" = "Enter a name";
|
||||
"MARK_AS_READ" = "Mark Read";
|
||||
|
|
|
@ -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,6 +601,22 @@
|
|||
"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_group_confirmation_alert_title" = "Delete Group";
|
||||
"delete_group_confirmation_alert_message" = "Are you sure you want to delete %@?";
|
||||
"delete_conversation_confirmation_alert_title" = "Delete Conversation";
|
||||
"delete_conversation_confirmation_alert_message" = "Are you sure you want to delete your conversation with %@?";
|
||||
"hide_note_to_self_confirmation_alert_title" = "Hide Note to Self";
|
||||
"hide_note_to_self_confirmation_alert_message" = "Are you sure you want to hide %@?";
|
||||
"REMOVE_AVATAR" = "Remove";
|
||||
"CONTACT_NICKNAME_PLACEHOLDER" = "Enter a name";
|
||||
"MARK_AS_READ" = "Mark Read";
|
||||
|
|
|
@ -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,6 +601,22 @@
|
|||
"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_group_confirmation_alert_title" = "Delete Group";
|
||||
"delete_group_confirmation_alert_message" = "Are you sure you want to delete %@?";
|
||||
"delete_conversation_confirmation_alert_title" = "Delete Conversation";
|
||||
"delete_conversation_confirmation_alert_message" = "Are you sure you want to delete your conversation with %@?";
|
||||
"hide_note_to_self_confirmation_alert_title" = "Hide Note to Self";
|
||||
"hide_note_to_self_confirmation_alert_message" = "Are you sure you want to hide %@?";
|
||||
"REMOVE_AVATAR" = "Remove";
|
||||
"CONTACT_NICKNAME_PLACEHOLDER" = "Enter a name";
|
||||
"MARK_AS_READ" = "Mark Read";
|
||||
|
|
|
@ -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,6 +601,22 @@
|
|||
"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_group_confirmation_alert_title" = "Delete Group";
|
||||
"delete_group_confirmation_alert_message" = "Are you sure you want to delete %@?";
|
||||
"delete_conversation_confirmation_alert_title" = "Delete Conversation";
|
||||
"delete_conversation_confirmation_alert_message" = "Are you sure you want to delete your conversation with %@?";
|
||||
"hide_note_to_self_confirmation_alert_title" = "Hide Note to Self";
|
||||
"hide_note_to_self_confirmation_alert_message" = "Are you sure you want to hide %@?";
|
||||
"REMOVE_AVATAR" = "Remove";
|
||||
"CONTACT_NICKNAME_PLACEHOLDER" = "Enter a name";
|
||||
"MARK_AS_READ" = "Mark Read";
|
||||
|
|
|
@ -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,6 +601,22 @@
|
|||
"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_group_confirmation_alert_title" = "Delete Group";
|
||||
"delete_group_confirmation_alert_message" = "Are you sure you want to delete %@?";
|
||||
"delete_conversation_confirmation_alert_title" = "Delete Conversation";
|
||||
"delete_conversation_confirmation_alert_message" = "Are you sure you want to delete your conversation with %@?";
|
||||
"hide_note_to_self_confirmation_alert_title" = "Hide Note to Self";
|
||||
"hide_note_to_self_confirmation_alert_message" = "Are you sure you want to hide %@?";
|
||||
"REMOVE_AVATAR" = "Remove";
|
||||
"CONTACT_NICKNAME_PLACEHOLDER" = "Enter a name";
|
||||
"MARK_AS_READ" = "Mark Read";
|
||||
|
|
|
@ -237,6 +237,7 @@ public class NotificationPresenter: NSObject, NotificationsProtocol {
|
|||
|
||||
let userPublicKey: String = getUserHexEncodedPublicKey(db)
|
||||
let userBlindedKey: String? = SessionThread.getUserHexEncodedBlindedKey(
|
||||
db,
|
||||
threadId: thread.id,
|
||||
threadVariant: thread.variant
|
||||
)
|
||||
|
|
|
@ -5,7 +5,7 @@ import SessionUIKit
|
|||
import SignalUtilitiesKit
|
||||
import SessionMessagingKit
|
||||
|
||||
public final class FullConversationCell: UITableViewCell {
|
||||
public final class FullConversationCell: UITableViewCell, SwipeActionOptimisticCell {
|
||||
public static let unreadCountViewSize: CGFloat = 20
|
||||
private static let statusIndicatorSize: CGFloat = 14
|
||||
|
||||
|
@ -394,7 +394,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.threadPinnedPriority == 0)
|
||||
|
@ -434,12 +434,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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -455,10 +477,23 @@ public final class FullConversationCell: UITableViewCell {
|
|||
)
|
||||
}
|
||||
|
||||
// MARK: - SwipeActionOptimisticCell
|
||||
|
||||
public func optimisticUpdate(
|
||||
isBlocked: Bool? = nil,
|
||||
isPinned: Bool? = nil
|
||||
isMuted: Bool?,
|
||||
isBlocked: Bool?,
|
||||
isPinned: Bool?,
|
||||
hasUnread: Bool?
|
||||
) {
|
||||
// TODO: Decide on this
|
||||
if let isMuted: Bool = isMuted {
|
||||
if isMuted {
|
||||
|
||||
} else {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
if let isBlocked: Bool = isBlocked {
|
||||
if isBlocked {
|
||||
accentLineView.themeBackgroundColor = .danger
|
||||
|
@ -466,16 +501,25 @@ public final class FullConversationCell: UITableViewCell {
|
|||
}
|
||||
else {
|
||||
accentLineView.themeBackgroundColor = .conversationButton_unreadStripBackground
|
||||
accentLineView.alpha = (!unreadCountView.isHidden || !unreadImageView.isHidden ?
|
||||
1 :
|
||||
0.0001 // Setting the alpha to exactly 0 causes an issue on iOS 12
|
||||
)
|
||||
accentLineView.alpha = (!unreadCountView.isHidden || !unreadImageView.isHidden ? 1 : 0)
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
|
@ -514,7 +558,10 @@ public final class FullConversationCell: UITableViewCell {
|
|||
))
|
||||
}
|
||||
|
||||
if cellViewModel.threadVariant == .legacyGroup || cellViewModel.threadVariant == .group || cellViewModel.threadVariant == .community {
|
||||
if
|
||||
(cellViewModel.threadVariant == .legacyGroup || cellViewModel.threadVariant == .group || cellViewModel.threadVariant == .community) &&
|
||||
(cellViewModel.interactionVariant?.isGroupControlMessage == false)
|
||||
{
|
||||
let authorName: String = cellViewModel.authorName(for: cellViewModel.threadVariant)
|
||||
|
||||
result.append(NSAttributedString(
|
||||
|
@ -523,17 +570,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
|
||||
|
|
|
@ -0,0 +1,521 @@
|
|||
// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved.
|
||||
|
||||
import UIKit
|
||||
import SessionMessagingKit
|
||||
import SessionUIKit
|
||||
|
||||
protocol SwipeActionOptimisticCell {
|
||||
func optimisticUpdate(isMuted: Bool?, isBlocked: Bool?, isPinned: Bool?, hasUnread: Bool?)
|
||||
}
|
||||
|
||||
extension SwipeActionOptimisticCell {
|
||||
public func optimisticUpdate(isMuted: Bool) {
|
||||
optimisticUpdate(isMuted: isMuted, isBlocked: nil, isPinned: nil, hasUnread: nil)
|
||||
}
|
||||
|
||||
public func optimisticUpdate(isBlocked: Bool) {
|
||||
optimisticUpdate(isMuted: nil, isBlocked: isBlocked, isPinned: nil, hasUnread: nil)
|
||||
}
|
||||
|
||||
public func optimisticUpdate(isPinned: Bool) {
|
||||
optimisticUpdate(isMuted: nil, isBlocked: nil, isPinned: isPinned, hasUnread: nil)
|
||||
}
|
||||
|
||||
public func optimisticUpdate(hasUnread: Bool) {
|
||||
optimisticUpdate(isMuted: nil, isBlocked: nil, isPinned: nil, hasUnread: hasUnread)
|
||||
}
|
||||
}
|
||||
|
||||
public extension UIContextualAction {
|
||||
enum SwipeAction {
|
||||
case toggleReadStatus
|
||||
case hide
|
||||
case pin
|
||||
case mute
|
||||
case block
|
||||
case leave
|
||||
case delete
|
||||
}
|
||||
|
||||
static func configuration(for actions: [UIContextualAction]?) -> UISwipeActionsConfiguration? {
|
||||
return actions.map { UISwipeActionsConfiguration(actions: $0) }
|
||||
}
|
||||
|
||||
static func generateSwipeActions(
|
||||
_ actions: [SwipeAction],
|
||||
for side: UIContextualAction.Side,
|
||||
indexPath: IndexPath,
|
||||
tableView: UITableView,
|
||||
threadViewModel: SessionThreadViewModel,
|
||||
viewController: UIViewController?
|
||||
) -> [UIContextualAction]? {
|
||||
guard !actions.isEmpty else { return nil }
|
||||
|
||||
let unswipeAnimationDelay: DispatchTimeInterval = .milliseconds(500)
|
||||
|
||||
// Note: for some reason the `UISwipeActionsConfiguration` expects actions to be left-to-right
|
||||
// for leading actions, but right-to-left for trailing actions...
|
||||
let targetActions: [SwipeAction] = (side == .trailing ? actions.reversed() : actions)
|
||||
let actionBackgroundColor: [ThemeValue] = [
|
||||
.conversationButton_swipeDestructive,
|
||||
.conversationButton_swipeSecondary,
|
||||
.conversationButton_swipeTertiary
|
||||
]
|
||||
|
||||
return targetActions
|
||||
.enumerated()
|
||||
.map { index, action -> UIContextualAction in
|
||||
// Even though we have to reverse the actions above, the indexes in the view hierarchy
|
||||
// are in the expected order
|
||||
let targetIndex: Int = (side == .trailing ? (targetActions.count - index) : index)
|
||||
let themeBackgroundColor: ThemeValue = actionBackgroundColor[
|
||||
index % actionBackgroundColor.count
|
||||
]
|
||||
|
||||
switch action {
|
||||
// MARK: -- toggleReadStatus
|
||||
|
||||
case .toggleReadStatus:
|
||||
let isUnread: Bool = (
|
||||
threadViewModel.threadWasMarkedUnread == true ||
|
||||
(threadViewModel.threadUnreadCount ?? 0) > 0
|
||||
)
|
||||
|
||||
return UIContextualAction(
|
||||
title: (isUnread ?
|
||||
"MARK_AS_READ".localized() :
|
||||
"MARK_AS_UNREAD".localized()
|
||||
),
|
||||
icon: (isUnread ?
|
||||
UIImage(systemName: "envelope.open") :
|
||||
UIImage(systemName: "envelope.badge")
|
||||
),
|
||||
themeTintColor: .white,
|
||||
themeBackgroundColor: .conversationButton_swipeRead, // Always Custom
|
||||
side: side,
|
||||
actionIndex: targetIndex,
|
||||
indexPath: indexPath,
|
||||
tableView: tableView
|
||||
) { _, _, completionHandler in
|
||||
// Delay the change to give the cell "unswipe" animation some time to complete
|
||||
DispatchQueue.global(qos: .default).asyncAfter(deadline: .now() + unswipeAnimationDelay) {
|
||||
switch isUnread {
|
||||
case true: threadViewModel.markAsRead(
|
||||
target: .threadAndInteractions(
|
||||
interactionsBeforeInclusive: threadViewModel.interactionId
|
||||
)
|
||||
)
|
||||
|
||||
case false: threadViewModel.markAsUnread()
|
||||
}
|
||||
}
|
||||
completionHandler(true)
|
||||
}
|
||||
|
||||
// MARK: -- hide
|
||||
|
||||
case .hide:
|
||||
return UIContextualAction(
|
||||
title: "TXT_HIDE_TITLE".localized(),
|
||||
icon: UIImage(systemName: "eye.slash"),
|
||||
themeTintColor: .white,
|
||||
themeBackgroundColor: themeBackgroundColor,
|
||||
side: side,
|
||||
actionIndex: targetIndex,
|
||||
indexPath: indexPath,
|
||||
tableView: tableView
|
||||
) { _, _, completionHandler in
|
||||
switch threadViewModel.threadId {
|
||||
case SessionThreadViewModel.messageRequestsSectionId:
|
||||
Storage.shared.write { db in db[.hasHiddenMessageRequests] = true }
|
||||
completionHandler(true)
|
||||
|
||||
default:
|
||||
let confirmationModalExplanation: NSAttributedString = {
|
||||
let message = String(
|
||||
format: "hide_note_to_self_confirmation_alert_message".localized(),
|
||||
threadViewModel.displayName
|
||||
)
|
||||
|
||||
return NSAttributedString(string: message)
|
||||
.adding(
|
||||
attributes: [
|
||||
.font: UIFont.boldSystemFont(ofSize: Values.smallFontSize)
|
||||
],
|
||||
range: (message as NSString).range(of: threadViewModel.displayName)
|
||||
)
|
||||
}()
|
||||
|
||||
let confirmationModal: ConfirmationModal = ConfirmationModal(
|
||||
info: ConfirmationModal.Info(
|
||||
title: "hide_note_to_self_confirmation_alert_title".localized(),
|
||||
attributedExplanation: confirmationModalExplanation,
|
||||
confirmTitle: "TXT_HIDE_TITLE".localized(),
|
||||
confirmStyle: .danger,
|
||||
cancelStyle: .alert_text,
|
||||
dismissOnConfirm: true,
|
||||
onConfirm: { _ in
|
||||
Storage.shared.writeAsync { db in
|
||||
try SessionThread.deleteOrLeave(
|
||||
db,
|
||||
threadId: threadViewModel.threadId,
|
||||
threadVariant: threadViewModel.threadVariant,
|
||||
groupLeaveType: .forced,
|
||||
calledFromConfigHandling: false
|
||||
)
|
||||
}
|
||||
viewController?.dismiss(animated: true, completion: nil)
|
||||
|
||||
completionHandler(true)
|
||||
},
|
||||
afterClosed: { completionHandler(false) }
|
||||
)
|
||||
)
|
||||
|
||||
viewController?.present(confirmationModal, animated: true, completion: nil)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: -- pin
|
||||
|
||||
case .pin:
|
||||
return UIContextualAction(
|
||||
title: (threadViewModel.threadPinnedPriority > 0 ?
|
||||
"UNPIN_BUTTON_TEXT".localized() :
|
||||
"PIN_BUTTON_TEXT".localized()
|
||||
),
|
||||
icon: (threadViewModel.threadPinnedPriority > 0 ?
|
||||
UIImage(systemName: "pin.slash") :
|
||||
UIImage(systemName: "pin")
|
||||
),
|
||||
themeTintColor: .white,
|
||||
themeBackgroundColor: .conversationButton_swipeTertiary, // Always Tertiary
|
||||
side: side,
|
||||
actionIndex: targetIndex,
|
||||
indexPath: indexPath,
|
||||
tableView: tableView
|
||||
) { _, _, completionHandler in
|
||||
(tableView.cellForRow(at: indexPath) as? SwipeActionOptimisticCell)?
|
||||
.optimisticUpdate(
|
||||
isPinned: !(threadViewModel.threadPinnedPriority > 0)
|
||||
)
|
||||
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 SessionThread
|
||||
.filter(id: threadViewModel.threadId)
|
||||
.updateAllAndConfig(
|
||||
db,
|
||||
SessionThread.Columns.pinnedPriority
|
||||
.set(to: (threadViewModel.threadPinnedPriority == 0 ? 1 : 0))
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: -- mute
|
||||
|
||||
case .mute:
|
||||
return UIContextualAction(
|
||||
title: (threadViewModel.threadMutedUntilTimestamp == nil ?
|
||||
"mute_button_text".localized() :
|
||||
"unmute_button_text".localized()
|
||||
),
|
||||
icon: (threadViewModel.threadMutedUntilTimestamp == nil ?
|
||||
UIImage(systemName: "speaker.slash") :
|
||||
UIImage(systemName: "speaker")
|
||||
),
|
||||
iconHeight: Values.mediumFontSize,
|
||||
themeTintColor: .white,
|
||||
themeBackgroundColor: themeBackgroundColor,
|
||||
side: side,
|
||||
actionIndex: targetIndex,
|
||||
indexPath: indexPath,
|
||||
tableView: tableView
|
||||
) { _, _, completionHandler in
|
||||
(tableView.cellForRow(at: indexPath) as? SwipeActionOptimisticCell)?
|
||||
.optimisticUpdate(
|
||||
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
|
||||
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,
|
||||
SessionThread.Columns.mutedUntilTimestamp.set(
|
||||
to: (currentValue == nil ?
|
||||
Date.distantFuture.timeIntervalSince1970 :
|
||||
nil
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: -- block
|
||||
|
||||
case .block:
|
||||
return UIContextualAction(
|
||||
title: (threadViewModel.threadIsBlocked == true ?
|
||||
"BLOCK_LIST_UNBLOCK_BUTTON".localized() :
|
||||
"BLOCK_LIST_BLOCK_BUTTON".localized()
|
||||
),
|
||||
icon: UIImage(named: "table_ic_block"),
|
||||
iconHeight: Values.mediumFontSize,
|
||||
themeTintColor: .white,
|
||||
themeBackgroundColor: themeBackgroundColor,
|
||||
side: side,
|
||||
actionIndex: targetIndex,
|
||||
indexPath: indexPath,
|
||||
tableView: tableView
|
||||
) { [weak viewController] _, _, completionHandler in
|
||||
let threadIsBlocked: Bool = (threadViewModel.threadIsBlocked == true)
|
||||
let threadIsMessageRequest: Bool = (threadViewModel.threadIsMessageRequest == true)
|
||||
let contactChanges: [ConfigColumnAssignment] = [
|
||||
Contact.Columns.isBlocked.set(to: !threadIsBlocked),
|
||||
(!threadIsMessageRequest ? nil : Contact.Columns.isApproved.set(to: false)),
|
||||
|
||||
// Note: We set this to true so the current user will be able to send a
|
||||
// message to the person who originally sent them the message request in
|
||||
// the future if they unblock them
|
||||
(!threadIsMessageRequest ? nil : Contact.Columns.didApproveMe.set(to: true))
|
||||
].compactMap { $0 }
|
||||
|
||||
let performBlock: (UIViewController?) -> () = { viewController in
|
||||
(tableView.cellForRow(at: indexPath) as? SwipeActionOptimisticCell)?
|
||||
.optimisticUpdate(
|
||||
isBlocked: !threadIsBlocked
|
||||
)
|
||||
viewController?.dismiss(animated: true, completion: 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
|
||||
.writePublisher(receiveOn: DispatchQueue.global(qos: .userInitiated)) { db in
|
||||
// Create the contact if it doesn't exist
|
||||
try Contact
|
||||
.fetchOrCreate(db, id: threadViewModel.threadId)
|
||||
.save(db)
|
||||
try Contact
|
||||
.filter(id: threadViewModel.threadId)
|
||||
.updateAllAndConfig(db, contactChanges)
|
||||
|
||||
// Blocked message requests should be deleted
|
||||
if threadIsMessageRequest {
|
||||
try SessionThread.deleteOrLeave(
|
||||
db,
|
||||
threadId: threadViewModel.threadId,
|
||||
threadVariant: .contact,
|
||||
groupLeaveType: .silent,
|
||||
calledFromConfigHandling: false
|
||||
)
|
||||
}
|
||||
}
|
||||
.sinkUntilComplete()
|
||||
}
|
||||
}
|
||||
|
||||
switch threadIsMessageRequest {
|
||||
case false: performBlock(nil)
|
||||
case true:
|
||||
let confirmationModal: ConfirmationModal = ConfirmationModal(
|
||||
info: ConfirmationModal.Info(
|
||||
title: "MESSAGE_REQUESTS_BLOCK_CONFIRMATION_ACTON".localized(),
|
||||
confirmTitle: "BLOCK_LIST_BLOCK_BUTTON".localized(),
|
||||
confirmStyle: .danger,
|
||||
cancelStyle: .alert_text,
|
||||
dismissOnConfirm: true,
|
||||
onConfirm: { _ in
|
||||
performBlock(viewController)
|
||||
},
|
||||
afterClosed: { completionHandler(false) }
|
||||
)
|
||||
)
|
||||
|
||||
viewController?.present(confirmationModal, animated: true, completion: nil)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: -- leave
|
||||
|
||||
case .leave:
|
||||
return UIContextualAction(
|
||||
title: "LEAVE_BUTTON_TITLE".localized(),
|
||||
icon: UIImage(systemName: "rectangle.portrait.and.arrow.right"),
|
||||
iconHeight: Values.mediumFontSize,
|
||||
themeTintColor: .white,
|
||||
themeBackgroundColor: themeBackgroundColor,
|
||||
side: side,
|
||||
actionIndex: targetIndex,
|
||||
indexPath: indexPath,
|
||||
tableView: tableView
|
||||
) { [weak viewController] _, _, completionHandler in
|
||||
let confirmationModalTitle: String = {
|
||||
switch threadViewModel.threadVariant {
|
||||
case .legacyGroup, .group:
|
||||
return "leave_group_confirmation_alert_title".localized()
|
||||
|
||||
default: return "leave_community_confirmation_alert_title".localized()
|
||||
}
|
||||
}()
|
||||
|
||||
let confirmationModalExplanation: NSAttributedString = {
|
||||
if 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: { _ in
|
||||
Storage.shared.writeAsync { db in
|
||||
try SessionThread.deleteOrLeave(
|
||||
db,
|
||||
threadId: threadViewModel.threadId,
|
||||
threadVariant: threadViewModel.threadVariant,
|
||||
groupLeaveType: .standard,
|
||||
calledFromConfigHandling: false
|
||||
)
|
||||
}
|
||||
viewController?.dismiss(animated: true, completion: nil)
|
||||
|
||||
completionHandler(true)
|
||||
},
|
||||
afterClosed: { completionHandler(false) }
|
||||
)
|
||||
)
|
||||
|
||||
viewController?.present(confirmationModal, animated: true, completion: nil)
|
||||
}
|
||||
|
||||
// MARK: -- delete
|
||||
|
||||
case .delete:
|
||||
return UIContextualAction(
|
||||
title: "TXT_DELETE_TITLE".localized(),
|
||||
icon: UIImage(named: "icon_bin"),
|
||||
iconHeight: Values.mediumFontSize,
|
||||
themeTintColor: .white,
|
||||
themeBackgroundColor: themeBackgroundColor,
|
||||
side: side,
|
||||
actionIndex: targetIndex,
|
||||
indexPath: indexPath,
|
||||
tableView: tableView
|
||||
) { [weak viewController] _, _, completionHandler in
|
||||
let isMessageRequest: Bool = (threadViewModel.threadIsMessageRequest == true)
|
||||
let confirmationModalTitle: String = {
|
||||
switch (threadViewModel.threadVariant, isMessageRequest) {
|
||||
case (_, true): return "TXT_DELETE_TITLE".localized()
|
||||
case (.contact, _):
|
||||
return "delete_conversation_confirmation_alert_title".localized()
|
||||
|
||||
case (.legacyGroup, _), (.group, _):
|
||||
return "delete_group_confirmation_alert_title".localized()
|
||||
|
||||
case (.community, _): return "TXT_DELETE_TITLE".localized()
|
||||
}
|
||||
}()
|
||||
let confirmationModalExplanation: NSAttributedString = {
|
||||
guard !isMessageRequest else {
|
||||
return NSAttributedString(
|
||||
string: "MESSAGE_REQUESTS_DELETE_CONFIRMATION_ACTON".localized()
|
||||
)
|
||||
}
|
||||
guard threadViewModel.currentUserIsClosedGroupAdmin == false else {
|
||||
return NSAttributedString(
|
||||
string: "admin_group_leave_warning".localized()
|
||||
)
|
||||
}
|
||||
|
||||
let message = String(
|
||||
format: {
|
||||
switch threadViewModel.threadVariant {
|
||||
case .contact:
|
||||
return
|
||||
"delete_conversation_confirmation_alert_message".localized()
|
||||
|
||||
case .legacyGroup, .group:
|
||||
return
|
||||
"delete_group_confirmation_alert_message".localized()
|
||||
|
||||
case .community:
|
||||
return "leave_community_confirmation_alert_message".localized()
|
||||
}
|
||||
}(),
|
||||
threadViewModel.displayName
|
||||
)
|
||||
|
||||
return NSAttributedString(string: message)
|
||||
.adding(
|
||||
attributes: [
|
||||
.font: UIFont.boldSystemFont(ofSize: Values.smallFontSize)
|
||||
],
|
||||
range: (message as NSString).range(of: threadViewModel.displayName)
|
||||
)
|
||||
}()
|
||||
|
||||
let confirmationModal: ConfirmationModal = ConfirmationModal(
|
||||
info: ConfirmationModal.Info(
|
||||
title: confirmationModalTitle,
|
||||
attributedExplanation: confirmationModalExplanation,
|
||||
confirmTitle: "TXT_DELETE_TITLE".localized(),
|
||||
confirmStyle: .danger,
|
||||
cancelStyle: .alert_text,
|
||||
dismissOnConfirm: true,
|
||||
onConfirm: { _ in
|
||||
Storage.shared.writeAsync { db in
|
||||
try SessionThread.deleteOrLeave(
|
||||
db,
|
||||
threadId: threadViewModel.threadId,
|
||||
threadVariant: threadViewModel.threadVariant,
|
||||
groupLeaveType: (isMessageRequest ? .silent : .forced),
|
||||
calledFromConfigHandling: false
|
||||
)
|
||||
}
|
||||
viewController?.dismiss(animated: true, completion: nil)
|
||||
|
||||
completionHandler(true)
|
||||
},
|
||||
afterClosed: { completionHandler(false) }
|
||||
)
|
||||
)
|
||||
|
||||
viewController?.present(confirmationModal, animated: true, completion: nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -54,6 +54,7 @@ public enum SNMessagingKit { // Just to make the external API nice
|
|||
JobRunner.add(executor: NotifyPushServerJob.self, for: .notifyPushServer)
|
||||
JobRunner.add(executor: SendReadReceiptsJob.self, for: .sendReadReceipts)
|
||||
JobRunner.add(executor: AttachmentUploadJob.self, for: .attachmentUpload)
|
||||
JobRunner.add(executor: GroupLeavingJob.self, for: .groupLeaving)
|
||||
JobRunner.add(executor: AttachmentDownloadJob.self, for: .attachmentDownload)
|
||||
JobRunner.add(executor: ConfigurationSyncJob.self, for: .configurationSync)
|
||||
}
|
||||
|
|
|
@ -98,6 +98,12 @@ public extension ClosedGroup {
|
|||
// MARK: - Convenience
|
||||
|
||||
public extension ClosedGroup {
|
||||
enum LeaveType {
|
||||
case standard
|
||||
case silent
|
||||
case forced
|
||||
}
|
||||
|
||||
static func removeKeysAndUnsubscribe(
|
||||
_ db: Database? = nil,
|
||||
threadId: String,
|
||||
|
|
|
@ -163,7 +163,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
|
||||
|
||||
|
@ -91,7 +93,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
|
||||
|
@ -101,6 +104,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 {
|
||||
|
@ -110,7 +132,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
|
||||
|
@ -898,6 +921,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())
|
||||
|
||||
|
|
|
@ -290,14 +290,14 @@ public extension SessionThread {
|
|||
_ db: Database,
|
||||
threadId: String,
|
||||
threadVariant: Variant,
|
||||
shouldSendLeaveMessageForGroups: Bool,
|
||||
groupLeaveType: ClosedGroup.LeaveType,
|
||||
calledFromConfigHandling: Bool
|
||||
) throws {
|
||||
try deleteOrLeave(
|
||||
db,
|
||||
threadIds: [threadId],
|
||||
threadVariant: threadVariant,
|
||||
shouldSendLeaveMessageForGroups: shouldSendLeaveMessageForGroups,
|
||||
groupLeaveType: groupLeaveType,
|
||||
calledFromConfigHandling: calledFromConfigHandling
|
||||
)
|
||||
}
|
||||
|
@ -306,14 +306,14 @@ public extension SessionThread {
|
|||
_ db: Database,
|
||||
threadIds: [String],
|
||||
threadVariant: Variant,
|
||||
shouldSendLeaveMessageForGroups: Bool,
|
||||
groupLeaveType: ClosedGroup.LeaveType,
|
||||
calledFromConfigHandling: Bool
|
||||
) throws {
|
||||
let currentUserPublicKey: String = getUserHexEncodedPublicKey(db)
|
||||
let remainingThreadIds: [String] = threadIds.filter { $0 != currentUserPublicKey }
|
||||
|
||||
switch threadVariant {
|
||||
case .contact:
|
||||
switch (threadVariant, groupLeaveType) {
|
||||
case (.contact, _):
|
||||
// We need to custom handle the 'Note to Self' conversation (it should just be
|
||||
// hidden rather than deleted
|
||||
if threadIds.contains(currentUserPublicKey) {
|
||||
|
@ -337,24 +337,29 @@ public extension SessionThread {
|
|||
.hide(db, contactIds: threadIds)
|
||||
}
|
||||
|
||||
case .legacyGroup, .group:
|
||||
if shouldSendLeaveMessageForGroups {
|
||||
threadIds.forEach { threadId in
|
||||
MessageSender
|
||||
.leave(db, groupPublicKey: threadId)
|
||||
.sinkUntilComplete()
|
||||
}
|
||||
}
|
||||
else {
|
||||
try ClosedGroup.removeKeysAndUnsubscribe(
|
||||
db,
|
||||
threadIds: threadIds,
|
||||
removeGroupData: true,
|
||||
calledFromConfigHandling: calledFromConfigHandling
|
||||
)
|
||||
_ = try SessionThread
|
||||
.filter(ids: remainingThreadIds)
|
||||
.deleteAll(db)
|
||||
|
||||
case (.legacyGroup, .standard), (.group, .standard):
|
||||
try threadIds.forEach { threadId in
|
||||
try MessageSender
|
||||
.leave(
|
||||
db,
|
||||
groupPublicKey: threadId,
|
||||
deleteThread: true
|
||||
)
|
||||
}
|
||||
|
||||
case .community:
|
||||
case (.legacyGroup, .silent), (.legacyGroup, .forced), (.group, .forced), (.group, .silent):
|
||||
try ClosedGroup.removeKeysAndUnsubscribe(
|
||||
db,
|
||||
threadIds: threadIds,
|
||||
removeGroupData: true,
|
||||
calledFromConfigHandling: calledFromConfigHandling
|
||||
)
|
||||
|
||||
case (.community, _):
|
||||
threadIds.forEach { threadId in
|
||||
OpenGroupManager.shared.delete(
|
||||
db,
|
||||
|
@ -363,10 +368,6 @@ public extension SessionThread {
|
|||
)
|
||||
}
|
||||
}
|
||||
|
||||
_ = try SessionThread
|
||||
.filter(ids: remainingThreadIds)
|
||||
.deleteAll(db)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -412,7 +413,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 ?
|
||||
|
@ -427,7 +428,7 @@ public extension SessionThread {
|
|||
\(SQL("\(thread[.id]) != \(userPublicKey)")) AND
|
||||
IFNULL(\(contact[.isApproved]), false) = false
|
||||
"""
|
||||
)
|
||||
).sqlExpression
|
||||
}
|
||||
|
||||
func isNoteToSelf(_ db: Database? = nil) -> Bool {
|
||||
|
@ -503,42 +504,46 @@ public extension SessionThread {
|
|||
}
|
||||
|
||||
static func getUserHexEncodedBlindedKey(
|
||||
_ db: Database? = nil,
|
||||
threadId: String,
|
||||
threadVariant: Variant
|
||||
) -> String? {
|
||||
guard threadVariant == .community 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 == .community,
|
||||
let blindingInfo: (edkeyPair: 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: KeyPair = blindingInfo.edkeyPair,
|
||||
let publicKey: String = blindingInfo.publicKey,
|
||||
blindingInfo.capabilities.isEmpty || blindingInfo.capabilities.contains(.blind)
|
||||
let userEdKeyPair: 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: 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,158 @@
|
|||
// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved.
|
||||
|
||||
import Foundation
|
||||
import Combine
|
||||
import GRDB
|
||||
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 (Job, Bool) -> (),
|
||||
failure: @escaping (Job, Error?, Bool) -> (),
|
||||
deferred: @escaping (Job) -> ())
|
||||
{
|
||||
guard
|
||||
let detailsData: Data = job.details,
|
||||
let details: Details = try? JSONDecoder().decode(Details.self, from: detailsData),
|
||||
let threadId: String = job.threadId,
|
||||
let interactionId: Int64 = job.interactionId
|
||||
else {
|
||||
failure(job, JobRunnerError.missingRequiredDetails, true)
|
||||
return
|
||||
}
|
||||
|
||||
let destination: Message.Destination = .closedGroup(groupPublicKey: threadId)
|
||||
|
||||
Storage.shared
|
||||
.writePublisher(receiveOn: queue) { db in
|
||||
guard (try? SessionThread.exists(db, id: threadId)) == true else {
|
||||
SNLog("Can't update nonexistent closed group.")
|
||||
throw MessageSenderError.noThread
|
||||
}
|
||||
guard (try? ClosedGroup.exists(db, id: threadId)) == true else {
|
||||
throw MessageSenderError.invalidClosedGroupUpdate
|
||||
}
|
||||
|
||||
return try MessageSender.preparedSendData(
|
||||
db,
|
||||
message: ClosedGroupControlMessage(
|
||||
kind: .memberLeft
|
||||
),
|
||||
to: destination,
|
||||
namespace: destination.defaultNamespace,
|
||||
interactionId: job.interactionId,
|
||||
isSyncMessage: false
|
||||
)
|
||||
}
|
||||
.flatMap { MessageSender.sendImmediate(preparedSendData: $0) }
|
||||
.receive(on: queue)
|
||||
.sinkUntilComplete(
|
||||
receiveCompletion: { result in
|
||||
switch result {
|
||||
case .failure:
|
||||
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)
|
||||
|
||||
case .finished:
|
||||
Storage.shared.writeAsync { db in
|
||||
// Update the group (if the admin leaves the group is disbanded)
|
||||
let currentUserPublicKey: String = getUserHexEncodedPublicKey(db)
|
||||
let wasAdminUser: Bool = GroupMember
|
||||
.filter(GroupMember.Columns.groupId == threadId)
|
||||
.filter(GroupMember.Columns.profileId == currentUserPublicKey)
|
||||
.filter(GroupMember.Columns.role == GroupMember.Role.admin)
|
||||
.isNotEmpty(db)
|
||||
|
||||
if wasAdminUser {
|
||||
try GroupMember
|
||||
.filter(GroupMember.Columns.groupId == threadId)
|
||||
.deleteAll(db)
|
||||
}
|
||||
else {
|
||||
try GroupMember
|
||||
.filter(GroupMember.Columns.groupId == threadId)
|
||||
.filter(GroupMember.Columns.profileId == currentUserPublicKey)
|
||||
.deleteAll(db)
|
||||
}
|
||||
|
||||
// Update the transaction
|
||||
try Interaction
|
||||
.filter(id: interactionId)
|
||||
.updateAll(
|
||||
db,
|
||||
[
|
||||
Interaction.Columns.variant
|
||||
.set(to: Interaction.Variant.infoClosedGroupCurrentUserLeft),
|
||||
Interaction.Columns.body.set(to: "GROUP_YOU_LEFT".localized())
|
||||
]
|
||||
)
|
||||
|
||||
// Clear out the group info as needed
|
||||
try ClosedGroup.removeKeysAndUnsubscribe(
|
||||
db,
|
||||
threadId: threadId,
|
||||
removeGroupData: details.deleteThread,
|
||||
calledFromConfigHandling: false
|
||||
)
|
||||
}
|
||||
|
||||
success(job, false)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - GroupLeavingJob.Details
|
||||
|
||||
extension GroupLeavingJob {
|
||||
public struct Details: Codable {
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case deleteThread
|
||||
}
|
||||
|
||||
public let deleteThread: Bool
|
||||
|
||||
// MARK: - Initialization
|
||||
|
||||
public init(deleteThread: Bool) {
|
||||
self.deleteThread = deleteThread
|
||||
}
|
||||
|
||||
// MARK: - Codable
|
||||
|
||||
public init(from decoder: Decoder) throws {
|
||||
let container: KeyedDecodingContainer<CodingKeys> = try decoder.container(keyedBy: CodingKeys.self)
|
||||
|
||||
self = Details(
|
||||
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(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
|
||||
}
|
||||
|
||||
|
|
|
@ -175,7 +175,7 @@ internal extension SessionUtil {
|
|||
db,
|
||||
threadId: contact.id,
|
||||
threadVariant: .contact,
|
||||
shouldSendLeaveMessageForGroups: false,
|
||||
groupLeaveType: .forced,
|
||||
calledFromConfigHandling: true
|
||||
)
|
||||
|
||||
|
@ -239,7 +239,7 @@ internal extension SessionUtil {
|
|||
db,
|
||||
threadIds: contactIdsToRemove,
|
||||
threadVariant: .contact,
|
||||
shouldSendLeaveMessageForGroups: false,
|
||||
groupLeaveType: .forced,
|
||||
calledFromConfigHandling: true
|
||||
)
|
||||
|
||||
|
|
|
@ -176,7 +176,7 @@ internal extension SessionUtil {
|
|||
db,
|
||||
threadIds: Array(communityIdsToRemove),
|
||||
threadVariant: .community,
|
||||
shouldSendLeaveMessageForGroups: false,
|
||||
groupLeaveType: .forced,
|
||||
calledFromConfigHandling: true
|
||||
)
|
||||
}
|
||||
|
@ -336,7 +336,7 @@ internal extension SessionUtil {
|
|||
db,
|
||||
threadIds: Array(legacyGroupIdsToRemove),
|
||||
threadVariant: .legacyGroup,
|
||||
shouldSendLeaveMessageForGroups: false,
|
||||
groupLeaveType: .forced,
|
||||
calledFromConfigHandling: true
|
||||
)
|
||||
}
|
||||
|
|
|
@ -108,7 +108,7 @@ internal extension SessionUtil {
|
|||
db,
|
||||
threadId: userPublicKey,
|
||||
threadVariant: .contact,
|
||||
shouldSendLeaveMessageForGroups: false,
|
||||
groupLeaveType: .forced,
|
||||
calledFromConfigHandling: true
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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: .community
|
||||
)
|
||||
|
|
|
@ -97,7 +97,7 @@ extension MessageReceiver {
|
|||
db,
|
||||
threadId: blindedIdLookup.blindedId,
|
||||
threadVariant: .contact,
|
||||
shouldSendLeaveMessageForGroups: false,
|
||||
groupLeaveType: .forced,
|
||||
calledFromConfigHandling: false
|
||||
)
|
||||
}
|
||||
|
|
|
@ -572,110 +572,31 @@ extension MessageSender {
|
|||
/// The returned promise is fulfilled when the `MEMBER_LEFT` message has been sent to the group.
|
||||
public static func leave(
|
||||
_ db: Database,
|
||||
groupPublicKey: String
|
||||
) -> AnyPublisher<Void, Error> {
|
||||
guard let thread: SessionThread = try? SessionThread.fetchOne(db, id: groupPublicKey) else {
|
||||
SNLog("Can't leave nonexistent closed group.")
|
||||
return Fail(error: MessageSenderError.noThread)
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
guard thread.closedGroup.isNotEmpty(db) else {
|
||||
return Fail(error: MessageSenderError.invalidClosedGroupUpdate)
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
groupPublicKey: String,
|
||||
deleteThread: Bool
|
||||
) throws {
|
||||
let userPublicKey: String = getUserHexEncodedPublicKey(db)
|
||||
let sendData: MessageSender.PreparedSendData
|
||||
|
||||
do {
|
||||
// Notify the user
|
||||
let interaction: Interaction = try Interaction(
|
||||
threadId: thread.id,
|
||||
authorId: userPublicKey,
|
||||
variant: .infoClosedGroupCurrentUserLeft,
|
||||
body: ClosedGroupControlMessage.Kind
|
||||
.memberLeft
|
||||
.infoMessage(db, sender: userPublicKey),
|
||||
timestampMs: SnodeAPI.currentOffsetTimestampMs()
|
||||
).inserted(db)
|
||||
|
||||
guard let interactionId: Int64 = interaction.id else {
|
||||
return Fail(error: StorageError.objectNotSaved)
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
// Send the update to the group
|
||||
sendData = try MessageSender
|
||||
.preparedSendData(
|
||||
db,
|
||||
message: ClosedGroupControlMessage(
|
||||
kind: .memberLeft
|
||||
),
|
||||
to: try Message.Destination.from(db, threadId: groupPublicKey, threadVariant: .legacyGroup),
|
||||
namespace: try Message.Destination
|
||||
.from(db, threadId: groupPublicKey, threadVariant: .legacyGroup)
|
||||
.defaultNamespace,
|
||||
interactionId: interactionId
|
||||
// Notify the user
|
||||
let interaction: Interaction = try Interaction(
|
||||
threadId: groupPublicKey,
|
||||
authorId: userPublicKey,
|
||||
variant: .infoClosedGroupCurrentUserLeaving,
|
||||
body: "group_you_leaving".localized(),
|
||||
timestampMs: SnodeAPI.currentOffsetTimestampMs()
|
||||
).inserted(db)
|
||||
|
||||
JobRunner.upsert(
|
||||
db,
|
||||
job: Job(
|
||||
variant: .groupLeaving,
|
||||
threadId: groupPublicKey,
|
||||
interactionId: interaction.id,
|
||||
details: GroupLeavingJob.Details(
|
||||
deleteThread: deleteThread
|
||||
)
|
||||
|
||||
// Update the group (if the admin leaves the group is disbanded)
|
||||
let wasAdminUser: Bool = 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)
|
||||
}
|
||||
}
|
||||
catch {
|
||||
switch error {
|
||||
// There are some cases where the keys for a ClosedGroup can be lost or become invalid, in
|
||||
// those cases we don't want to prevent the user from being able to leave a group so catch
|
||||
// them and just remove the group from the users devices
|
||||
case MessageSenderError.noKeyPair, MessageSenderError.encryptionFailed:
|
||||
try? ClosedGroup.removeKeysAndUnsubscribe(
|
||||
db,
|
||||
threadId: groupPublicKey,
|
||||
removeGroupData: false,
|
||||
calledFromConfigHandling: false
|
||||
)
|
||||
return Just(())
|
||||
.setFailureType(to: Error.self)
|
||||
.eraseToAnyPublisher()
|
||||
|
||||
default: break
|
||||
}
|
||||
|
||||
return Fail(error: error)
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
return MessageSender
|
||||
.sendImmediate(preparedSendData: sendData)
|
||||
.handleEvents(
|
||||
receiveCompletion: { result in
|
||||
switch result {
|
||||
case .failure: break
|
||||
case .finished:
|
||||
try? ClosedGroup.removeKeysAndUnsubscribe(
|
||||
threadId: groupPublicKey,
|
||||
removeGroupData: false,
|
||||
calledFromConfigHandling: false
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
.eraseToAnyPublisher()
|
||||
)
|
||||
}
|
||||
|
||||
public static func sendLatestEncryptionKeyPair(
|
||||
|
|
|
@ -103,8 +103,8 @@ public struct SessionThreadViewModel: FetchableRecordWithRowId, Decodable, Equat
|
|||
public var canWrite: Bool {
|
||||
switch threadVariant {
|
||||
case .contact: return true
|
||||
case .legacyGroup, .group: return currentUserIsClosedGroupMember == true
|
||||
case .community: return openGroupPermissions?.contains(.write) ?? false
|
||||
case .legacyGroup, .group: return (currentUserIsClosedGroupMember == true && interactionVariant?.isGroupLeavingStatus != true)
|
||||
case .community: return (openGroupPermissions?.contains(.write) ?? false)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -333,10 +333,11 @@ public struct SessionThreadViewModel: FetchableRecordWithRowId, Decodable, Equat
|
|||
|
||||
public extension SessionThreadViewModel {
|
||||
static let invalidId: String = "INVALID_THREAD_ID"
|
||||
static let messageRequestsSectionId: String = "MESSAGE_REQUESTS_SECTION_INVALID_THREAD_ID"
|
||||
|
||||
// Note: This init method is only used system-created cells or empty states
|
||||
init(
|
||||
threadId: String? = nil,
|
||||
threadId: String,
|
||||
threadVariant: SessionThread.Variant? = nil,
|
||||
threadIsNoteToSelf: Bool = false,
|
||||
threadIsBlocked: Bool? = nil,
|
||||
|
@ -346,7 +347,7 @@ public extension SessionThreadViewModel {
|
|||
unreadCount: UInt = 0
|
||||
) {
|
||||
self.rowId = -1
|
||||
self.threadId = (threadId ?? SessionThreadViewModel.invalidId)
|
||||
self.threadId = threadId
|
||||
self.threadVariant = (threadVariant ?? .contact)
|
||||
self.threadCreationDateTimestamp = 0
|
||||
self.threadMemberNames = nil
|
||||
|
@ -464,6 +465,7 @@ public extension SessionThreadViewModel {
|
|||
}
|
||||
|
||||
func populatingCurrentUserBlindedKey(
|
||||
_ db: Database? = nil,
|
||||
currentUserBlindedPublicKeyForThisThread: String? = nil
|
||||
) -> SessionThreadViewModel {
|
||||
return SessionThreadViewModel(
|
||||
|
@ -516,6 +518,7 @@ public extension SessionThreadViewModel {
|
|||
currentUserBlindedPublicKey: (
|
||||
currentUserBlindedPublicKeyForThisThread ??
|
||||
SessionThread.getUserHexEncodedBlindedKey(
|
||||
db,
|
||||
threadId: self.threadId,
|
||||
threadVariant: self.threadVariant
|
||||
)
|
||||
|
@ -562,14 +565,17 @@ 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
|
||||
/// parse and might throw
|
||||
///
|
||||
/// Explicitly set default values for the fields ignored for search results
|
||||
let numColumnsBeforeProfiles: Int = 13
|
||||
let numColumnsBetweenProfilesAndAttachmentInfo: Int = 11 // The attachment info columns will be combined
|
||||
let numColumnsBeforeProfiles: Int = 14
|
||||
let numColumnsBetweenProfilesAndAttachmentInfo: Int = 12 // The attachment info columns will be combined
|
||||
|
||||
let request: SQLRequest<ViewModel> = """
|
||||
SELECT
|
||||
|
@ -583,6 +589,11 @@ public extension SessionThreadViewModel {
|
|||
\(contact[.isBlocked]) AS \(ViewModel.threadIsBlockedKey),
|
||||
\(thread[.mutedUntilTimestamp]) AS \(ViewModel.threadMutedUntilTimestampKey),
|
||||
\(thread[.onlyNotifyForMentions]) AS \(ViewModel.threadOnlyNotifyForMentionsKey),
|
||||
(
|
||||
\(SQL("\(thread[.variant]) = \(SessionThread.Variant.contact)")) AND
|
||||
\(SQL("\(thread[.id]) != \(userPublicKey)")) AND
|
||||
IFNULL(\(contact[.isApproved]), false) = false
|
||||
) AS \(ViewModel.threadIsMessageRequestKey),
|
||||
|
||||
(\(typingIndicator[.threadId]) IS NOT NULL) AS \(ViewModel.threadContactIsTypingKey),
|
||||
\(thread[.markedAsUnread]) AS \(ViewModel.threadWasMarkedUnreadKey),
|
||||
|
@ -594,7 +605,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),
|
||||
|
||||
|
@ -669,10 +681,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 (
|
||||
|
|
|
@ -28,7 +28,7 @@ public extension UIContextualAction {
|
|||
title: String? = nil,
|
||||
icon: UIImage? = nil,
|
||||
iconHeight: CGFloat = Values.mediumFontSize,
|
||||
themeTintColor: ThemeValue = .textPrimary,
|
||||
themeTintColor: ThemeValue = .white,
|
||||
themeBackgroundColor: ThemeValue,
|
||||
side: Side,
|
||||
actionIndex: Int,
|
||||
|
@ -66,7 +66,7 @@ public extension UIContextualAction {
|
|||
let stackView: UIStackView = UIStackView()
|
||||
stackView.axis = .vertical
|
||||
stackView.alignment = .center
|
||||
stackView.spacing = 3
|
||||
stackView.spacing = 4
|
||||
|
||||
if let icon: UIImage = icon {
|
||||
let aspectRatio: CGFloat = (icon.size.width / icon.size.height)
|
||||
|
|
|
@ -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(
|
||||
|
@ -103,6 +103,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
|
||||
|
||||
/// This is a job that runs once whenever the user config or a closed group config changes, it retrieves the
|
||||
/// state of all config objects and syncs any that are flagged as needing to be synced
|
||||
|
|
|
@ -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)) ?? [:])
|
||||
}
|
||||
|
|
|
@ -66,6 +66,7 @@ public final class JobRunner {
|
|||
jobVariants.remove(.messageSend),
|
||||
jobVariants.remove(.notifyPushServer),
|
||||
jobVariants.remove(.sendReadReceipts),
|
||||
jobVariants.remove(.groupLeaving),
|
||||
jobVariants.remove(.configurationSync)
|
||||
].compactMap { $0 }
|
||||
)
|
||||
|
@ -635,9 +636,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) {
|
||||
|
@ -772,13 +777,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, []))
|
||||
|
||||
|
@ -790,39 +797,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
|
||||
/// 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)
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
|
||||
handleJobDeferred(nextJob)
|
||||
return
|
||||
}
|
||||
|
@ -940,6 +923,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
|
||||
|
@ -1002,26 +991,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)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1081,19 +1061,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
|
||||
|
@ -1101,13 +1092,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
|
||||
}
|
||||
|
@ -1116,7 +1100,7 @@ private final class JobQueue {
|
|||
|
||||
_ = try job
|
||||
.with(
|
||||
failureCount: (job.failureCount + 1),
|
||||
failureCount: updatedFailureCount,
|
||||
nextRunTimestamp: nextRunTimestamp
|
||||
)
|
||||
.saved(db)
|
||||
|
@ -1127,22 +1111,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)
|
||||
|
|
|
@ -46,8 +46,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…
Reference in New Issue