Merge branch 'ipad-support-1' into voice-calls-2
This commit is contained in:
commit
52407aec03
|
@ -64,7 +64,6 @@
|
|||
34F308A21ECB469700BB7697 /* OWSBezierPathView.m in Sources */ = {isa = PBXBuildFile; fileRef = 34F308A11ECB469700BB7697 /* OWSBezierPathView.m */; };
|
||||
4503F1BE20470A5B00CEE724 /* classic-quiet.aifc in Resources */ = {isa = PBXBuildFile; fileRef = 4503F1BB20470A5B00CEE724 /* classic-quiet.aifc */; };
|
||||
4503F1BF20470A5B00CEE724 /* classic.aifc in Resources */ = {isa = PBXBuildFile; fileRef = 4503F1BC20470A5B00CEE724 /* classic.aifc */; };
|
||||
450DF2051E0D74AC003D14BE /* Platform.swift in Sources */ = {isa = PBXBuildFile; fileRef = 450DF2041E0D74AC003D14BE /* Platform.swift */; };
|
||||
450DF2091E0DD2C6003D14BE /* UserNotificationsAdaptee.swift in Sources */ = {isa = PBXBuildFile; fileRef = 450DF2081E0DD2C6003D14BE /* UserNotificationsAdaptee.swift */; };
|
||||
451166C01FD86B98000739BA /* AccountManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 451166BF1FD86B98000739BA /* AccountManager.swift */; };
|
||||
451A13B11E13DED2000A50FD /* AppNotifications.swift in Sources */ = {isa = PBXBuildFile; fileRef = 451A13B01E13DED2000A50FD /* AppNotifications.swift */; };
|
||||
|
@ -150,6 +149,12 @@
|
|||
7B7CB18E270D066F0079FF93 /* IncomingCallBanner.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B7CB18D270D066F0079FF93 /* IncomingCallBanner.swift */; };
|
||||
7B7CB190270FB2150079FF93 /* MiniCallView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B7CB18F270FB2150079FF93 /* MiniCallView.swift */; };
|
||||
7B7CB192271508AD0079FF93 /* CallRingTonePlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B7CB191271508AD0079FF93 /* CallRingTonePlayer.swift */; };
|
||||
7B93D06A27CF173D00811CB6 /* MessageRequestsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B93D06927CF173D00811CB6 /* MessageRequestsViewController.swift */; };
|
||||
7B93D06D27CF175800811CB6 /* MessageRequestsCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B93D06C27CF175800811CB6 /* MessageRequestsCell.swift */; };
|
||||
7B93D07027CF194000811CB6 /* ConfigurationMessage+Convenience.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B93D06E27CF194000811CB6 /* ConfigurationMessage+Convenience.swift */; };
|
||||
7B93D07127CF194000811CB6 /* MessageRequestResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B93D06F27CF194000811CB6 /* MessageRequestResponse.swift */; };
|
||||
7B93D07327CF19C800811CB6 /* MessageRequestsMigration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B93D07227CF19C800811CB6 /* MessageRequestsMigration.swift */; };
|
||||
7B93D07727CF1A8A00811CB6 /* MockDataGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B93D07527CF1A8900811CB6 /* MockDataGenerator.swift */; };
|
||||
7BA68909272A27BE00EFC32F /* SessionCall.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BA68908272A27BE00EFC32F /* SessionCall.swift */; };
|
||||
7BA6890D27325CCC00EFC32F /* SessionCallManager+CXCallController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BA6890C27325CCC00EFC32F /* SessionCallManager+CXCallController.swift */; };
|
||||
7BA6890F27325CE300EFC32F /* SessionCallManager+CXProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BA6890E27325CE300EFC32F /* SessionCallManager+CXProvider.swift */; };
|
||||
|
@ -568,8 +573,6 @@
|
|||
C38EF00C255B61CC007E1867 /* SignalUtilitiesKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C33FD9AB255A548A00E217F9 /* SignalUtilitiesKit.framework */; };
|
||||
C38EF216255B6D3B007E1867 /* Theme.h in Headers */ = {isa = PBXBuildFile; fileRef = C38EF212255B6D3A007E1867 /* Theme.h */; settings = {ATTRIBUTES = (Public, ); }; };
|
||||
C38EF218255B6D3B007E1867 /* Theme.m in Sources */ = {isa = PBXBuildFile; fileRef = C38EF214255B6D3A007E1867 /* Theme.m */; };
|
||||
C38EF228255B6D5D007E1867 /* AttachmentSharing.m in Sources */ = {isa = PBXBuildFile; fileRef = C38EF223255B6D5D007E1867 /* AttachmentSharing.m */; };
|
||||
C38EF22A255B6D5D007E1867 /* AttachmentSharing.h in Headers */ = {isa = PBXBuildFile; fileRef = C38EF225255B6D5D007E1867 /* AttachmentSharing.h */; settings = {ATTRIBUTES = (Public, ); }; };
|
||||
C38EF22B255B6D5D007E1867 /* ShareViewDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = C38EF226255B6D5D007E1867 /* ShareViewDelegate.swift */; };
|
||||
C38EF22C255B6D5D007E1867 /* OWSVideoPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = C38EF227255B6D5D007E1867 /* OWSVideoPlayer.swift */; };
|
||||
C38EF243255B6D67007E1867 /* UIViewController+OWS.h in Headers */ = {isa = PBXBuildFile; fileRef = C38EF236255B6D65007E1867 /* UIViewController+OWS.h */; settings = {ATTRIBUTES = (Public, ); }; };
|
||||
|
@ -1059,7 +1062,6 @@
|
|||
4503F1BB20470A5B00CEE724 /* classic-quiet.aifc */ = {isa = PBXFileReference; lastKnownFileType = file; path = "classic-quiet.aifc"; sourceTree = "<group>"; };
|
||||
4503F1BC20470A5B00CEE724 /* classic.aifc */ = {isa = PBXFileReference; lastKnownFileType = file; path = classic.aifc; sourceTree = "<group>"; };
|
||||
4509E7991DD653700025A59F /* WebRTC.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WebRTC.framework; path = ThirdParty/WebRTC/Build/WebRTC.framework; sourceTree = "<group>"; };
|
||||
450DF2041E0D74AC003D14BE /* Platform.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Platform.swift; sourceTree = "<group>"; };
|
||||
450DF2081E0DD2C6003D14BE /* UserNotificationsAdaptee.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = UserNotificationsAdaptee.swift; sourceTree = "<group>"; xcLanguageSpecificationIdentifier = xcode.lang.swift; };
|
||||
451166BF1FD86B98000739BA /* AccountManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AccountManager.swift; sourceTree = "<group>"; };
|
||||
451A13B01E13DED2000A50FD /* AppNotifications.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = AppNotifications.swift; sourceTree = "<group>"; xcLanguageSpecificationIdentifier = xcode.lang.swift; };
|
||||
|
@ -1160,6 +1162,12 @@
|
|||
7B7CB18D270D066F0079FF93 /* IncomingCallBanner.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IncomingCallBanner.swift; sourceTree = "<group>"; };
|
||||
7B7CB18F270FB2150079FF93 /* MiniCallView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MiniCallView.swift; sourceTree = "<group>"; };
|
||||
7B7CB191271508AD0079FF93 /* CallRingTonePlayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallRingTonePlayer.swift; sourceTree = "<group>"; };
|
||||
7B93D06927CF173D00811CB6 /* MessageRequestsViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageRequestsViewController.swift; sourceTree = "<group>"; };
|
||||
7B93D06C27CF175800811CB6 /* MessageRequestsCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageRequestsCell.swift; sourceTree = "<group>"; };
|
||||
7B93D06E27CF194000811CB6 /* ConfigurationMessage+Convenience.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "ConfigurationMessage+Convenience.swift"; sourceTree = "<group>"; };
|
||||
7B93D06F27CF194000811CB6 /* MessageRequestResponse.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageRequestResponse.swift; sourceTree = "<group>"; };
|
||||
7B93D07227CF19C800811CB6 /* MessageRequestsMigration.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageRequestsMigration.swift; sourceTree = "<group>"; };
|
||||
7B93D07527CF1A8900811CB6 /* MockDataGenerator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MockDataGenerator.swift; sourceTree = "<group>"; };
|
||||
7BA68908272A27BE00EFC32F /* SessionCall.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionCall.swift; sourceTree = "<group>"; };
|
||||
7BA6890C27325CCC00EFC32F /* SessionCallManager+CXCallController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SessionCallManager+CXCallController.swift"; sourceTree = "<group>"; };
|
||||
7BA6890E27325CE300EFC32F /* SessionCallManager+CXProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SessionCallManager+CXProvider.swift"; sourceTree = "<group>"; };
|
||||
|
@ -1599,9 +1607,7 @@
|
|||
C38EEF09255B49A8007E1867 /* SNProtoEnvelope+Conversion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SNProtoEnvelope+Conversion.swift"; sourceTree = "<group>"; };
|
||||
C38EF212255B6D3A007E1867 /* Theme.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = Theme.h; path = SignalUtilitiesKit/Utilities/Theme.h; sourceTree = SOURCE_ROOT; };
|
||||
C38EF214255B6D3A007E1867 /* Theme.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = Theme.m; path = SignalUtilitiesKit/Utilities/Theme.m; sourceTree = SOURCE_ROOT; };
|
||||
C38EF223255B6D5D007E1867 /* AttachmentSharing.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = AttachmentSharing.m; path = "SignalUtilitiesKit/Media Viewing & Editing/AttachmentSharing.m"; sourceTree = SOURCE_ROOT; };
|
||||
C38EF224255B6D5D007E1867 /* SignalAttachment.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = SignalAttachment.swift; path = "SessionMessagingKit/Sending & Receiving/Attachments/SignalAttachment.swift"; sourceTree = SOURCE_ROOT; };
|
||||
C38EF225255B6D5D007E1867 /* AttachmentSharing.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = AttachmentSharing.h; path = "SignalUtilitiesKit/Media Viewing & Editing/AttachmentSharing.h"; sourceTree = SOURCE_ROOT; };
|
||||
C38EF226255B6D5D007E1867 /* ShareViewDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = ShareViewDelegate.swift; path = SignalUtilitiesKit/Utilities/ShareViewDelegate.swift; sourceTree = SOURCE_ROOT; };
|
||||
C38EF227255B6D5D007E1867 /* OWSVideoPlayer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = OWSVideoPlayer.swift; path = "SignalUtilitiesKit/Media Viewing & Editing/OWSVideoPlayer.swift"; sourceTree = SOURCE_ROOT; };
|
||||
C38EF236255B6D65007E1867 /* UIViewController+OWS.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = "UIViewController+OWS.h"; path = "SignalUtilitiesKit/Utilities/UIViewController+OWS.h"; sourceTree = SOURCE_ROOT; };
|
||||
|
@ -2071,6 +2077,7 @@
|
|||
76EB03C118170B33006006FC /* Utilities */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
7B93D07527CF1A8900811CB6 /* MockDataGenerator.swift */,
|
||||
451166BF1FD86B98000739BA /* AccountManager.swift */,
|
||||
4C090A1A210FD9C7001FD7F9 /* HapticFeedback.swift */,
|
||||
34D5CCA71EAE3D30005515DB /* AvatarViewHelper.h */,
|
||||
|
@ -2088,7 +2095,6 @@
|
|||
45B5360D206DD8BB00D61655 /* UIResponder+OWS.swift */,
|
||||
4C586924224FAB83003FD070 /* AVAudioSession+OWS.h */,
|
||||
4C586925224FAB83003FD070 /* AVAudioSession+OWS.m */,
|
||||
450DF2041E0D74AC003D14BE /* Platform.swift */,
|
||||
4521C3BF1F59F3BA00B4C582 /* TextFieldHelper.swift */,
|
||||
34D1F0BF1F8EC1760066283D /* MessageRecipientStatusUtils.swift */,
|
||||
B8544E3223D50E4900299F14 /* SNAppearance.swift */,
|
||||
|
@ -2121,6 +2127,22 @@
|
|||
path = "Views & Modals";
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
7B93D06827CF173D00811CB6 /* Message Requests */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
7B93D06927CF173D00811CB6 /* MessageRequestsViewController.swift */,
|
||||
);
|
||||
path = "Message Requests";
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
7B93D06B27CF175800811CB6 /* Views */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
7B93D06C27CF175800811CB6 /* MessageRequestsCell.swift */,
|
||||
);
|
||||
path = Views;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
7BA68907272A279900EFC32F /* Call Management */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
|
@ -2557,6 +2579,8 @@
|
|||
C300A5C72554B03900555489 /* Control Messages */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
7B93D06E27CF194000811CB6 /* ConfigurationMessage+Convenience.swift */,
|
||||
7B93D06F27CF194000811CB6 /* MessageRequestResponse.swift */,
|
||||
C3C2A7702553A41E00C340D1 /* ControlMessage.swift */,
|
||||
B8DE1FB526C22FCB0079C9CE /* CallMessage.swift */,
|
||||
C300A5BC2554B00D00555489 /* ReadReceipt.swift */,
|
||||
|
@ -2934,6 +2958,8 @@
|
|||
C360968E25AD16E8008B62B2 /* Home */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
7B93D06B27CF175800811CB6 /* Views */,
|
||||
7B93D06827CF173D00811CB6 /* Message Requests */,
|
||||
7BAF54CA27ACCEEC003D12F8 /* GlobalSearch */,
|
||||
B8BB82A4238F627000BA5194 /* HomeVC.swift */,
|
||||
B83F2B85240C7B8F000A54AB /* NewConversationButtonSet.swift */,
|
||||
|
@ -3078,8 +3104,6 @@
|
|||
children = (
|
||||
C379DCEA2567334F0002D4EB /* Attachment Approval */,
|
||||
C379DCE9256733390002D4EB /* Image Editing */,
|
||||
C38EF225255B6D5D007E1867 /* AttachmentSharing.h */,
|
||||
C38EF223255B6D5D007E1867 /* AttachmentSharing.m */,
|
||||
C38EF358255B6DCC007E1867 /* MediaMessageView.swift */,
|
||||
C38EF357255B6DCC007E1867 /* MessageApprovalViewController.swift */,
|
||||
C38EF227255B6D5D007E1867 /* OWSVideoPlayer.swift */,
|
||||
|
@ -3149,6 +3173,7 @@
|
|||
C379DCE82567330E0002D4EB /* Migrations */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
7B93D07227CF19C800811CB6 /* MessageRequestsMigration.swift */,
|
||||
B8B32044258C117C0020074B /* ContactsMigration.swift */,
|
||||
C38EF271255B6D79007E1867 /* OWSDatabaseMigration.h */,
|
||||
C38EF270255B6D79007E1867 /* OWSDatabaseMigration.m */,
|
||||
|
@ -3749,7 +3774,6 @@
|
|||
C38EF24C255B6D67007E1867 /* NSAttributedString+OWS.h in Headers */,
|
||||
C38EF32B255B6DBF007E1867 /* OWSFormat.h in Headers */,
|
||||
C33FDC2C255A581F00E217F9 /* OWSFailedAttachmentDownloadsJob.h in Headers */,
|
||||
C38EF22A255B6D5D007E1867 /* AttachmentSharing.h in Headers */,
|
||||
C33FDDB8255A582000E217F9 /* NSSet+Functional.h in Headers */,
|
||||
C33FDDCC255A582000E217F9 /* TSConstants.h in Headers */,
|
||||
C33FDDBD255A582000E217F9 /* ByteParser.h in Headers */,
|
||||
|
@ -4646,7 +4670,6 @@
|
|||
C38EF3BF255B6DE7007E1867 /* ImageEditorView.swift in Sources */,
|
||||
C38EF365255B6DCC007E1867 /* OWSTableViewController.m in Sources */,
|
||||
C38EF36B255B6DCC007E1867 /* ScreenLockViewController.m in Sources */,
|
||||
C38EF228255B6D5D007E1867 /* AttachmentSharing.m in Sources */,
|
||||
C38EF40C255B6DF7007E1867 /* GradientView.swift in Sources */,
|
||||
C38EF35C255B6DCC007E1867 /* SelectThreadViewController.m in Sources */,
|
||||
C38EF30E255B6DBF007E1867 /* FullTextSearcher.swift in Sources */,
|
||||
|
@ -4674,6 +4697,7 @@
|
|||
C33FDC98255A582000E217F9 /* SwiftSingletons.swift in Sources */,
|
||||
C33FDC27255A581F00E217F9 /* YapDatabase+Promise.swift in Sources */,
|
||||
C33FDCD3255A582000E217F9 /* GroupUtilities.swift in Sources */,
|
||||
7B93D07327CF19C800811CB6 /* MessageRequestsMigration.swift in Sources */,
|
||||
C38EF326255B6DBF007E1867 /* ConversationStyle.swift in Sources */,
|
||||
C38EF3B8255B6DE7007E1867 /* ImageEditorTextViewController.swift in Sources */,
|
||||
C33FDD91255A582000E217F9 /* OWSMessageUtils.m in Sources */,
|
||||
|
@ -4840,6 +4864,7 @@
|
|||
C3A3A171256E1D25004D228D /* SSKReachabilityManager.swift in Sources */,
|
||||
B8DE1FB626C22FCB0079C9CE /* CallMessage.swift in Sources */,
|
||||
C3A71D0B2558989C0043A11F /* MessageWrapper.swift in Sources */,
|
||||
7B93D07127CF194000811CB6 /* MessageRequestResponse.swift in Sources */,
|
||||
B8F5F60325EDE16F003BF8D4 /* DataExtractionNotification.swift in Sources */,
|
||||
C32C5D24256DD4C0003C73A2 /* MentionsManager.swift in Sources */,
|
||||
C3A71D1E25589AC30043A11F /* WebSocketProto.swift in Sources */,
|
||||
|
@ -4861,6 +4886,7 @@
|
|||
B8856CEE256F1054001CE70E /* OWSAudioPlayer.m in Sources */,
|
||||
C32C5EDC256DF501003C73A2 /* YapDatabaseConnection+OWS.m in Sources */,
|
||||
C3BBE0762554CDA60050F1E3 /* Configuration.swift in Sources */,
|
||||
7B93D07027CF194000811CB6 /* ConfigurationMessage+Convenience.swift in Sources */,
|
||||
C35D76DB26606304009AA5FB /* ThreadUpdateBatcher.swift in Sources */,
|
||||
B8B32033258B235D0020074B /* Storage+Contacts.swift in Sources */,
|
||||
B8856D69256F141F001CE70E /* OWSWindowManager.m in Sources */,
|
||||
|
@ -4997,7 +5023,6 @@
|
|||
34E3E5681EC4B19400495BAC /* AudioProgressView.swift in Sources */,
|
||||
B8D0A26925E4A2C200C1835E /* Onboarding.swift in Sources */,
|
||||
34D1F0521F7E8EA30066283D /* GiphyDownloader.swift in Sources */,
|
||||
450DF2051E0D74AC003D14BE /* Platform.swift in Sources */,
|
||||
4CC613362227A00400E21A3A /* ConversationSearch.swift in Sources */,
|
||||
B82149B825D60393009C0F2A /* BlockedModal.swift in Sources */,
|
||||
B82B408C239A068800A248E7 /* RegisterVC.swift in Sources */,
|
||||
|
@ -5087,6 +5112,7 @@
|
|||
76EB054018170B33006006FC /* AppDelegate.m in Sources */,
|
||||
340FC8B6204DAC8D007AEB0F /* OWSQRCodeScanningViewController.m in Sources */,
|
||||
7B0EFDF0275084AA00FFAAE7 /* CallMessageCell.swift in Sources */,
|
||||
7B93D06A27CF173D00811CB6 /* MessageRequestsViewController.swift in Sources */,
|
||||
C33100082558FF6D00070591 /* NewConversationButtonSet.swift in Sources */,
|
||||
C3AAFFF225AE99710089E6DD /* AppDelegate.swift in Sources */,
|
||||
B8BB82A5238F627000BA5194 /* HomeVC.swift in Sources */,
|
||||
|
@ -5105,6 +5131,8 @@
|
|||
B8D0A25025E3678700C1835E /* LinkDeviceVC.swift in Sources */,
|
||||
3496957321A301A100DCFE74 /* OWSBackupJob.m in Sources */,
|
||||
B894D0752339EDCF00B4D94D /* NukeDataModal.swift in Sources */,
|
||||
7B93D06D27CF175800811CB6 /* MessageRequestsCell.swift in Sources */,
|
||||
7B93D07727CF1A8A00811CB6 /* MockDataGenerator.swift in Sources */,
|
||||
7B4C75CD26BB92060000AC89 /* DeletedMessageView.swift in Sources */,
|
||||
B897621C25D201F7004F83B2 /* ScrollToBottomButton.swift in Sources */,
|
||||
346B66311F4E29B200E5122F /* CropScaleImageViewController.swift in Sources */,
|
||||
|
@ -6473,6 +6501,7 @@
|
|||
SWIFT_OBJC_INTERFACE_HEADER_NAME = "Session-Swift.h";
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
TEST_AFTER_BUILD = YES;
|
||||
WRAPPER_EXTENSION = app;
|
||||
};
|
||||
|
@ -6542,6 +6571,7 @@
|
|||
SWIFT_OBJC_BRIDGING_HEADER = "Session/Meta/Signal-Bridging-Header.h";
|
||||
SWIFT_OBJC_INTERFACE_HEADER_NAME = "Session-Swift.h";
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
TEST_AFTER_BUILD = YES;
|
||||
WRAPPER_EXTENSION = app;
|
||||
};
|
||||
|
|
|
@ -7,7 +7,6 @@
|
|||
#import "Session-Swift.h"
|
||||
|
||||
#import <PromiseKit/AnyPromise.h>
|
||||
#import <SignalUtilitiesKit/AttachmentSharing.h>
|
||||
#import <SessionMessagingKit/Environment.h>
|
||||
#import <SignalUtilitiesKit/SignalUtilitiesKit-Swift.h>
|
||||
#import <SignalUtilitiesKit/UIColor+OWS.h>
|
||||
|
|
|
@ -2,6 +2,7 @@ import UIKit
|
|||
import CoreServices
|
||||
import Photos
|
||||
import PhotosUI
|
||||
import PromiseKit
|
||||
import SessionUtilitiesKit
|
||||
import SignalUtilitiesKit
|
||||
|
||||
|
@ -10,6 +11,16 @@ extension ConversationVC : InputViewDelegate, MessageCellDelegate, ContextMenuAc
|
|||
ConversationTitleViewDelegate {
|
||||
|
||||
func handleTitleViewTapped() {
|
||||
// Don't take the user to settings for message requests
|
||||
guard
|
||||
let contactThread: TSContactThread = thread as? TSContactThread,
|
||||
let contact: Contact = Storage.shared.getContact(with: contactThread.contactSessionID()),
|
||||
contact.isApproved,
|
||||
contact.didApproveMe
|
||||
else {
|
||||
return
|
||||
}
|
||||
|
||||
openSettings()
|
||||
}
|
||||
|
||||
|
@ -53,6 +64,13 @@ extension ConversationVC : InputViewDelegate, MessageCellDelegate, ContextMenuAc
|
|||
UIView.animate(withDuration: 0.25, animations: {
|
||||
self.blockedBanner.alpha = 0
|
||||
}, completion: { _ in
|
||||
if let contact: Contact = Storage.shared.getContact(with: publicKey) {
|
||||
Storage.shared.write { transaction in
|
||||
contact.isBlocked = false
|
||||
Storage.shared.setContact(contact, using: transaction)
|
||||
}
|
||||
}
|
||||
|
||||
OWSBlockingManager.shared().removeBlockedPhoneNumber(publicKey)
|
||||
})
|
||||
}
|
||||
|
@ -174,7 +192,7 @@ extension ConversationVC : InputViewDelegate, MessageCellDelegate, ContextMenuAc
|
|||
} catch {
|
||||
let alert = UIAlertController(title: "Session", message: "An error occurred.", preferredStyle: .alert)
|
||||
alert.addAction(UIAlertAction(title: "OK", style: .default, handler: nil))
|
||||
return present(alert, animated: true, completion: nil)
|
||||
return presentAlert(alert)
|
||||
}
|
||||
let type = urlResourceValues.typeIdentifier ?? (kUTTypeData as String)
|
||||
guard urlResourceValues.isDirectory != true else {
|
||||
|
@ -234,9 +252,12 @@ extension ConversationVC : InputViewDelegate, MessageCellDelegate, ContextMenuAc
|
|||
|
||||
func sendMessage(hasPermissionToSendSeed: Bool = false) {
|
||||
guard !showBlockedModalIfNeeded() else { return }
|
||||
|
||||
let text = replaceMentions(in: snInputView.text.trimmingCharacters(in: .whitespacesAndNewlines))
|
||||
let thread = self.thread
|
||||
|
||||
guard !text.isEmpty else { return }
|
||||
|
||||
if text.contains(mnemonic) && !thread.isNoteToSelf() && !hasPermissionToSendSeed {
|
||||
// Warn the user if they're about to send their seed to someone
|
||||
let modal = SendSeedModal()
|
||||
|
@ -245,58 +266,123 @@ extension ConversationVC : InputViewDelegate, MessageCellDelegate, ContextMenuAc
|
|||
modal.proceed = { self.sendMessage(hasPermissionToSendSeed: true) }
|
||||
return present(modal, animated: true, completion: nil)
|
||||
}
|
||||
let message = VisibleMessage()
|
||||
message.sentTimestamp = NSDate.millisecondTimestamp()
|
||||
|
||||
let sentTimestamp: UInt64 = NSDate.millisecondTimestamp()
|
||||
let message: VisibleMessage = VisibleMessage()
|
||||
message.sentTimestamp = sentTimestamp
|
||||
message.text = text
|
||||
message.quote = VisibleMessage.Quote.from(snInputView.quoteDraftInfo?.model)
|
||||
|
||||
// Note: 'shouldBeVisible' is set to true the first time a thread is saved so we can
|
||||
// use it to determine if the user is creating a new thread and update the 'isApproved'
|
||||
// flags appropriately
|
||||
let oldThreadShouldBeVisible: Bool = thread.shouldBeVisible
|
||||
let linkPreviewDraft = snInputView.linkPreviewInfo?.draft
|
||||
let tsMessage = TSOutgoingMessage.from(message, associatedWith: thread)
|
||||
viewModel.appendUnsavedOutgoingTextMessage(tsMessage)
|
||||
|
||||
Storage.write(with: { transaction in
|
||||
message.linkPreview = VisibleMessage.LinkPreview.from(linkPreviewDraft, using: transaction)
|
||||
}, completion: { [weak self] in
|
||||
tsMessage.linkPreview = OWSLinkPreview.from(message.linkPreview)
|
||||
Storage.shared.write(with: { transaction in
|
||||
tsMessage.save(with: transaction as! YapDatabaseReadWriteTransaction)
|
||||
}, completion: { [weak self] in
|
||||
// At this point the TSOutgoingMessage should have its link preview set, so we can scroll to the bottom knowing
|
||||
// the height of the new message cell
|
||||
self?.scrollToBottom(isAnimated: false)
|
||||
})
|
||||
Storage.shared.write { transaction in
|
||||
MessageSender.send(message, with: [], in: thread, using: transaction as! YapDatabaseReadWriteTransaction)
|
||||
let promise: Promise<Void> = self.approveMessageRequestIfNeeded(
|
||||
for: self.thread,
|
||||
with: transaction,
|
||||
isNewThread: !oldThreadShouldBeVisible,
|
||||
timestamp: (sentTimestamp - 1) // Set 1ms earlier as this is used for sorting
|
||||
)
|
||||
.map { [weak self] _ in
|
||||
self?.viewModel.appendUnsavedOutgoingTextMessage(tsMessage)
|
||||
|
||||
Storage.write(with: { transaction in
|
||||
message.linkPreview = VisibleMessage.LinkPreview.from(linkPreviewDraft, using: transaction)
|
||||
}, completion: { [weak self] in
|
||||
tsMessage.linkPreview = OWSLinkPreview.from(message.linkPreview)
|
||||
|
||||
Storage.shared.write(
|
||||
with: { transaction in
|
||||
tsMessage.save(with: transaction as! YapDatabaseReadWriteTransaction)
|
||||
},
|
||||
completion: { [weak self] in
|
||||
// At this point the TSOutgoingMessage should have its link preview set, so we can scroll to the bottom knowing
|
||||
// the height of the new message cell
|
||||
self?.scrollToBottom(isAnimated: false)
|
||||
}
|
||||
)
|
||||
|
||||
Storage.shared.write { transaction in
|
||||
MessageSender.send(message, with: [], in: thread, using: transaction as! YapDatabaseReadWriteTransaction)
|
||||
}
|
||||
|
||||
self?.handleMessageSent()
|
||||
})
|
||||
}
|
||||
self?.handleMessageSent()
|
||||
|
||||
// Show an error indicating that approving the thread failed
|
||||
promise.catch(on: DispatchQueue.main) { [weak self] _ in
|
||||
let alert = UIAlertController(title: "Session", message: "An error occurred when trying to accept this message request", preferredStyle: .alert)
|
||||
alert.addAction(UIAlertAction(title: "OK", style: .default, handler: nil))
|
||||
self?.present(alert, animated: true, completion: nil)
|
||||
}
|
||||
|
||||
promise.retainUntilComplete()
|
||||
})
|
||||
}
|
||||
|
||||
func sendAttachments(_ attachments: [SignalAttachment], with text: String, onComplete: (() -> ())? = nil) {
|
||||
guard !showBlockedModalIfNeeded() else { return }
|
||||
|
||||
for attachment in attachments {
|
||||
if attachment.hasError {
|
||||
return showErrorAlert(for: attachment, onDismiss: onComplete)
|
||||
}
|
||||
}
|
||||
|
||||
let thread = self.thread
|
||||
let sentTimestamp: UInt64 = NSDate.millisecondTimestamp()
|
||||
let message = VisibleMessage()
|
||||
message.sentTimestamp = NSDate.millisecondTimestamp()
|
||||
message.sentTimestamp = sentTimestamp
|
||||
message.text = replaceMentions(in: text)
|
||||
|
||||
// Note: 'shouldBeVisible' is set to true the first time a thread is saved so we can
|
||||
// use it to determine if the user is creating a new thread and update the 'isApproved'
|
||||
// flags appropriately
|
||||
let oldThreadShouldBeVisible: Bool = thread.shouldBeVisible
|
||||
let tsMessage = TSOutgoingMessage.from(message, associatedWith: thread)
|
||||
|
||||
Storage.write(with: { transaction in
|
||||
tsMessage.save(with: transaction)
|
||||
// The new message cell is inserted at this point, but the TSOutgoingMessage doesn't have its attachment yet
|
||||
}, completion: { [weak self] in
|
||||
Storage.write(with: { transaction in
|
||||
MessageSender.send(message, with: attachments, in: thread, using: transaction)
|
||||
}, completion: { [weak self] in
|
||||
// At this point the TSOutgoingMessage should have its attachments set, so we can scroll to the bottom knowing
|
||||
// the height of the new message cell
|
||||
self?.scrollToBottom(isAnimated: false)
|
||||
})
|
||||
self?.handleMessageSent()
|
||||
let promise: Promise<Void> = self.approveMessageRequestIfNeeded(
|
||||
for: self.thread,
|
||||
with: transaction,
|
||||
isNewThread: !oldThreadShouldBeVisible,
|
||||
timestamp: (sentTimestamp - 1) // Set 1ms earlier as this is used for sorting
|
||||
)
|
||||
.map { [weak self] _ in
|
||||
Storage.write(
|
||||
with: { transaction in
|
||||
tsMessage.save(with: transaction)
|
||||
// The new message cell is inserted at this point, but the TSOutgoingMessage doesn't have its attachment yet
|
||||
},
|
||||
completion: { [weak self] in
|
||||
Storage.write(with: { transaction in
|
||||
MessageSender.send(message, with: attachments, in: thread, using: transaction)
|
||||
}, completion: { [weak self] in
|
||||
// At this point the TSOutgoingMessage should have its attachments set, so we can scroll to the bottom knowing
|
||||
// the height of the new message cell
|
||||
self?.scrollToBottom(isAnimated: false)
|
||||
})
|
||||
self?.handleMessageSent()
|
||||
|
||||
// Attachment successfully sent - dismiss the screen
|
||||
onComplete?()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// Show an error indicating that approving the thread failed
|
||||
promise.catch(on: DispatchQueue.main) { [weak self] _ in
|
||||
let alert = UIAlertController(title: "Session", message: "An error occurred when trying to accept this message request", preferredStyle: .alert)
|
||||
alert.addAction(UIAlertAction(title: "OK", style: .default, handler: nil))
|
||||
self?.present(alert, animated: true, completion: nil)
|
||||
}
|
||||
|
||||
// Attachment successfully sent - dismiss the screen
|
||||
onComplete?()
|
||||
promise.retainUntilComplete()
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -304,6 +390,21 @@ extension ConversationVC : InputViewDelegate, MessageCellDelegate, ContextMenuAc
|
|||
resetMentions()
|
||||
self.snInputView.text = ""
|
||||
self.snInputView.quoteDraftInfo = nil
|
||||
|
||||
// Update the input state if this is a contact thread
|
||||
if let contactThread: TSContactThread = thread as? TSContactThread {
|
||||
let contact: Contact? = Storage.shared.getContact(with: contactThread.contactSessionID())
|
||||
|
||||
// If the contact doesn't exist yet then it's a message request without the first message sent
|
||||
// so only allow text-based messages
|
||||
self.snInputView.setEnabledMessageTypes(
|
||||
(thread.isNoteToSelf() || contact?.didApproveMe == true || thread.isMessageRequest() ?
|
||||
.all : .textOnly
|
||||
),
|
||||
message: nil
|
||||
)
|
||||
}
|
||||
|
||||
self.markAllAsRead()
|
||||
if Environment.shared.preferences.soundInForeground() {
|
||||
let soundID = OWSSounds.systemSoundID(for: .messageSent, quiet: true)
|
||||
|
@ -503,6 +604,12 @@ extension ConversationVC : InputViewDelegate, MessageCellDelegate, ContextMenuAc
|
|||
// Open the document if possible
|
||||
guard let url = viewItem.attachmentStream?.originalMediaURL else { return }
|
||||
let shareVC = UIActivityViewController(activityItems: [ url ], applicationActivities: nil)
|
||||
if UIDevice.current.isIPad {
|
||||
shareVC.excludedActivityTypes = []
|
||||
shareVC.popoverPresentationController?.permittedArrowDirections = []
|
||||
shareVC.popoverPresentationController?.sourceView = self.view
|
||||
shareVC.popoverPresentationController?.sourceRect = self.view.bounds
|
||||
}
|
||||
navigationController!.present(shareVC, animated: true, completion: nil)
|
||||
}
|
||||
case .textOnlyMessage:
|
||||
|
@ -567,7 +674,7 @@ extension ConversationVC : InputViewDelegate, MessageCellDelegate, ContextMenuAc
|
|||
}))
|
||||
}
|
||||
}
|
||||
present(sheet, animated: true, completion: nil)
|
||||
presentAlert(sheet)
|
||||
}
|
||||
|
||||
func handleViewItemDoubleTapped(_ viewItem: ConversationViewItem) {
|
||||
|
@ -702,7 +809,7 @@ extension ConversationVC : InputViewDelegate, MessageCellDelegate, ContextMenuAc
|
|||
OpenGroupAPIV2.ban(publicKey, from: openGroupV2.room, on: openGroupV2.server).retainUntilComplete()
|
||||
}))
|
||||
alert.addAction(UIAlertAction(title: "Cancel", style: .default, handler: nil))
|
||||
present(alert, animated: true, completion: nil)
|
||||
presentAlert(alert)
|
||||
}
|
||||
|
||||
func banAndDeleteAllMessages(_ viewItem: ConversationViewItem) {
|
||||
|
@ -716,7 +823,7 @@ extension ConversationVC : InputViewDelegate, MessageCellDelegate, ContextMenuAc
|
|||
OpenGroupAPIV2.banAndDeleteAllMessages(publicKey, from: openGroupV2.room, on: openGroupV2.server).retainUntilComplete()
|
||||
}))
|
||||
alert.addAction(UIAlertAction(title: "Cancel", style: .default, handler: nil))
|
||||
present(alert, animated: true, completion: nil)
|
||||
presentAlert(alert)
|
||||
}
|
||||
|
||||
func handleQuoteViewCancelButtonTapped() {
|
||||
|
@ -1028,3 +1135,182 @@ extension ConversationVC: UIDocumentInteractionControllerDelegate {
|
|||
return self
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Message Request Actions
|
||||
|
||||
extension ConversationVC {
|
||||
@objc func handleBackPressed() {
|
||||
// If this thread started as a message request but isn't one anymore then we want to skip the
|
||||
// `MessageRequestsViewController` when going back
|
||||
guard
|
||||
threadStartedAsMessageRequest,
|
||||
!thread.isMessageRequest(),
|
||||
let viewControllers: [UIViewController] = navigationController?.viewControllers,
|
||||
let messageRequestsIndex = viewControllers.firstIndex(where: { $0 is MessageRequestsViewController }),
|
||||
messageRequestsIndex > 0
|
||||
else {
|
||||
navigationController?.popViewController(animated: true)
|
||||
return
|
||||
}
|
||||
|
||||
navigationController?.popToViewController(viewControllers[messageRequestsIndex - 1], animated: true)
|
||||
}
|
||||
|
||||
fileprivate func approveMessageRequestIfNeeded(for thread: TSThread?, with transaction: YapDatabaseReadWriteTransaction, isNewThread: Bool, timestamp: UInt64) -> Promise<Void> {
|
||||
guard let contactThread: TSContactThread = thread as? TSContactThread else { return Promise.value(()) }
|
||||
|
||||
// If the contact doesn't exist then we should create it so we can store the 'isApproved' state
|
||||
// (it'll be updated with correct profile info if they accept the message request so this
|
||||
// shouldn't cause weird behaviours)
|
||||
let sessionId: String = contactThread.contactSessionID()
|
||||
let contact: Contact = (Storage.shared.getContact(with: sessionId) ?? Contact(sessionID: sessionId))
|
||||
|
||||
guard !contact.isApproved else { return Promise.value(()) }
|
||||
|
||||
return Promise.value(())
|
||||
.then { [weak self] _ -> Promise<Void> in
|
||||
guard !isNewThread else { return Promise.value(()) }
|
||||
guard let strongSelf = self else { return Promise(error: MessageSender.Error.noThread) }
|
||||
|
||||
// If we aren't creating a new thread (ie. sending a message request) then send a
|
||||
// messageRequestResponse back to the sender (this allows the sender to know that
|
||||
// they have been approved and can now use this contact in closed groups)
|
||||
let (promise, seal) = Promise<Void>.pending()
|
||||
let messageRequestResponse: MessageRequestResponse = MessageRequestResponse(
|
||||
isApproved: true
|
||||
)
|
||||
messageRequestResponse.sentTimestamp = timestamp
|
||||
|
||||
// Show a loading indicator
|
||||
ModalActivityIndicatorViewController.present(fromViewController: strongSelf, canCancel: false) { _ in
|
||||
seal.fulfill(())
|
||||
}
|
||||
|
||||
return promise
|
||||
.then { MessageSender.sendNonDurably(messageRequestResponse, in: contactThread, using: transaction) }
|
||||
.map { _ in
|
||||
if self?.presentedViewController is ModalActivityIndicatorViewController {
|
||||
self?.dismiss(animated: true, completion: nil) // Dismiss the loader
|
||||
}
|
||||
}
|
||||
}
|
||||
.map { _ in
|
||||
// Default 'didApproveMe' to true for the person approving the message request
|
||||
contact.isApproved = true
|
||||
contact.didApproveMe = (contact.didApproveMe || !isNewThread)
|
||||
Storage.shared.setContact(contact, using: transaction)
|
||||
|
||||
// Hide the 'messageRequestView' since the request has been approved and force a config
|
||||
// sync to propagate the contact approval state (both must run on the main thread)
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
let messageRequestViewWasVisible: Bool = (self?.messageRequestView.isHidden == false)
|
||||
|
||||
UIView.animate(withDuration: 0.3) {
|
||||
self?.messageRequestView.isHidden = true
|
||||
self?.scrollButtonMessageRequestsBottomConstraint?.isActive = false
|
||||
self?.scrollButtonBottomConstraint?.isActive = true
|
||||
|
||||
// Update the table content inset and offset to account for the dissapearance of
|
||||
// the messageRequestsView
|
||||
if messageRequestViewWasVisible {
|
||||
let messageRequestsOffset: CGFloat = ((self?.messageRequestView.bounds.height ?? 0) + 16)
|
||||
let oldContentInset: UIEdgeInsets = (self?.messagesTableView.contentInset ?? UIEdgeInsets.zero)
|
||||
self?.messagesTableView.contentInset = UIEdgeInsets(
|
||||
top: 0,
|
||||
leading: 0,
|
||||
bottom: max(oldContentInset.bottom - messageRequestsOffset, 0),
|
||||
trailing: 0
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Update UI
|
||||
self?.updateNavBarButtons()
|
||||
|
||||
// Send a sync message with the details of the contact
|
||||
if let appDelegate = UIApplication.shared.delegate as? AppDelegate {
|
||||
appDelegate.forceSyncConfigurationNowIfNeeded(with: transaction).retainUntilComplete()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@objc func acceptMessageRequest() {
|
||||
Storage.write { transaction in
|
||||
let promise: Promise<Void> = self.approveMessageRequestIfNeeded(
|
||||
for: self.thread,
|
||||
with: transaction,
|
||||
isNewThread: false,
|
||||
timestamp: NSDate.millisecondTimestamp()
|
||||
)
|
||||
|
||||
// Show an error indicating that approving the thread failed
|
||||
promise.catch(on: DispatchQueue.main) { [weak self] _ in
|
||||
let alert = UIAlertController(title: "Session", message: NSLocalizedString("MESSAGE_REQUESTS_APPROVAL_ERROR_MESSAGE", comment: ""), preferredStyle: .alert)
|
||||
alert.addAction(UIAlertAction(title: NSLocalizedString("BUTTON_OK", comment: ""), style: .default, handler: nil))
|
||||
self?.present(alert, animated: true, completion: nil)
|
||||
}
|
||||
|
||||
promise.retainUntilComplete()
|
||||
}
|
||||
}
|
||||
|
||||
@objc func deleteMessageRequest() {
|
||||
guard let uniqueId: String = thread.uniqueId else { return }
|
||||
|
||||
let alertVC: UIAlertController = UIAlertController(title: NSLocalizedString("MESSAGE_REQUESTS_DELETE_CONFIRMATION_ACTON", comment: ""), message: nil, preferredStyle: .actionSheet)
|
||||
alertVC.addAction(UIAlertAction(title: NSLocalizedString("TXT_DELETE_TITLE", comment: ""), style: .destructive) { _ in
|
||||
// Delete the request
|
||||
Storage.write(
|
||||
with: { [weak self] transaction in
|
||||
Storage.shared.cancelPendingMessageSendJobs(for: uniqueId, using: transaction)
|
||||
|
||||
// Update the contact
|
||||
if let contactThread: TSContactThread = self?.thread as? TSContactThread {
|
||||
let sessionId: String = contactThread.contactSessionID()
|
||||
|
||||
if let contact: Contact = Storage.shared.getContact(with: sessionId) {
|
||||
contact.isApproved = false
|
||||
contact.isBlocked = 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.didApproveMe = true
|
||||
|
||||
Storage.shared.setContact(contact, using: transaction)
|
||||
}
|
||||
}
|
||||
|
||||
// Delete all thread content
|
||||
self?.thread.removeAllThreadInteractions(with: transaction)
|
||||
self?.thread.remove(with: transaction)
|
||||
},
|
||||
completion: { [weak self] in
|
||||
// Block the contact
|
||||
if let sessionId: String = (self?.thread as? TSContactThread)?.contactSessionID(), !OWSBlockingManager.shared().isRecipientIdBlocked(sessionId) {
|
||||
|
||||
// Stop observing the `BlockListDidChange` notification (we are about to pop the screen
|
||||
// so showing the banner just looks buggy)
|
||||
if let strongSelf = self {
|
||||
NotificationCenter.default.removeObserver(strongSelf, name: NSNotification.Name(rawValue: kNSNotificationName_BlockListDidChange), object: nil)
|
||||
}
|
||||
|
||||
OWSBlockingManager.shared().addBlockedPhoneNumber(sessionId)
|
||||
}
|
||||
|
||||
// Force a config sync and pop to the previous screen (both must run on the main thread)
|
||||
DispatchQueue.main.async {
|
||||
if let appDelegate = UIApplication.shared.delegate as? AppDelegate {
|
||||
appDelegate.forceSyncConfigurationNowIfNeeded().retainUntilComplete()
|
||||
}
|
||||
|
||||
self?.navigationController?.popViewController(animated: true)
|
||||
}
|
||||
}
|
||||
)
|
||||
})
|
||||
alertVC.addAction(UIAlertAction(title: NSLocalizedString("TXT_CANCEL_TITLE", comment: ""), style: .cancel, handler: nil))
|
||||
self.present(alertVC, animated: true, completion: nil)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,3 +1,6 @@
|
|||
import SessionUIKit
|
||||
import SessionMessagingKit
|
||||
import UIKit
|
||||
|
||||
// TODO:
|
||||
// • Slight paging glitch when scrolling up and loading more content
|
||||
|
@ -7,10 +10,13 @@
|
|||
final class ConversationVC : BaseVC, ConversationViewModelDelegate, OWSConversationSettingsViewDelegate, ConversationSearchControllerDelegate, UITableViewDataSource, UITableViewDelegate {
|
||||
let isUnsendRequestsEnabled = true // Set to true once unsend requests are done on all platforms
|
||||
let thread: TSThread
|
||||
let threadStartedAsMessageRequest: Bool
|
||||
let focusedMessageID: String? // This is used for global search
|
||||
var focusedMessageIndexPath: IndexPath?
|
||||
var unreadViewItems: [ConversationViewItem] = []
|
||||
var scrollButtonConstraint: NSLayoutConstraint?
|
||||
var scrollButtonBottomConstraint: NSLayoutConstraint?
|
||||
var scrollButtonMessageRequestsBottomConstraint: NSLayoutConstraint?
|
||||
var messageRequestsViewBotomConstraint: NSLayoutConstraint?
|
||||
// Search
|
||||
var isShowingSearchUI = false
|
||||
var lastSearchedText: String?
|
||||
|
@ -93,7 +99,10 @@ final class ConversationVC : BaseVC, ConversationViewModelDelegate, OWSConversat
|
|||
return result
|
||||
}()
|
||||
|
||||
// MARK: UI Components
|
||||
// MARK: - UI
|
||||
|
||||
private static let messageRequestButtonHeight: CGFloat = 34
|
||||
|
||||
lazy var titleView: ConversationTitleView = {
|
||||
let result = ConversationTitleView(thread: thread)
|
||||
result.delegate = self
|
||||
|
@ -101,13 +110,21 @@ final class ConversationVC : BaseVC, ConversationViewModelDelegate, OWSConversat
|
|||
}()
|
||||
|
||||
lazy var messagesTableView: MessagesTableView = {
|
||||
let result = MessagesTableView()
|
||||
let result: MessagesTableView = MessagesTableView()
|
||||
result.dataSource = self
|
||||
result.delegate = self
|
||||
result.contentInsetAdjustmentBehavior = .never
|
||||
result.contentInset = UIEdgeInsets(
|
||||
top: 0,
|
||||
leading: 0,
|
||||
bottom: Values.mediumSpacing,
|
||||
trailing: 0
|
||||
)
|
||||
|
||||
return result
|
||||
}()
|
||||
|
||||
lazy var snInputView = InputView(delegate: self)
|
||||
lazy var snInputView: InputView = InputView(delegate: self)
|
||||
|
||||
lazy var unreadCountView: UIView = {
|
||||
let result = UIView()
|
||||
|
@ -128,8 +145,6 @@ final class ConversationVC : BaseVC, ConversationViewModelDelegate, OWSConversat
|
|||
return result
|
||||
}()
|
||||
|
||||
lazy var scrollButton = ScrollToBottomButton(delegate: self)
|
||||
|
||||
lazy var blockedBanner: InfoBanner = {
|
||||
let name: String
|
||||
if let thread = thread as? TSContactThread {
|
||||
|
@ -146,6 +161,104 @@ final class ConversationVC : BaseVC, ConversationViewModelDelegate, OWSConversat
|
|||
return result
|
||||
}()
|
||||
|
||||
lazy var footerControlsStackView: UIStackView = {
|
||||
let result: UIStackView = UIStackView()
|
||||
result.translatesAutoresizingMaskIntoConstraints = false
|
||||
result.axis = .vertical
|
||||
result.alignment = .trailing
|
||||
result.distribution = .equalSpacing
|
||||
result.spacing = 10
|
||||
result.layoutMargins = UIEdgeInsets(top: 0, left: 20, bottom: 0, right: 20)
|
||||
result.isLayoutMarginsRelativeArrangement = true
|
||||
|
||||
return result
|
||||
}()
|
||||
|
||||
lazy var scrollButton = ScrollToBottomButton(delegate: self)
|
||||
|
||||
lazy var messageRequestView: UIView = {
|
||||
let result: UIView = UIView()
|
||||
result.translatesAutoresizingMaskIntoConstraints = false
|
||||
result.isHidden = !thread.isMessageRequest()
|
||||
result.setGradient(Gradients.defaultBackground)
|
||||
|
||||
return result
|
||||
}()
|
||||
|
||||
private let messageRequestDescriptionLabel: UILabel = {
|
||||
let result: UILabel = UILabel()
|
||||
result.translatesAutoresizingMaskIntoConstraints = false
|
||||
result.font = UIFont.systemFont(ofSize: 12)
|
||||
result.text = NSLocalizedString("MESSAGE_REQUESTS_INFO", comment: "")
|
||||
result.textColor = Colors.sessionMessageRequestsInfoText
|
||||
result.textAlignment = .center
|
||||
result.numberOfLines = 2
|
||||
|
||||
return result
|
||||
}()
|
||||
|
||||
private let messageRequestAcceptButton: UIButton = {
|
||||
let result: UIButton = UIButton()
|
||||
result.translatesAutoresizingMaskIntoConstraints = false
|
||||
result.clipsToBounds = true
|
||||
result.titleLabel?.font = UIFont.boldSystemFont(ofSize: 18)
|
||||
result.setTitle(NSLocalizedString("TXT_DELETE_ACCEPT", comment: ""), for: .normal)
|
||||
result.setTitleColor(Colors.sessionHeading, for: .normal)
|
||||
result.setBackgroundImage(
|
||||
Colors.sessionHeading
|
||||
.withAlphaComponent(isDarkMode ? 0.2 : 0.06)
|
||||
.toImage(isDarkMode: isDarkMode),
|
||||
for: .highlighted
|
||||
)
|
||||
result.layer.cornerRadius = (ConversationVC.messageRequestButtonHeight / 2)
|
||||
result.layer.borderColor = {
|
||||
if #available(iOS 13.0, *) {
|
||||
return Colors.sessionHeading
|
||||
.resolvedColor(
|
||||
// Note: This is needed for '.cgColor' to support dark mode
|
||||
with: UITraitCollection(userInterfaceStyle: isDarkMode ? .dark : .light)
|
||||
).cgColor
|
||||
}
|
||||
|
||||
return Colors.sessionHeading.cgColor
|
||||
}()
|
||||
result.layer.borderWidth = 1
|
||||
result.addTarget(self, action: #selector(acceptMessageRequest), for: .touchUpInside)
|
||||
|
||||
return result
|
||||
}()
|
||||
|
||||
private let messageRequestDeleteButton: UIButton = {
|
||||
let result: UIButton = UIButton()
|
||||
result.translatesAutoresizingMaskIntoConstraints = false
|
||||
result.clipsToBounds = true
|
||||
result.titleLabel?.font = UIFont.boldSystemFont(ofSize: 18)
|
||||
result.setTitle(NSLocalizedString("TXT_DELETE_TITLE", comment: ""), for: .normal)
|
||||
result.setTitleColor(Colors.destructive, for: .normal)
|
||||
result.setBackgroundImage(
|
||||
Colors.destructive
|
||||
.withAlphaComponent(isDarkMode ? 0.2 : 0.06)
|
||||
.toImage(isDarkMode: isDarkMode),
|
||||
for: .highlighted
|
||||
)
|
||||
result.layer.cornerRadius = (ConversationVC.messageRequestButtonHeight / 2)
|
||||
result.layer.borderColor = {
|
||||
if #available(iOS 13.0, *) {
|
||||
return Colors.destructive
|
||||
.resolvedColor(
|
||||
// Note: This is needed for '.cgColor' to support dark mode
|
||||
with: UITraitCollection(userInterfaceStyle: isDarkMode ? .dark : .light)
|
||||
).cgColor
|
||||
}
|
||||
|
||||
return Colors.destructive.cgColor
|
||||
}()
|
||||
result.layer.borderWidth = 1
|
||||
result.addTarget(self, action: #selector(deleteMessageRequest), for: .touchUpInside)
|
||||
|
||||
return result
|
||||
}()
|
||||
|
||||
// MARK: Settings
|
||||
static let unreadCountViewSize: CGFloat = 20
|
||||
/// The table view's bottom inset (content will have this distance to the bottom if the table view is fully scrolled down).
|
||||
|
@ -162,6 +275,7 @@ final class ConversationVC : BaseVC, ConversationViewModelDelegate, OWSConversat
|
|||
// MARK: Lifecycle
|
||||
init(thread: TSThread, focusedMessageID: String? = nil) {
|
||||
self.thread = thread
|
||||
self.threadStartedAsMessageRequest = thread.isMessageRequest()
|
||||
self.focusedMessageID = focusedMessageID
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
var unreadCount: UInt = 0
|
||||
|
@ -187,9 +301,49 @@ final class ConversationVC : BaseVC, ConversationViewModelDelegate, OWSConversat
|
|||
// Constraints
|
||||
view.addSubview(messagesTableView)
|
||||
messagesTableView.pin(to: view)
|
||||
|
||||
// Blocked banner
|
||||
addOrRemoveBlockedBanner()
|
||||
|
||||
// Message requests view & scroll to bottom
|
||||
view.addSubview(scrollButton)
|
||||
scrollButton.pin(.right, to: .right, of: view, withInset: -16)
|
||||
scrollButtonConstraint = scrollButton.pin(.bottom, to: .bottom, of: view, withInset: -16)
|
||||
view.addSubview(messageRequestView)
|
||||
|
||||
messageRequestView.addSubview(messageRequestDescriptionLabel)
|
||||
messageRequestView.addSubview(messageRequestAcceptButton)
|
||||
messageRequestView.addSubview(messageRequestDeleteButton)
|
||||
|
||||
scrollButton.pin(.right, to: .right, of: view, withInset: -20)
|
||||
messageRequestView.pin(.left, to: .left, of: view)
|
||||
messageRequestView.pin(.right, to: .right, of: view)
|
||||
self.messageRequestsViewBotomConstraint = messageRequestView.pin(.bottom, to: .bottom, of: view, withInset: -16)
|
||||
self.scrollButtonBottomConstraint = scrollButton.pin(.bottom, to: .bottom, of: view, withInset: -16)
|
||||
self.scrollButtonBottomConstraint?.isActive = false // Note: Need to disable this to avoid a conflict with the other bottom constraint
|
||||
self.scrollButtonMessageRequestsBottomConstraint = scrollButton.pin(.bottom, to: .top, of: messageRequestView, withInset: -16)
|
||||
self.scrollButtonMessageRequestsBottomConstraint?.isActive = thread.isMessageRequest()
|
||||
self.scrollButtonBottomConstraint?.isActive = !thread.isMessageRequest()
|
||||
|
||||
messageRequestDescriptionLabel.pin(.top, to: .top, of: messageRequestView, withInset: 10)
|
||||
messageRequestDescriptionLabel.pin(.left, to: .left, of: messageRequestView, withInset: 40)
|
||||
messageRequestDescriptionLabel.pin(.right, to: .right, of: messageRequestView, withInset: -40)
|
||||
|
||||
messageRequestAcceptButton.pin(.top, to: .bottom, of: messageRequestDescriptionLabel, withInset: 20)
|
||||
messageRequestAcceptButton.pin(.left, to: .left, of: messageRequestView, withInset: 20)
|
||||
messageRequestAcceptButton.pin(.bottom, to: .bottom, of: messageRequestView)
|
||||
messageRequestAcceptButton.set(.height, to: ConversationVC.messageRequestButtonHeight)
|
||||
|
||||
messageRequestAcceptButton.pin(.top, to: .bottom, of: messageRequestDescriptionLabel, withInset: 20)
|
||||
messageRequestAcceptButton.pin(.left, to: .left, of: messageRequestView, withInset: 20)
|
||||
messageRequestAcceptButton.pin(.bottom, to: .bottom, of: messageRequestView)
|
||||
messageRequestAcceptButton.set(.height, to: ConversationVC.messageRequestButtonHeight)
|
||||
|
||||
messageRequestDeleteButton.pin(.top, to: .bottom, of: messageRequestDescriptionLabel, withInset: 20)
|
||||
messageRequestDeleteButton.pin(.left, to: .right, of: messageRequestAcceptButton, withInset: 20)
|
||||
messageRequestDeleteButton.pin(.right, to: .right, of: messageRequestView, withInset: -20)
|
||||
messageRequestDeleteButton.pin(.bottom, to: .bottom, of: messageRequestView)
|
||||
messageRequestDeleteButton.set(.width, to: .width, of: messageRequestAcceptButton)
|
||||
messageRequestDeleteButton.set(.height, to: ConversationVC.messageRequestButtonHeight)
|
||||
|
||||
// Unread count view
|
||||
view.addSubview(unreadCountView)
|
||||
unreadCountView.addSubview(unreadCountLabel)
|
||||
|
@ -200,8 +354,7 @@ final class ConversationVC : BaseVC, ConversationViewModelDelegate, OWSConversat
|
|||
unreadCountView.centerYAnchor.constraint(equalTo: scrollButton.topAnchor).isActive = true
|
||||
unreadCountView.center(.horizontal, in: scrollButton)
|
||||
updateUnreadCountView()
|
||||
// Blocked banner
|
||||
addOrRemoveBlockedBanner()
|
||||
|
||||
// Notifications
|
||||
let notificationCenter = NotificationCenter.default
|
||||
notificationCenter.addObserver(self, selector: #selector(handleKeyboardWillChangeFrameNotification(_:)), name: UIResponder.keyboardWillChangeFrameNotification, object: nil)
|
||||
|
@ -221,6 +374,21 @@ final class ConversationVC : BaseVC, ConversationViewModelDelegate, OWSConversat
|
|||
if !draft.isEmpty {
|
||||
snInputView.text = draft
|
||||
}
|
||||
|
||||
// Update the input state if this is a contact thread
|
||||
if let contactThread: TSContactThread = thread as? TSContactThread {
|
||||
let contact: Contact? = Storage.shared.getContact(with: contactThread.contactSessionID())
|
||||
|
||||
// If the contact doesn't exist yet then it's a message request without the first message sent
|
||||
// so only allow text-based messages
|
||||
self.snInputView.setEnabledMessageTypes(
|
||||
(thread.isNoteToSelf() || contact?.didApproveMe == true || thread.isMessageRequest() ?
|
||||
.all : .textOnly
|
||||
),
|
||||
message: nil
|
||||
)
|
||||
}
|
||||
|
||||
// Update member count if this is a V2 open group
|
||||
if let v2OpenGroup = Storage.shared.getV2OpenGroup(for: thread.uniqueId!) {
|
||||
OpenGroupAPIV2.getMemberCount(for: v2OpenGroup.room, on: v2OpenGroup.server).retainUntilComplete()
|
||||
|
@ -297,32 +465,46 @@ final class ConversationVC : BaseVC, ConversationViewModelDelegate, OWSConversat
|
|||
}
|
||||
|
||||
// MARK: Updating
|
||||
|
||||
func updateNavBarButtons() {
|
||||
navigationItem.hidesBackButton = isShowingSearchUI
|
||||
|
||||
if isShowingSearchUI {
|
||||
navigationItem.leftBarButtonItem = nil
|
||||
navigationItem.rightBarButtonItems = []
|
||||
} else {
|
||||
}
|
||||
else {
|
||||
navigationItem.leftBarButtonItem = UIViewController.createOWSBackButton(withTarget: self, selector: #selector(handleBackPressed))
|
||||
var rightBarButtonItems: [UIBarButtonItem] = []
|
||||
if thread is TSContactThread {
|
||||
let size = Values.verySmallProfilePictureSize
|
||||
let profilePictureView = ProfilePictureView()
|
||||
profilePictureView.accessibilityLabel = "Settings button"
|
||||
profilePictureView.size = size
|
||||
profilePictureView.update(for: thread)
|
||||
profilePictureView.set(.width, to: size)
|
||||
profilePictureView.set(.height, to: size)
|
||||
let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(openSettings))
|
||||
profilePictureView.addGestureRecognizer(tapGestureRecognizer)
|
||||
let settingsButton = UIBarButtonItem(customView: profilePictureView)
|
||||
settingsButton.accessibilityLabel = "Settings button"
|
||||
settingsButton.isAccessibilityElement = true
|
||||
rightBarButtonItems.append(settingsButton)
|
||||
let shouldShowCallButton = SessionCall.isEnabled && !thread.isNoteToSelf() && SSKPreferences.areCallsEnabled
|
||||
if shouldShowCallButton {
|
||||
let callButton = UIBarButtonItem(image: UIImage(named: "Phone")!, style: .plain, target: self, action: #selector(startCall))
|
||||
rightBarButtonItems.append(callButton)
|
||||
if let contactThread: TSContactThread = thread as? TSContactThread {
|
||||
// Don't show the settings button for message requests
|
||||
if let contact: Contact = Storage.shared.getContact(with: contactThread.contactSessionID()), contact.isApproved, contact.didApproveMe {
|
||||
let size = Values.verySmallProfilePictureSize
|
||||
let profilePictureView = ProfilePictureView()
|
||||
profilePictureView.accessibilityLabel = "Settings button"
|
||||
profilePictureView.size = size
|
||||
profilePictureView.update(for: thread)
|
||||
profilePictureView.set(.width, to: size)
|
||||
profilePictureView.set(.height, to: size)
|
||||
let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(openSettings))
|
||||
profilePictureView.addGestureRecognizer(tapGestureRecognizer)
|
||||
let settingsButton = UIBarButtonItem(customView: profilePictureView)
|
||||
settingsButton.accessibilityLabel = "Settings button"
|
||||
settingsButton.isAccessibilityElement = true
|
||||
rightBarButtonItems.append(settingsButton)
|
||||
let shouldShowCallButton = SessionCall.isEnabled && !thread.isNoteToSelf() && SSKPreferences.areCallsEnabled
|
||||
if shouldShowCallButton {
|
||||
let callButton = UIBarButtonItem(image: UIImage(named: "Phone")!, style: .plain, target: self, action: #selector(startCall))
|
||||
rightBarButtonItems.append(callButton)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
else {
|
||||
// Note: Adding an empty button because without it the title alignment is busted (Note: The size was
|
||||
// taken from the layout inspector for the back button in Xcode
|
||||
rightBarButtonItems.append(UIBarButtonItem(customView: UIView(frame: CGRect(x: 0, y: 0, width: 37, height: 44))))
|
||||
}
|
||||
}
|
||||
else {
|
||||
let settingsButton = UIBarButtonItem(image: UIImage(named: "Gear"), style: .plain, target: self, action: #selector(openSettings))
|
||||
settingsButton.accessibilityLabel = "Settings button"
|
||||
settingsButton.isAccessibilityElement = true
|
||||
|
@ -340,24 +522,96 @@ final class ConversationVC : BaseVC, ConversationViewModelDelegate, OWSConversat
|
|||
}
|
||||
|
||||
@objc func handleKeyboardWillChangeFrameNotification(_ notification: Notification) {
|
||||
guard let newHeight = (notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue.size.height else { return }
|
||||
if (newHeight > 0 && baselineKeyboardHeight == 0) {
|
||||
baselineKeyboardHeight = newHeight
|
||||
self.messagesTableView.keyboardHeight = newHeight
|
||||
// Please refer to https://github.com/mapbox/mapbox-navigation-ios/issues/1600
|
||||
// and https://stackoverflow.com/a/25260930 to better understand what we are
|
||||
// doing with the UIViewAnimationOptions
|
||||
let userInfo: [AnyHashable: Any] = (notification.userInfo ?? [:])
|
||||
let duration = ((userInfo[UIResponder.keyboardAnimationDurationUserInfoKey] as? TimeInterval) ?? 0)
|
||||
let curveValue: Int = ((userInfo[UIResponder.keyboardAnimationCurveUserInfoKey] as? Int) ?? Int(UIView.AnimationOptions.curveEaseInOut.rawValue))
|
||||
let options: UIView.AnimationOptions = UIView.AnimationOptions(rawValue: UInt(curveValue << 16))
|
||||
let keyboardRect: CGRect = ((userInfo[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect) ?? CGRect.zero)
|
||||
|
||||
// Calculate new positions (Need the ensure the 'messageRequestView' has been layed out as it's
|
||||
// needed for proper calculations, so force an initial layout if it doesn't have a size)
|
||||
var hasDoneLayout: Bool = true
|
||||
|
||||
if messageRequestView.bounds.height <= CGFloat.leastNonzeroMagnitude {
|
||||
hasDoneLayout = false
|
||||
|
||||
UIView.performWithoutAnimation {
|
||||
self.view.layoutIfNeeded()
|
||||
}
|
||||
}
|
||||
scrollButtonConstraint?.constant = -(newHeight + 16)
|
||||
let newContentOffsetY = max(self.messagesTableView.contentOffset.y + min(lastPageTop, 0) + newHeight - self.messagesTableView.keyboardHeight, 0.0)
|
||||
self.messagesTableView.contentOffset.y = newContentOffsetY
|
||||
self.messagesTableView.keyboardHeight = newHeight
|
||||
self.scrollButton.alpha = self.getScrollButtonOpacity()
|
||||
|
||||
let keyboardTop = (UIScreen.main.bounds.height - keyboardRect.minY)
|
||||
let messageRequestsOffset: CGFloat = (messageRequestView.isHidden ? 0 : messageRequestView.bounds.height + 16)
|
||||
let oldContentInset: UIEdgeInsets = messagesTableView.contentInset
|
||||
let newContentInset: UIEdgeInsets = UIEdgeInsets(
|
||||
top: 0,
|
||||
leading: 0,
|
||||
bottom: (Values.mediumSpacing + keyboardTop + messageRequestsOffset),
|
||||
trailing: 0
|
||||
)
|
||||
let newContentOffsetY: CGFloat = (messagesTableView.contentOffset.y + (newContentInset.bottom - oldContentInset.bottom))
|
||||
let changes = { [weak self] in
|
||||
self?.scrollButtonBottomConstraint?.constant = -(keyboardTop + 16)
|
||||
self?.messageRequestsViewBotomConstraint?.constant = -(keyboardTop + 16)
|
||||
self?.messagesTableView.contentInset = newContentInset
|
||||
self?.messagesTableView.contentOffset.y = newContentOffsetY
|
||||
|
||||
let scrollButtonOpacity: CGFloat = (self?.getScrollButtonOpacity() ?? 0)
|
||||
self?.scrollButton.alpha = scrollButtonOpacity
|
||||
|
||||
self?.view.setNeedsLayout()
|
||||
self?.view.layoutIfNeeded()
|
||||
}
|
||||
|
||||
// Perform the changes (don't animate if the initial layout hasn't been completed)
|
||||
guard hasDoneLayout else {
|
||||
UIView.performWithoutAnimation {
|
||||
changes()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
UIView.animate(
|
||||
withDuration: duration,
|
||||
delay: 0,
|
||||
options: options,
|
||||
animations: changes,
|
||||
completion: nil
|
||||
)
|
||||
}
|
||||
|
||||
@objc func handleKeyboardWillHideNotification(_ notification: Notification) {
|
||||
self.messagesTableView.contentOffset.y -= (self.messagesTableView.keyboardHeight - self.baselineKeyboardHeight)
|
||||
self.messagesTableView.keyboardHeight = self.baselineKeyboardHeight
|
||||
scrollButtonConstraint?.constant = -(self.baselineKeyboardHeight + 16)
|
||||
self.scrollButton.alpha = self.getScrollButtonOpacity()
|
||||
self.unreadCountView.alpha = self.scrollButton.alpha
|
||||
// Please refer to https://github.com/mapbox/mapbox-navigation-ios/issues/1600
|
||||
// and https://stackoverflow.com/a/25260930 to better understand what we are
|
||||
// doing with the UIViewAnimationOptions
|
||||
let userInfo: [AnyHashable: Any] = (notification.userInfo ?? [:])
|
||||
let duration = ((userInfo[UIResponder.keyboardAnimationDurationUserInfoKey] as? TimeInterval) ?? 0)
|
||||
let curveValue: Int = ((userInfo[UIResponder.keyboardAnimationCurveUserInfoKey] as? Int) ?? Int(UIView.AnimationOptions.curveEaseInOut.rawValue))
|
||||
let options: UIView.AnimationOptions = UIView.AnimationOptions(rawValue: UInt(curveValue << 16))
|
||||
|
||||
let keyboardRect: CGRect = ((userInfo[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect) ?? CGRect.zero)
|
||||
let keyboardTop = (UIScreen.main.bounds.height - keyboardRect.minY)
|
||||
|
||||
UIView.animate(
|
||||
withDuration: duration,
|
||||
delay: 0,
|
||||
options: options,
|
||||
animations: { [weak self] in
|
||||
self?.scrollButtonBottomConstraint?.constant = -(keyboardTop + 16)
|
||||
self?.messageRequestsViewBotomConstraint?.constant = -(keyboardTop + 16)
|
||||
|
||||
let scrollButtonOpacity: CGFloat = (self?.getScrollButtonOpacity() ?? 0)
|
||||
self?.scrollButton.alpha = scrollButtonOpacity
|
||||
self?.unreadCountView.alpha = scrollButtonOpacity
|
||||
|
||||
self?.view.setNeedsLayout()
|
||||
self?.view.layoutIfNeeded()
|
||||
},
|
||||
completion: nil
|
||||
)
|
||||
}
|
||||
|
||||
func conversationViewModelWillUpdate() {
|
||||
|
@ -402,6 +656,20 @@ final class ConversationVC : BaseVC, ConversationViewModelDelegate, OWSConversat
|
|||
self.scrollToBottom(isAnimated: false)
|
||||
}
|
||||
}
|
||||
|
||||
// Update the input state if this is a contact thread
|
||||
if let contactThread: TSContactThread = thread as? TSContactThread {
|
||||
let contact: Contact? = Storage.shared.getContact(with: contactThread.contactSessionID())
|
||||
|
||||
// If the contact doesn't exist yet then it's a message request without the first message sent
|
||||
// so only allow text-based messages
|
||||
self.snInputView.setEnabledMessageTypes(
|
||||
(thread.isNoteToSelf() || contact?.didApproveMe == true || thread.isMessageRequest() ?
|
||||
.all : .textOnly
|
||||
),
|
||||
message: nil
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func conversationViewModelWillLoadMoreItems() {
|
||||
|
@ -468,7 +736,11 @@ final class ConversationVC : BaseVC, ConversationViewModelDelegate, OWSConversat
|
|||
|
||||
func markAllAsRead() {
|
||||
guard let lastSortID = viewItems.last?.interaction.sortId else { return }
|
||||
OWSReadReceiptManager.shared().markAsReadLocally(beforeSortId: lastSortID, thread: thread)
|
||||
OWSReadReceiptManager.shared().markAsReadLocally(
|
||||
beforeSortId: lastSortID,
|
||||
thread: thread,
|
||||
trySendReadReceipt: !thread.isMessageRequest()
|
||||
)
|
||||
SSKEnvironment.shared.disappearingMessagesJob.cleanupMessagesWhichFailedToStartExpiringFromNow()
|
||||
}
|
||||
|
||||
|
@ -568,29 +840,34 @@ final class ConversationVC : BaseVC, ConversationViewModelDelegate, OWSConversat
|
|||
func showSearchUI() {
|
||||
isShowingSearchUI = true
|
||||
// Search bar
|
||||
// FIXME: This code is duplicated with SearchBar
|
||||
let searchBar = searchController.uiSearchController.searchBar
|
||||
searchBar.searchBarStyle = .minimal
|
||||
searchBar.barStyle = .black
|
||||
searchBar.tintColor = Colors.accent
|
||||
let searchIcon = UIImage(named: "searchbar_search")!.asTintedImage(color: Colors.searchBarPlaceholder)
|
||||
searchBar.setImage(searchIcon, for: .search, state: UIControl.State.normal)
|
||||
let clearIcon = UIImage(named: "searchbar_clear")!.asTintedImage(color: Colors.searchBarPlaceholder)
|
||||
searchBar.setImage(clearIcon, for: .clear, state: UIControl.State.normal)
|
||||
let searchTextField: UITextField
|
||||
if #available(iOS 13, *) {
|
||||
searchTextField = searchBar.searchTextField
|
||||
searchBar.setUpSessionStyle()
|
||||
|
||||
let searchBarContainer = UIView()
|
||||
searchBarContainer.layoutMargins = UIEdgeInsets.zero
|
||||
searchBar.sizeToFit()
|
||||
searchBar.layoutMargins = UIEdgeInsets.zero
|
||||
searchBarContainer.set(.height, to: 44)
|
||||
searchBarContainer.set(.width, to: UIScreen.main.bounds.width - 32)
|
||||
searchBarContainer.addSubview(searchBar)
|
||||
navigationItem.titleView = searchBarContainer
|
||||
|
||||
// On iPad, the cancel button won't show
|
||||
// See more https://developer.apple.com/documentation/uikit/uisearchbar/1624283-showscancelbutton?language=objc
|
||||
if UIDevice.current.isIPad {
|
||||
let ipadCancelButton = UIButton()
|
||||
ipadCancelButton.setTitle("Cancel", for: .normal)
|
||||
ipadCancelButton.addTarget(self, action: #selector(hideSearchUI(_ :)), for: .touchUpInside)
|
||||
ipadCancelButton.setTitleColor(Colors.text, for: .normal)
|
||||
searchBarContainer.addSubview(ipadCancelButton)
|
||||
ipadCancelButton.pin(.trailing, to: .trailing, of: searchBarContainer)
|
||||
ipadCancelButton.autoVCenterInSuperview()
|
||||
searchBar.autoPinEdgesToSuperviewEdges(with: UIEdgeInsets.zero, excludingEdge: .trailing)
|
||||
searchBar.pin(.trailing, to: .leading, of: ipadCancelButton, withInset: -Values.smallSpacing)
|
||||
} else {
|
||||
searchTextField = searchBar.value(forKey: "_searchField") as! UITextField
|
||||
searchBar.autoPinEdgesToSuperviewMargins()
|
||||
}
|
||||
searchTextField.backgroundColor = Colors.searchBarBackground
|
||||
searchTextField.textColor = Colors.text
|
||||
searchTextField.attributedPlaceholder = NSAttributedString(string: "Search", attributes: [ .foregroundColor : Colors.searchBarPlaceholder ])
|
||||
searchTextField.keyboardAppearance = isLightMode ? .default : .dark
|
||||
searchBar.setPositionAdjustment(UIOffset(horizontal: 4, vertical: 0), for: .search)
|
||||
searchBar.searchTextPositionAdjustment = UIOffset(horizontal: 2, vertical: 0)
|
||||
searchBar.setPositionAdjustment(UIOffset(horizontal: -4, vertical: 0), for: .clear)
|
||||
navigationItem.titleView = searchBar
|
||||
|
||||
// Nav bar buttons
|
||||
updateNavBarButtons()
|
||||
// Hack so that the ResultsBar stays on the screen when dismissing the search field
|
||||
|
@ -624,7 +901,7 @@ final class ConversationVC : BaseVC, ConversationViewModelDelegate, OWSConversat
|
|||
navBar.stubbedNextResponder = self
|
||||
}
|
||||
|
||||
func hideSearchUI() {
|
||||
@objc func hideSearchUI(_ sender: Any? = nil) {
|
||||
isShowingSearchUI = false
|
||||
navigationItem.titleView = titleView
|
||||
updateNavBarButtons()
|
||||
|
|
|
@ -131,7 +131,6 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType);
|
|||
|
||||
- (void)copyMediaAction;
|
||||
- (void)copyTextAction;
|
||||
- (void)shareMediaAction;
|
||||
- (void)saveMediaAction;
|
||||
- (void)deleteLocallyAction;
|
||||
- (void)deleteRemotelyAction;
|
||||
|
|
|
@ -800,42 +800,6 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType)
|
|||
[UIPasteboard.generalPasteboard setData:data forPasteboardType:utiType];
|
||||
}
|
||||
|
||||
- (void)shareMediaAction
|
||||
{
|
||||
if (self.attachmentPointer != nil) {
|
||||
OWSFailDebug(@"Can't share not-yet-downloaded attachment");
|
||||
return;
|
||||
}
|
||||
|
||||
switch (self.messageCellType) {
|
||||
case OWSMessageCellType_Unknown:
|
||||
case OWSMessageCellType_TextOnlyMessage:
|
||||
case OWSMessageCellType_Audio:
|
||||
case OWSMessageCellType_GenericAttachment:
|
||||
[AttachmentSharing showShareUIForAttachment:self.attachmentStream];
|
||||
break;
|
||||
case OWSMessageCellType_MediaMessage: {
|
||||
// TODO: We need a "canShareMediaAction" method.
|
||||
OWSAssertDebug(self.mediaAlbumItems);
|
||||
NSMutableArray<TSAttachmentStream *> *attachmentStreams = [NSMutableArray new];
|
||||
for (ConversationMediaAlbumItem *mediaAlbumItem in self.mediaAlbumItems) {
|
||||
if (mediaAlbumItem.attachmentStream && mediaAlbumItem.attachmentStream.isValidVisualMedia) {
|
||||
[attachmentStreams addObject:mediaAlbumItem.attachmentStream];
|
||||
}
|
||||
}
|
||||
if (attachmentStreams.count < 1) {
|
||||
OWSFailDebug(@"Can't share media album; no valid items.");
|
||||
return;
|
||||
}
|
||||
[AttachmentSharing showShareUIForAttachments:attachmentStreams completion:nil];
|
||||
break;
|
||||
}
|
||||
case OWSMessageCellType_OversizeTextDownloading:
|
||||
OWSFailDebug(@"Can't share not-yet-downloaded attachment");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
- (BOOL)canCopyMedia
|
||||
{
|
||||
if (self.attachmentPointer != nil) {
|
||||
|
|
|
@ -3,6 +3,16 @@ final class ExpandingAttachmentsButton : UIView, InputViewButtonDelegate {
|
|||
private weak var delegate: ExpandingAttachmentsButtonDelegate?
|
||||
private var isExpanded = false { didSet { expandOrCollapse() } }
|
||||
|
||||
override var isUserInteractionEnabled: Bool {
|
||||
didSet {
|
||||
gifButton.isUserInteractionEnabled = isUserInteractionEnabled
|
||||
documentButton.isUserInteractionEnabled = isUserInteractionEnabled
|
||||
libraryButton.isUserInteractionEnabled = isUserInteractionEnabled
|
||||
cameraButton.isUserInteractionEnabled = isUserInteractionEnabled
|
||||
mainButton.isUserInteractionEnabled = isUserInteractionEnabled
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Constraints
|
||||
private lazy var gifButtonContainerBottomConstraint = gifButtonContainer.pin(.bottom, to: .bottom, of: self)
|
||||
private lazy var documentButtonContainerBottomConstraint = documentButtonContainer.pin(.bottom, to: .bottom, of: self)
|
||||
|
@ -134,7 +144,8 @@ final class ExpandingAttachmentsButton : UIView, InputViewButtonDelegate {
|
|||
}
|
||||
|
||||
// MARK: Delegate
|
||||
protocol ExpandingAttachmentsButtonDelegate : class {
|
||||
|
||||
protocol ExpandingAttachmentsButtonDelegate: AnyObject {
|
||||
|
||||
func handleGIFButtonTapped()
|
||||
func handleDocumentButtonTapped()
|
||||
|
|
|
@ -1,5 +1,13 @@
|
|||
import UIKit
|
||||
import SessionUIKit
|
||||
|
||||
final class InputView : UIView, InputViewButtonDelegate, InputTextViewDelegate, QuoteViewDelegate, LinkPreviewViewDelegate, MentionSelectionViewDelegate {
|
||||
enum MessageTypes {
|
||||
case all
|
||||
case textOnly
|
||||
case none
|
||||
}
|
||||
|
||||
private weak var delegate: InputViewDelegate?
|
||||
var quoteDraftInfo: (model: OWSQuotedReplyModel, isOutgoing: Bool)? { didSet { handleQuoteDraftChanged() } }
|
||||
var linkPreviewInfo: (url: String, draft: OWSLinkPreviewDraft?)?
|
||||
|
@ -16,10 +24,18 @@ final class InputView : UIView, InputViewButtonDelegate, InputTextViewDelegate,
|
|||
set { inputTextView.text = newValue }
|
||||
}
|
||||
|
||||
var enabledMessageTypes: MessageTypes = .all {
|
||||
didSet {
|
||||
setEnabledMessageTypes(enabledMessageTypes, message: nil)
|
||||
}
|
||||
}
|
||||
|
||||
override var intrinsicContentSize: CGSize { CGSize.zero }
|
||||
var lastSearchedText: String? { nil }
|
||||
|
||||
// MARK: UI Components
|
||||
|
||||
private var bottomStackView: UIStackView?
|
||||
private lazy var attachmentsButton = ExpandingAttachmentsButton(delegate: delegate)
|
||||
|
||||
private lazy var voiceMessageButton: InputViewButton = {
|
||||
|
@ -29,6 +45,7 @@ final class InputView : UIView, InputViewButtonDelegate, InputTextViewDelegate,
|
|||
return result
|
||||
}()
|
||||
|
||||
|
||||
private lazy var sendButton: InputViewButton = {
|
||||
let result = InputViewButton(icon: #imageLiteral(resourceName: "ArrowUp"), isSendButton: true, delegate: self)
|
||||
result.isHidden = true
|
||||
|
@ -66,6 +83,17 @@ final class InputView : UIView, InputViewButtonDelegate, InputTextViewDelegate,
|
|||
let maxWidth = UIScreen.main.bounds.width - 2 * InputViewButton.expandedSize - 2 * Values.smallSpacing - 2 * (Values.mediumSpacing - adjustment)
|
||||
return InputTextView(delegate: self, maxWidth: maxWidth)
|
||||
}()
|
||||
|
||||
private lazy var disabledInputLabel: UILabel = {
|
||||
let label: UILabel = UILabel()
|
||||
label.translatesAutoresizingMaskIntoConstraints = false
|
||||
label.font = UIFont.systemFont(ofSize: Values.smallFontSize)
|
||||
label.textColor = Colors.text.withAlphaComponent(Values.mediumOpacity)
|
||||
label.textAlignment = .center
|
||||
label.alpha = 0
|
||||
|
||||
return label
|
||||
}()
|
||||
|
||||
private lazy var additionalContentContainer = UIView()
|
||||
|
||||
|
@ -109,6 +137,7 @@ final class InputView : UIView, InputViewButtonDelegate, InputTextViewDelegate,
|
|||
bottomStackView.axis = .horizontal
|
||||
bottomStackView.spacing = Values.smallSpacing
|
||||
bottomStackView.alignment = .center
|
||||
self.bottomStackView = bottomStackView
|
||||
// Main stack view
|
||||
let mainStackView = UIStackView(arrangedSubviews: [ additionalContentContainer, bottomStackView ])
|
||||
mainStackView.axis = .vertical
|
||||
|
@ -119,6 +148,14 @@ final class InputView : UIView, InputViewButtonDelegate, InputTextViewDelegate,
|
|||
mainStackView.pin(.top, to: .bottom, of: separator)
|
||||
mainStackView.pin([ UIView.HorizontalEdge.leading, UIView.HorizontalEdge.trailing ], to: self)
|
||||
mainStackView.pin(.bottom, to: .bottom, of: self)
|
||||
|
||||
addSubview(disabledInputLabel)
|
||||
|
||||
disabledInputLabel.pin(.top, to: .top, of: mainStackView)
|
||||
disabledInputLabel.pin(.left, to: .left, of: mainStackView)
|
||||
disabledInputLabel.pin(.right, to: .right, of: mainStackView)
|
||||
disabledInputLabel.set(.height, to: InputViewButton.expandedSize)
|
||||
|
||||
// Mentions
|
||||
insertSubview(mentionsViewContainer, belowSubview: mainStackView)
|
||||
mentionsViewContainer.pin([ UIView.HorizontalEdge.left, UIView.HorizontalEdge.right ], to: self)
|
||||
|
@ -168,6 +205,9 @@ final class InputView : UIView, InputViewButtonDelegate, InputTextViewDelegate,
|
|||
}
|
||||
|
||||
private func autoGenerateLinkPreviewIfPossible() {
|
||||
// Don't allow link previews on 'none' or 'textOnly' input
|
||||
guard enabledMessageTypes == .all else { return }
|
||||
|
||||
// Suggest that the user enable link previews if they haven't already and we haven't
|
||||
// told them about link previews yet
|
||||
let text = inputTextView.text!
|
||||
|
@ -216,6 +256,29 @@ final class InputView : UIView, InputViewButtonDelegate, InputTextViewDelegate,
|
|||
}.retainUntilComplete()
|
||||
}
|
||||
|
||||
func setEnabledMessageTypes(_ messageTypes: MessageTypes, message: String?) {
|
||||
guard enabledMessageTypes != messageTypes else { return }
|
||||
|
||||
enabledMessageTypes = messageTypes
|
||||
disabledInputLabel.text = (message ?? "")
|
||||
|
||||
attachmentsButton.isUserInteractionEnabled = (messageTypes == .all)
|
||||
voiceMessageButton.isUserInteractionEnabled = (messageTypes == .all)
|
||||
|
||||
UIView.animate(withDuration: 0.3) { [weak self] in
|
||||
self?.bottomStackView?.alpha = (messageTypes != .none ? 1 : 0)
|
||||
self?.attachmentsButton.alpha = (messageTypes == .all ?
|
||||
1 :
|
||||
(messageTypes == .textOnly ? 0.4 : 0)
|
||||
)
|
||||
self?.voiceMessageButton.alpha = (messageTypes == .all ?
|
||||
1 :
|
||||
(messageTypes == .textOnly ? 0.4 : 0)
|
||||
)
|
||||
self?.disabledInputLabel.alpha = (messageTypes != .none ? 0 : 1)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Interaction
|
||||
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
||||
// Needed so that the user can tap the buttons when the expanding attachments button is expanded
|
||||
|
|
|
@ -98,6 +98,8 @@ final class InputViewButton : UIView {
|
|||
// We want to detect both taps and long presses
|
||||
|
||||
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
|
||||
guard isUserInteractionEnabled else { return }
|
||||
|
||||
UIImpactFeedbackGenerator(style: .heavy).impactOccurred()
|
||||
expand()
|
||||
invalidateLongPressIfNeeded()
|
||||
|
@ -109,12 +111,16 @@ final class InputViewButton : UIView {
|
|||
}
|
||||
|
||||
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
|
||||
guard isUserInteractionEnabled else { return }
|
||||
|
||||
if isLongPress {
|
||||
delegate?.handleInputViewButtonLongPressMoved(self, with: touches.first!)
|
||||
}
|
||||
}
|
||||
|
||||
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
|
||||
guard isUserInteractionEnabled else { return }
|
||||
|
||||
collapse()
|
||||
if !isLongPress {
|
||||
delegate?.handleInputViewButtonTapped(self)
|
||||
|
|
|
@ -165,6 +165,13 @@ public class LongTextViewController: OWSViewController {
|
|||
// MARK: - Actions
|
||||
|
||||
@objc func shareButtonPressed() {
|
||||
AttachmentSharing.showShareUI(forText: fullText)
|
||||
let shareVC = UIActivityViewController(activityItems: [ fullText ], applicationActivities: nil)
|
||||
if UIDevice.current.isIPad {
|
||||
shareVC.excludedActivityTypes = []
|
||||
shareVC.popoverPresentationController?.permittedArrowDirections = []
|
||||
shareVC.popoverPresentationController?.sourceView = self.view
|
||||
shareVC.popoverPresentationController?.sourceRect = self.view.bounds
|
||||
}
|
||||
self.present(shareVC, animated: true, completion: nil)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -121,7 +121,7 @@ final class VisibleMessageCell : MessageCell, LinkPreviewViewDelegate {
|
|||
private static let authorLabelInset: CGFloat = 12
|
||||
private static let replyButtonSize: CGFloat = 24
|
||||
private static let maxBubbleTranslationX: CGFloat = 40
|
||||
private static let swipeToReplyThreshold: CGFloat = 130
|
||||
private static let swipeToReplyThreshold: CGFloat = 110
|
||||
static let smallCornerRadius: CGFloat = 4
|
||||
static let largeCornerRadius: CGFloat = 18
|
||||
static let contactThreadHSpacing = Values.mediumSpacing
|
||||
|
|
|
@ -277,7 +277,7 @@ CGFloat kIconViewLength = 24;
|
|||
contents.title = NSLocalizedString(@"CONVERSATION_SETTINGS", @"title for conversation settings screen");
|
||||
|
||||
BOOL isNoteToSelf = self.thread.isNoteToSelf;
|
||||
|
||||
|
||||
__weak OWSConversationSettingsViewController *weakSelf = self;
|
||||
|
||||
OWSTableSection *section = [OWSTableSection new];
|
||||
|
@ -332,7 +332,7 @@ CGFloat kIconViewLength = 24;
|
|||
} actionBlock:^{
|
||||
[weakSelf tappedConversationSearch];
|
||||
}]];
|
||||
|
||||
|
||||
// Disappearing messages
|
||||
if (![self isOpenGroup]) {
|
||||
[section addItem:[OWSTableItem itemWithCustomCellBlock:^{
|
||||
|
@ -430,7 +430,7 @@ CGFloat kIconViewLength = 24;
|
|||
[slider autoPinEdge:ALEdgeLeading toEdge:ALEdgeLeading ofView:rowLabel];
|
||||
[slider autoPinTrailingToSuperviewMargin];
|
||||
[slider autoPinBottomToSuperviewMargin];
|
||||
|
||||
|
||||
cell.userInteractionEnabled = !strongSelf.hasLeftGroup;
|
||||
|
||||
cell.accessibilityIdentifier = ACCESSIBILITY_IDENTIFIER_WITH_NAME(
|
||||
|
|
|
@ -75,13 +75,17 @@ final class ConversationTitleView : UIView {
|
|||
private func getTitle() -> String {
|
||||
if let thread = thread as? TSGroupThread {
|
||||
return thread.groupModel.groupName!
|
||||
} else if thread.isNoteToSelf() {
|
||||
}
|
||||
else if thread.isNoteToSelf() {
|
||||
return "Note to Self"
|
||||
} else {
|
||||
}
|
||||
else {
|
||||
let sessionID = (thread as! TSContactThread).contactSessionID()
|
||||
var result = sessionID
|
||||
Storage.read { transaction in
|
||||
result = Storage.shared.getContact(with: sessionID)?.displayName(for: .regular) ?? "Anonymous"
|
||||
let displayName: String = ((Storage.shared.getContact(with: sessionID)?.displayName(for: .regular)) ?? sessionID)
|
||||
let middleTruncatedHexKey: String = "\(sessionID.prefix(4))...\(sessionID.suffix(4))"
|
||||
result = (displayName == sessionID ? middleTruncatedHexKey : displayName)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
|
|
@ -66,7 +66,7 @@ final class JoinOpenGroupModal : Modal {
|
|||
guard let (room, server, publicKey) = OpenGroupManagerV2.parseV2OpenGroup(from: url) else {
|
||||
let alert = UIAlertController(title: "Couldn't Join", message: nil, preferredStyle: .alert)
|
||||
alert.addAction(UIAlertAction(title: NSLocalizedString("BUTTON_OK", comment: ""), style: .default, handler: nil))
|
||||
return presentingViewController!.present(alert, animated: true, completion: nil)
|
||||
return presentingViewController!.presentAlert(alert)
|
||||
}
|
||||
presentingViewController!.dismiss(animated: true, completion: nil)
|
||||
Storage.shared.write { [presentingViewController = self.presentingViewController!] transaction in
|
||||
|
@ -78,7 +78,7 @@ final class JoinOpenGroupModal : Modal {
|
|||
.catch(on: DispatchQueue.main) { error in
|
||||
let alert = UIAlertController(title: "Couldn't Join", message: error.localizedDescription, preferredStyle: .alert)
|
||||
alert.addAction(UIAlertAction(title: NSLocalizedString("BUTTON_OK", comment: ""), style: .default, handler: nil))
|
||||
presentingViewController.present(alert, animated: true, completion: nil)
|
||||
presentingViewController.presentAlert(alert)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,22 +1,5 @@
|
|||
|
||||
final class MessagesTableView : UITableView {
|
||||
var keyboardHeight: CGFloat = 0
|
||||
|
||||
// Overriding contentInset and adjustedContentInset is to keep them from changing when the
|
||||
// conversation view controller is dismissed.
|
||||
|
||||
override var contentInset: UIEdgeInsets {
|
||||
get { UIEdgeInsets(top: 0, leading: 0, bottom: MessagesTableView.baselineContentInset + keyboardHeight, trailing: 0) }
|
||||
set { }
|
||||
}
|
||||
|
||||
override var adjustedContentInset: UIEdgeInsets {
|
||||
get { UIEdgeInsets(top: 0, leading: 0, bottom: MessagesTableView.baselineContentInset + keyboardHeight, trailing: 0) }
|
||||
set { }
|
||||
}
|
||||
|
||||
private static let baselineContentInset = Values.mediumSpacing
|
||||
|
||||
override init(frame: CGRect, style: UITableView.Style) {
|
||||
super.init(frame: frame, style: style)
|
||||
initialize()
|
||||
|
|
|
@ -73,7 +73,7 @@ final class NewDMVC : BaseVC, UIPageViewControllerDataSource, UIPageViewControll
|
|||
tabBar.pin(.leading, to: .leading, of: view)
|
||||
let tabBarInset: CGFloat
|
||||
if #available(iOS 13, *) {
|
||||
tabBarInset = navigationBar.height()
|
||||
tabBarInset = UIDevice.current.isIPad ? navigationBar.height() + 20 : navigationBar.height()
|
||||
} else {
|
||||
tabBarInset = 0
|
||||
}
|
||||
|
@ -332,6 +332,12 @@ private final class EnterPublicKeyVC : UIViewController {
|
|||
|
||||
@objc private func sharePublicKey() {
|
||||
let shareVC = UIActivityViewController(activityItems: [ getUserHexEncodedPublicKey() ], applicationActivities: nil)
|
||||
if UIDevice.current.isIPad {
|
||||
shareVC.excludedActivityTypes = []
|
||||
shareVC.popoverPresentationController?.permittedArrowDirections = []
|
||||
shareVC.popoverPresentationController?.sourceView = self.view
|
||||
shareVC.popoverPresentationController?.sourceRect = self.view.bounds
|
||||
}
|
||||
NewDMVC.navigationController!.present(shareVC, animated: true, completion: nil)
|
||||
}
|
||||
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
|
||||
|
||||
import UIKit
|
||||
|
||||
@objc
|
||||
class GlobalSearchViewController: BaseVC, UITableViewDelegate, UITableViewDataSource {
|
||||
|
||||
|
@ -94,8 +96,23 @@ class GlobalSearchViewController: BaseVC, UITableViewDelegate, UITableViewDataSo
|
|||
searchBarContainer.set(.height, to: 44)
|
||||
searchBarContainer.set(.width, to: UIScreen.main.bounds.width - 32)
|
||||
searchBarContainer.addSubview(searchBar)
|
||||
searchBar.autoPinEdgesToSuperviewMargins()
|
||||
navigationItem.titleView = searchBarContainer
|
||||
|
||||
// On iPad, the cancel button won't show
|
||||
// See more https://developer.apple.com/documentation/uikit/uisearchbar/1624283-showscancelbutton?language=objc
|
||||
if UIDevice.current.isIPad {
|
||||
let ipadCancelButton = UIButton()
|
||||
ipadCancelButton.setTitle("Cancel", for: .normal)
|
||||
ipadCancelButton.addTarget(self, action: #selector(cancel(_:)), for: .touchUpInside)
|
||||
ipadCancelButton.setTitleColor(Colors.text, for: .normal)
|
||||
searchBarContainer.addSubview(ipadCancelButton)
|
||||
ipadCancelButton.pin(.trailing, to: .trailing, of: searchBarContainer)
|
||||
ipadCancelButton.autoVCenterInSuperview()
|
||||
searchBar.autoPinEdgesToSuperviewEdges(with: UIEdgeInsets.zero, excludingEdge: .trailing)
|
||||
searchBar.pin(.trailing, to: .leading, of: ipadCancelButton, withInset: -Values.smallSpacing)
|
||||
} else {
|
||||
searchBar.autoPinEdgesToSuperviewMargins()
|
||||
}
|
||||
}
|
||||
|
||||
private func reloadTableData() {
|
||||
|
@ -169,6 +186,10 @@ class GlobalSearchViewController: BaseVC, UITableViewDelegate, UITableViewDataSo
|
|||
tableView.reloadSections([ SearchSection.recent.rawValue ], with: .top)
|
||||
Storage.shared.clearRecentSearchResults()
|
||||
}
|
||||
|
||||
@objc func cancel(_ sender: Any) {
|
||||
self.navigationController?.popViewController(animated: true)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
|
|
@ -6,6 +6,9 @@ final class HomeVC : BaseVC, UITableViewDataSource, UITableViewDelegate, NewConv
|
|||
private var threads: YapDatabaseViewMappings!
|
||||
private var threadViewModelCache: [String:ThreadViewModel] = [:] // Thread ID to ThreadViewModel
|
||||
private var tableViewTopConstraint: NSLayoutConstraint!
|
||||
private var unreadMessageRequestCount: UInt {
|
||||
OWSMessageUtils.sharedManager().unreadMessageRequestCount()
|
||||
}
|
||||
|
||||
private var threadCount: UInt {
|
||||
threads.numberOfItems(inGroup: TSInboxGroup)
|
||||
|
@ -34,6 +37,7 @@ final class HomeVC : BaseVC, UITableViewDataSource, UITableViewDelegate, NewConv
|
|||
let result = UITableView()
|
||||
result.backgroundColor = .clear
|
||||
result.separatorStyle = .none
|
||||
result.register(MessageRequestsCell.self, forCellReuseIdentifier: MessageRequestsCell.reuseIdentifier)
|
||||
result.register(ConversationCell.self, forCellReuseIdentifier: ConversationCell.reuseIdentifier)
|
||||
let bottomInset = Values.newConversationButtonBottomOffset + NewConversationButtonSet.expandedButtonSize + Values.largeSpacing + NewConversationButtonSet.collapsedButtonSize
|
||||
result.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: bottomInset, right: 0)
|
||||
|
@ -132,7 +136,7 @@ final class HomeVC : BaseVC, UITableViewDataSource, UITableViewDelegate, NewConv
|
|||
notificationCenter.addObserver(self, selector: #selector(handleSeedViewedNotification(_:)), name: .seedViewed, object: nil)
|
||||
notificationCenter.addObserver(self, selector: #selector(handleBlockedContactsUpdatedNotification(_:)), name: .blockedContactsUpdated, object: nil)
|
||||
// Threads (part 2)
|
||||
threads = YapDatabaseViewMappings(groups: [ TSInboxGroup ], view: TSThreadDatabaseViewExtensionName) // The extension should be registered at this point
|
||||
threads = YapDatabaseViewMappings(groups: [ TSMessageRequestGroup, TSInboxGroup ], view: TSThreadDatabaseViewExtensionName) // The extension should be registered at this point
|
||||
threads.setIsReversed(true, forGroup: TSInboxGroup)
|
||||
dbConnection.read { transaction in
|
||||
self.threads.update(with: transaction) // Perform the initial update
|
||||
|
@ -167,18 +171,42 @@ final class HomeVC : BaseVC, UITableViewDataSource, UITableViewDelegate, NewConv
|
|||
NotificationCenter.default.removeObserver(self)
|
||||
}
|
||||
|
||||
// MARK: Table View Data Source
|
||||
// MARK: - UITableViewDataSource
|
||||
|
||||
func numberOfSections(in tableView: UITableView) -> Int {
|
||||
return 2
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||
return Int(threadCount)
|
||||
switch section {
|
||||
case 0:
|
||||
if unreadMessageRequestCount > 0 && !CurrentAppContext().appUserDefaults()[.hasHiddenMessageRequests] {
|
||||
return 1
|
||||
}
|
||||
|
||||
return 0
|
||||
|
||||
case 1: return Int(threadCount)
|
||||
default: return 0
|
||||
}
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: ConversationCell.reuseIdentifier) as! ConversationCell
|
||||
cell.threadViewModel = threadViewModel(at: indexPath.row)
|
||||
return cell
|
||||
switch indexPath.section {
|
||||
case 0:
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: MessageRequestsCell.reuseIdentifier) as! MessageRequestsCell
|
||||
cell.update(with: Int(unreadMessageRequestCount))
|
||||
return cell
|
||||
|
||||
default:
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: ConversationCell.reuseIdentifier) as! ConversationCell
|
||||
cell.threadViewModel = threadViewModel(at: indexPath.row)
|
||||
return cell
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Updating
|
||||
|
||||
private func reload() {
|
||||
AssertIsOnMainThread()
|
||||
dbConnection.beginLongLivedReadTransaction() // Jump to the latest commit
|
||||
|
@ -203,28 +231,107 @@ final class HomeVC : BaseVC, UITableViewDataSource, UITableViewDelegate, NewConv
|
|||
let notifications = dbConnection.beginLongLivedReadTransaction()
|
||||
guard !notifications.isEmpty else { return }
|
||||
let ext = dbConnection.ext(TSThreadDatabaseViewExtensionName) as! YapDatabaseViewConnection
|
||||
let hasChanges = ext.hasChanges(forGroup: TSInboxGroup, in: notifications)
|
||||
let hasChanges = (
|
||||
ext.hasChanges(forGroup: TSMessageRequestGroup, in: notifications) ||
|
||||
ext.hasChanges(forGroup: TSInboxGroup, in: notifications)
|
||||
)
|
||||
|
||||
guard hasChanges else { return }
|
||||
|
||||
if let firstChangeSet = notifications[0].userInfo {
|
||||
let firstSnapshot = firstChangeSet[YapDatabaseSnapshotKey] as! UInt64
|
||||
|
||||
// The 'getSectionChanges' code below will crash if we try to process multiple commits at once
|
||||
// so just force a full reload
|
||||
if threads.snapshotOfLastUpdate != firstSnapshot - 1 {
|
||||
return reload() // The code below will crash if we try to process multiple commits at once
|
||||
// Check if we inserted a new message request (if so then unhide the message request banner)
|
||||
if
|
||||
let extensions: [String: Any] = firstChangeSet[YapDatabaseExtensionsKey] as? [String: Any],
|
||||
let viewExtensions: [String: Any] = extensions[TSThreadDatabaseViewExtensionName] as? [String: Any]
|
||||
{
|
||||
// Note: We do a 'flatMap' here rather than explicitly grab the desired key because
|
||||
// the key we need is 'changeset_key_changes' in 'YapDatabaseViewPrivate.h' so could
|
||||
// change due to an update and silently break this - this approach is a bit safer
|
||||
let allChanges: [Any] = Array(viewExtensions.values).compactMap { $0 as? [Any] }.flatMap { $0 }
|
||||
let messageRequestInserts = allChanges
|
||||
.compactMap { $0 as? YapDatabaseViewRowChange }
|
||||
.filter { $0.finalGroup == TSMessageRequestGroup && $0.type == .insert }
|
||||
|
||||
if !messageRequestInserts.isEmpty && CurrentAppContext().appUserDefaults()[.hasHiddenMessageRequests] {
|
||||
CurrentAppContext().appUserDefaults()[.hasHiddenMessageRequests] = false
|
||||
}
|
||||
}
|
||||
|
||||
// If there are no unread message requests then hide the message request banner
|
||||
if unreadMessageRequestCount == 0 {
|
||||
CurrentAppContext().appUserDefaults()[.hasHiddenMessageRequests] = true
|
||||
}
|
||||
|
||||
return reload()
|
||||
}
|
||||
}
|
||||
|
||||
var sectionChanges = NSArray()
|
||||
var rowChanges = NSArray()
|
||||
ext.getSectionChanges(§ionChanges, rowChanges: &rowChanges, for: notifications, with: threads)
|
||||
guard sectionChanges.count > 0 || rowChanges.count > 0 else { return }
|
||||
|
||||
// Separate out the changes for new message requests and the inbox (so we can avoid updating for
|
||||
// new messages within an existing message request)
|
||||
let messageRequestChanges = rowChanges
|
||||
.compactMap { $0 as? YapDatabaseViewRowChange }
|
||||
.filter { $0.originalGroup == TSMessageRequestGroup || $0.finalGroup == TSMessageRequestGroup }
|
||||
let inboxRowChanges = rowChanges
|
||||
.compactMap { $0 as? YapDatabaseViewRowChange }
|
||||
.filter { $0.originalGroup == TSInboxGroup || $0.finalGroup == TSInboxGroup }
|
||||
|
||||
guard sectionChanges.count > 0 || inboxRowChanges.count > 0 || messageRequestChanges.count > 0 else { return }
|
||||
|
||||
tableView.beginUpdates()
|
||||
rowChanges.forEach { rowChange in
|
||||
let rowChange = rowChange as! YapDatabaseViewRowChange
|
||||
|
||||
// If we need to unhide the message request row and then re-insert it
|
||||
if !messageRequestChanges.isEmpty {
|
||||
|
||||
// If there are no unread message requests then hide the message request banner
|
||||
if unreadMessageRequestCount == 0 && tableView.numberOfRows(inSection: 0) == 1 {
|
||||
CurrentAppContext().appUserDefaults()[.hasHiddenMessageRequests] = true
|
||||
tableView.deleteRows(at: [IndexPath(row: 0, section: 0)], with: .automatic)
|
||||
}
|
||||
else {
|
||||
if tableView.numberOfRows(inSection: 0) == 1 && Int(unreadMessageRequestCount) <= 0 {
|
||||
tableView.deleteRows(at: [IndexPath(row: 0, section: 0)], with: .automatic)
|
||||
}
|
||||
else if tableView.numberOfRows(inSection: 0) == 0 && Int(unreadMessageRequestCount) > 0 && !CurrentAppContext().appUserDefaults()[.hasHiddenMessageRequests] {
|
||||
tableView.insertRows(at: [IndexPath(row: 0, section: 0)], with: .automatic)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
inboxRowChanges.forEach { rowChange in
|
||||
let key = rowChange.collectionKey.key
|
||||
threadViewModelCache[key] = nil
|
||||
|
||||
switch rowChange.type {
|
||||
case .delete: tableView.deleteRows(at: [ rowChange.indexPath! ], with: UITableView.RowAnimation.automatic)
|
||||
case .insert: tableView.insertRows(at: [ rowChange.newIndexPath! ], with: UITableView.RowAnimation.automatic)
|
||||
case .update: tableView.reloadRows(at: [ rowChange.indexPath! ], with: UITableView.RowAnimation.automatic)
|
||||
default: break
|
||||
case .delete:
|
||||
tableView.deleteRows(at: [ rowChange.indexPath! ], with: .automatic)
|
||||
|
||||
case .insert:
|
||||
tableView.insertRows(at: [ rowChange.newIndexPath! ], with: .automatic)
|
||||
|
||||
case .update:
|
||||
tableView.reloadRows(at: [ rowChange.indexPath! ], with: .automatic)
|
||||
|
||||
case .move:
|
||||
// Note: We need to handle the move from the message requests section to the inbox (since
|
||||
// we are only showing a single row for message requests we need to custom handle this as
|
||||
// an insert as the change won't be defined correctly)
|
||||
if rowChange.originalGroup == TSMessageRequestGroup && rowChange.finalGroup == TSInboxGroup {
|
||||
tableView.insertRows(at: [ rowChange.newIndexPath! ], with: .automatic)
|
||||
}
|
||||
else if rowChange.originalGroup == TSInboxGroup && rowChange.finalGroup == TSMessageRequestGroup {
|
||||
tableView.deleteRows(at: [ rowChange.indexPath! ], with: .automatic)
|
||||
}
|
||||
|
||||
default: break
|
||||
}
|
||||
}
|
||||
tableView.endUpdates()
|
||||
|
@ -237,9 +344,18 @@ final class HomeVC : BaseVC, UITableViewDataSource, UITableViewDelegate, NewConv
|
|||
let rowChange = rowChange as! YapDatabaseViewRowChange
|
||||
let key = rowChange.collectionKey.key
|
||||
threadViewModelCache[key] = nil
|
||||
|
||||
switch rowChange.type {
|
||||
case .move: tableView.moveRow(at: rowChange.indexPath!, to: rowChange.newIndexPath!)
|
||||
default: break
|
||||
case .move:
|
||||
// Since we are custom handling this specific movement in the above 'updates' call we need
|
||||
// to avoid trying to handle it here
|
||||
if rowChange.originalGroup == TSMessageRequestGroup || rowChange.finalGroup == TSMessageRequestGroup {
|
||||
return
|
||||
}
|
||||
|
||||
tableView.moveRow(at: rowChange.indexPath!, to: rowChange.newIndexPath!)
|
||||
|
||||
default: break
|
||||
}
|
||||
}
|
||||
tableView.endUpdates()
|
||||
|
@ -289,11 +405,13 @@ final class HomeVC : BaseVC, UITableViewDataSource, UITableViewDelegate, NewConv
|
|||
profilePictureViewContainer.addSubview(pathStatusView)
|
||||
pathStatusView.pin(.trailing, to: .trailing, of: profilePictureViewContainer)
|
||||
pathStatusView.pin(.bottom, to: .bottom, of: profilePictureViewContainer)
|
||||
|
||||
// Left bar button item
|
||||
let leftBarButtonItem = UIBarButtonItem(customView: profilePictureViewContainer)
|
||||
leftBarButtonItem.accessibilityLabel = "Settings button"
|
||||
leftBarButtonItem.isAccessibilityElement = true
|
||||
navigationItem.leftBarButtonItem = leftBarButtonItem
|
||||
|
||||
// Right bar button item - search button
|
||||
let rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .search, target: self, action: #selector(showSearchUI))
|
||||
rightBarButtonItem.accessibilityLabel = "Search button"
|
||||
|
@ -308,19 +426,104 @@ final class HomeVC : BaseVC, UITableViewDataSource, UITableViewDelegate, NewConv
|
|||
tableView.reloadData()
|
||||
}
|
||||
|
||||
// MARK: Interaction
|
||||
// MARK: - UITableViewDelegate
|
||||
|
||||
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||
tableView.deselectRow(at: indexPath, animated: true)
|
||||
|
||||
switch indexPath.section {
|
||||
case 0:
|
||||
let viewController: MessageRequestsViewController = MessageRequestsViewController()
|
||||
self.navigationController?.pushViewController(viewController, animated: true)
|
||||
return
|
||||
|
||||
default:
|
||||
guard let thread = self.thread(at: indexPath.row) else { return }
|
||||
show(thread, with: ConversationViewAction.none, highlightedMessageID: nil, animated: true)
|
||||
}
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, editActionsForRowAt indexPath: IndexPath) -> [UITableViewRowAction]? {
|
||||
switch indexPath.section {
|
||||
case 0:
|
||||
let hide = UITableViewRowAction(style: .destructive, title: NSLocalizedString("TXT_HIDE_TITLE", comment: "")) { [weak self] _, _ in
|
||||
CurrentAppContext().appUserDefaults()[.hasHiddenMessageRequests] = true
|
||||
|
||||
// Animate the row removal
|
||||
self?.tableView.beginUpdates()
|
||||
self?.tableView.deleteRows(at: [indexPath], with: .automatic)
|
||||
self?.tableView.endUpdates()
|
||||
}
|
||||
hide.backgroundColor = Colors.destructive
|
||||
|
||||
return [hide]
|
||||
|
||||
default:
|
||||
guard let thread = self.thread(at: indexPath.row) else { return [] }
|
||||
let delete = UITableViewRowAction(style: .destructive, title: NSLocalizedString("TXT_DELETE_TITLE", comment: "")) { [weak self] _, _ in
|
||||
var message = NSLocalizedString("CONVERSATION_DELETE_CONFIRMATION_ALERT_MESSAGE", comment: "")
|
||||
if let thread = thread as? TSGroupThread, thread.isClosedGroup, thread.groupModel.groupAdminIds.contains(getUserHexEncodedPublicKey()) {
|
||||
message = NSLocalizedString("admin_group_leave_warning", comment: "")
|
||||
}
|
||||
let alert = UIAlertController(title: NSLocalizedString("CONVERSATION_DELETE_CONFIRMATION_ALERT_TITLE", comment: ""), message: message, preferredStyle: .alert)
|
||||
alert.addAction(UIAlertAction(title: NSLocalizedString("TXT_DELETE_TITLE", comment: ""), style: .destructive) { [weak self] _ in
|
||||
self?.delete(thread)
|
||||
})
|
||||
alert.addAction(UIAlertAction(title: NSLocalizedString("TXT_CANCEL_TITLE", comment: ""), style: .default) { _ in })
|
||||
guard let self = self else { return }
|
||||
self.presentAlert(alert)
|
||||
}
|
||||
delete.backgroundColor = Colors.destructive
|
||||
|
||||
let isPinned = thread.isPinned
|
||||
let pin = UITableViewRowAction(style: .normal, title: NSLocalizedString("PIN_BUTTON_TEXT", comment: "")) { [weak self] _, _ in
|
||||
thread.isPinned = true
|
||||
thread.save()
|
||||
self?.threadViewModelCache.removeValue(forKey: thread.uniqueId!)
|
||||
tableView.reloadRows(at: [ indexPath ], with: UITableView.RowAnimation.fade)
|
||||
}
|
||||
pin.backgroundColor = Colors.pathsBuilding
|
||||
let unpin = UITableViewRowAction(style: .normal, title: NSLocalizedString("UNPIN_BUTTON_TEXT", comment: "")) { [weak self] _, _ in
|
||||
thread.isPinned = false
|
||||
thread.save()
|
||||
self?.threadViewModelCache.removeValue(forKey: thread.uniqueId!)
|
||||
tableView.reloadRows(at: [ indexPath ], with: UITableView.RowAnimation.fade)
|
||||
}
|
||||
unpin.backgroundColor = Colors.pathsBuilding
|
||||
|
||||
if let thread = thread as? TSContactThread {
|
||||
let publicKey = thread.contactSessionID()
|
||||
let blockingManager = SSKEnvironment.shared.blockingManager
|
||||
let isBlocked = blockingManager.isRecipientIdBlocked(publicKey)
|
||||
let block = UITableViewRowAction(style: .normal, title: NSLocalizedString("BLOCK_LIST_BLOCK_BUTTON", comment: "")) { _, _ in
|
||||
blockingManager.addBlockedPhoneNumber(publicKey)
|
||||
tableView.reloadRows(at: [ indexPath ], with: UITableView.RowAnimation.fade)
|
||||
}
|
||||
block.backgroundColor = Colors.unimportant
|
||||
let unblock = UITableViewRowAction(style: .normal, title: NSLocalizedString("BLOCK_LIST_UNBLOCK_BUTTON", comment: "")) { _, _ in
|
||||
blockingManager.removeBlockedPhoneNumber(publicKey)
|
||||
tableView.reloadRows(at: [ indexPath ], with: UITableView.RowAnimation.fade)
|
||||
}
|
||||
unblock.backgroundColor = Colors.unimportant
|
||||
return [ delete, (isBlocked ? unblock : block), (isPinned ? unpin : pin) ]
|
||||
} else {
|
||||
return [ delete, (isPinned ? unpin : pin) ]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Interaction
|
||||
|
||||
func handleContinueButtonTapped(from seedReminderView: SeedReminderView) {
|
||||
let seedVC = SeedVC()
|
||||
let navigationController = OWSNavigationController(rootViewController: seedVC)
|
||||
present(navigationController, animated: true, completion: nil)
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||
guard let thread = self.thread(at: indexPath.row) else { return }
|
||||
show(thread, with: ConversationViewAction.none, highlightedMessageID: nil, animated: true)
|
||||
tableView.deselectRow(at: indexPath, animated: true)
|
||||
}
|
||||
|
||||
@objc func show(_ thread: TSThread, with action: ConversationViewAction, highlightedMessageID: String?, animated: Bool) {
|
||||
DispatchMainThreadSafe {
|
||||
if let presentedVC = self.presentedViewController {
|
||||
|
@ -331,63 +534,6 @@ final class HomeVC : BaseVC, UITableViewDataSource, UITableViewDelegate, NewConv
|
|||
}
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, editActionsForRowAt indexPath: IndexPath) -> [UITableViewRowAction]? {
|
||||
guard let thread = self.thread(at: indexPath.row) else { return [] }
|
||||
let delete = UITableViewRowAction(style: .destructive, title: NSLocalizedString("TXT_DELETE_TITLE", comment: "")) { [weak self] _, _ in
|
||||
var message = NSLocalizedString("CONVERSATION_DELETE_CONFIRMATION_ALERT_MESSAGE", comment: "")
|
||||
if let thread = thread as? TSGroupThread, thread.isClosedGroup, thread.groupModel.groupAdminIds.contains(getUserHexEncodedPublicKey()) {
|
||||
message = NSLocalizedString("admin_group_leave_warning", comment: "")
|
||||
}
|
||||
let alert = UIAlertController(title: NSLocalizedString("CONVERSATION_DELETE_CONFIRMATION_ALERT_TITLE", comment: ""), message: message, preferredStyle: .alert)
|
||||
alert.addAction(UIAlertAction(title: NSLocalizedString("TXT_DELETE_TITLE", comment: ""), style: .destructive) { [weak self] _ in
|
||||
self?.delete(thread)
|
||||
})
|
||||
alert.addAction(UIAlertAction(title: NSLocalizedString("TXT_CANCEL_TITLE", comment: ""), style: .default) { _ in })
|
||||
guard let self = self else { return }
|
||||
self.present(alert, animated: true, completion: nil)
|
||||
}
|
||||
delete.backgroundColor = Colors.destructive
|
||||
|
||||
let isPinned = thread.isPinned
|
||||
let pin = UITableViewRowAction(style: .normal, title: NSLocalizedString("PIN_BUTTON_TEXT", comment: "")) { [weak self] _, _ in
|
||||
thread.isPinned = true
|
||||
thread.save()
|
||||
self?.threadViewModelCache.removeValue(forKey: thread.uniqueId!)
|
||||
tableView.reloadRows(at: [ indexPath ], with: UITableView.RowAnimation.fade)
|
||||
}
|
||||
pin.backgroundColor = Colors.pathsBuilding
|
||||
let unpin = UITableViewRowAction(style: .normal, title: NSLocalizedString("UNPIN_BUTTON_TEXT", comment: "")) { [weak self] _, _ in
|
||||
thread.isPinned = false
|
||||
thread.save()
|
||||
self?.threadViewModelCache.removeValue(forKey: thread.uniqueId!)
|
||||
tableView.reloadRows(at: [ indexPath ], with: UITableView.RowAnimation.fade)
|
||||
}
|
||||
unpin.backgroundColor = Colors.pathsBuilding
|
||||
|
||||
if let thread = thread as? TSContactThread {
|
||||
let publicKey = thread.contactSessionID()
|
||||
let blockingManager = SSKEnvironment.shared.blockingManager
|
||||
let isBlocked = blockingManager.isRecipientIdBlocked(publicKey)
|
||||
let block = UITableViewRowAction(style: .normal, title: NSLocalizedString("BLOCK_LIST_BLOCK_BUTTON", comment: "")) { _, _ in
|
||||
blockingManager.addBlockedPhoneNumber(publicKey)
|
||||
tableView.reloadRows(at: [ indexPath ], with: UITableView.RowAnimation.fade)
|
||||
}
|
||||
block.backgroundColor = Colors.unimportant
|
||||
let unblock = UITableViewRowAction(style: .normal, title: NSLocalizedString("BLOCK_LIST_UNBLOCK_BUTTON", comment: "")) { _, _ in
|
||||
blockingManager.removeBlockedPhoneNumber(publicKey)
|
||||
tableView.reloadRows(at: [ indexPath ], with: UITableView.RowAnimation.fade)
|
||||
}
|
||||
unblock.backgroundColor = Colors.unimportant
|
||||
return [ delete, (isBlocked ? unblock : block), (isPinned ? unpin : pin) ]
|
||||
} else {
|
||||
return [ delete, (isPinned ? unpin : pin) ]
|
||||
}
|
||||
}
|
||||
|
||||
private func delete(_ thread: TSThread) {
|
||||
let openGroupV2 = Storage.shared.getV2OpenGroup(for: thread.uniqueId!)
|
||||
Storage.write { transaction in
|
||||
|
@ -410,6 +556,7 @@ final class HomeVC : BaseVC, UITableViewDataSource, UITableViewDelegate, NewConv
|
|||
@objc private func openSettings() {
|
||||
let settingsVC = SettingsVC()
|
||||
let navigationController = OWSNavigationController(rootViewController: settingsVC)
|
||||
navigationController.modalPresentationStyle = .fullScreen
|
||||
present(navigationController, animated: true, completion: nil)
|
||||
}
|
||||
|
||||
|
@ -424,12 +571,18 @@ final class HomeVC : BaseVC, UITableViewDataSource, UITableViewDelegate, NewConv
|
|||
@objc func joinOpenGroup() {
|
||||
let joinOpenGroupVC = JoinOpenGroupVC()
|
||||
let navigationController = OWSNavigationController(rootViewController: joinOpenGroupVC)
|
||||
if UIDevice.current.isIPad {
|
||||
navigationController.modalPresentationStyle = .fullScreen
|
||||
}
|
||||
present(navigationController, animated: true, completion: nil)
|
||||
}
|
||||
|
||||
@objc func createNewDM() {
|
||||
let newDMVC = NewDMVC()
|
||||
let navigationController = OWSNavigationController(rootViewController: newDMVC)
|
||||
if UIDevice.current.isIPad {
|
||||
navigationController.modalPresentationStyle = .fullScreen
|
||||
}
|
||||
present(navigationController, animated: true, completion: nil)
|
||||
}
|
||||
|
||||
|
@ -437,12 +590,18 @@ final class HomeVC : BaseVC, UITableViewDataSource, UITableViewDelegate, NewConv
|
|||
func createNewDMFromDeepLink(sessionID: String) {
|
||||
let newDMVC = NewDMVC(sessionID: sessionID)
|
||||
let navigationController = OWSNavigationController(rootViewController: newDMVC)
|
||||
if UIDevice.current.isIPad {
|
||||
navigationController.modalPresentationStyle = .fullScreen
|
||||
}
|
||||
present(navigationController, animated: true, completion: nil)
|
||||
}
|
||||
|
||||
@objc func createClosedGroup() {
|
||||
let newClosedGroupVC = NewClosedGroupVC()
|
||||
let navigationController = OWSNavigationController(rootViewController: newClosedGroupVC)
|
||||
if UIDevice.current.isIPad {
|
||||
navigationController.modalPresentationStyle = .fullScreen
|
||||
}
|
||||
present(navigationController, animated: true, completion: nil)
|
||||
}
|
||||
|
||||
|
@ -450,8 +609,9 @@ final class HomeVC : BaseVC, UITableViewDataSource, UITableViewDelegate, NewConv
|
|||
private func thread(at index: Int) -> TSThread? {
|
||||
var thread: TSThread? = nil
|
||||
dbConnection.read { transaction in
|
||||
// Note: Section needs to be '1' as we now have 'TSMessageRequests' as the 0th section
|
||||
let ext = transaction.ext(TSThreadDatabaseViewExtensionName) as! YapDatabaseViewTransaction
|
||||
thread = ext.object(atRow: UInt(index), inSection: 0, with: self.threads) as! TSThread?
|
||||
thread = ext.object(atRow: UInt(index), inSection: 1, with: self.threads) as? TSThread
|
||||
}
|
||||
return thread
|
||||
}
|
||||
|
|
|
@ -0,0 +1,441 @@
|
|||
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
|
||||
|
||||
import UIKit
|
||||
import SessionUIKit
|
||||
import SessionMessagingKit
|
||||
|
||||
@objc
|
||||
class MessageRequestsViewController: BaseVC, UITableViewDelegate, UITableViewDataSource {
|
||||
private var threads: YapDatabaseViewMappings!
|
||||
private var threadViewModelCache: [String: ThreadViewModel] = [:] // Thread ID to ThreadViewModel
|
||||
private var tableViewTopConstraint: NSLayoutConstraint!
|
||||
|
||||
private var messageRequestCount: UInt {
|
||||
threads.numberOfItems(inGroup: TSMessageRequestGroup)
|
||||
}
|
||||
|
||||
private lazy var dbConnection: YapDatabaseConnection = {
|
||||
let result = OWSPrimaryStorage.shared().newDatabaseConnection()
|
||||
result.objectCacheLimit = 500
|
||||
|
||||
return result
|
||||
}()
|
||||
|
||||
// MARK: - UI
|
||||
|
||||
private lazy var tableView: UITableView = {
|
||||
let result: UITableView = UITableView()
|
||||
result.translatesAutoresizingMaskIntoConstraints = false
|
||||
result.backgroundColor = .clear
|
||||
result.separatorStyle = .none
|
||||
result.register(MessageRequestsCell.self, forCellReuseIdentifier: MessageRequestsCell.reuseIdentifier)
|
||||
result.register(ConversationCell.self, forCellReuseIdentifier: ConversationCell.reuseIdentifier)
|
||||
result.dataSource = self
|
||||
result.delegate = self
|
||||
|
||||
let bottomInset = Values.newConversationButtonBottomOffset + NewConversationButtonSet.expandedButtonSize + Values.largeSpacing + NewConversationButtonSet.collapsedButtonSize
|
||||
result.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: bottomInset, right: 0)
|
||||
result.showsVerticalScrollIndicator = false
|
||||
|
||||
return result
|
||||
}()
|
||||
|
||||
private lazy var emptyStateLabel: UILabel = {
|
||||
let result: UILabel = UILabel()
|
||||
result.translatesAutoresizingMaskIntoConstraints = false
|
||||
result.isUserInteractionEnabled = false
|
||||
result.font = UIFont.systemFont(ofSize: Values.smallFontSize)
|
||||
result.text = NSLocalizedString("MESSAGE_REQUESTS_EMPTY_TEXT", comment: "")
|
||||
result.textColor = Colors.text
|
||||
result.textAlignment = .center
|
||||
result.numberOfLines = 0
|
||||
result.isHidden = true
|
||||
|
||||
return result
|
||||
}()
|
||||
|
||||
private lazy var fadeView: UIView = {
|
||||
let result: UIView = UIView()
|
||||
result.translatesAutoresizingMaskIntoConstraints = false
|
||||
result.isUserInteractionEnabled = false
|
||||
result.setGradient(Gradients.homeVCFade)
|
||||
|
||||
return result
|
||||
}()
|
||||
|
||||
private lazy var clearAllButton: Button = {
|
||||
let result: Button = Button(style: .destructiveOutline, size: .large)
|
||||
result.translatesAutoresizingMaskIntoConstraints = false
|
||||
result.setTitle(NSLocalizedString("MESSAGE_REQUESTS_CLEAR_ALL", comment: ""), for: .normal)
|
||||
result.setBackgroundImage(
|
||||
Colors.destructive
|
||||
.withAlphaComponent(isDarkMode ? 0.2 : 0.06)
|
||||
.toImage(isDarkMode: isDarkMode),
|
||||
for: .highlighted
|
||||
)
|
||||
result.addTarget(self, action: #selector(clearAllTapped), for: .touchUpInside)
|
||||
|
||||
return result
|
||||
}()
|
||||
|
||||
// MARK: - Lifecycle
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
ViewControllerUtilities.setUpDefaultSessionStyle(for: self, title: NSLocalizedString("MESSAGE_REQUESTS_TITLE", comment: ""), hasCustomBackButton: false)
|
||||
|
||||
// Threads (part 1)
|
||||
// Freeze the connection for use on the main thread (this gives us a stable data source that doesn't change until we tell it to)
|
||||
dbConnection.beginLongLivedReadTransaction()
|
||||
|
||||
// Add the UI (MUST be done after the thread freeze so the 'tableView' creation and setting
|
||||
// the dataSource has the correct data)
|
||||
view.addSubview(tableView)
|
||||
view.addSubview(emptyStateLabel)
|
||||
view.addSubview(fadeView)
|
||||
view.addSubview(clearAllButton)
|
||||
|
||||
// Notifications
|
||||
NotificationCenter.default.addObserver(
|
||||
self,
|
||||
selector: #selector(handleYapDatabaseModifiedNotification(_:)),
|
||||
name: .YapDatabaseModified,
|
||||
object: OWSPrimaryStorage.shared().dbNotificationObject
|
||||
)
|
||||
NotificationCenter.default.addObserver(
|
||||
self,
|
||||
selector: #selector(handleProfileDidChangeNotification(_:)),
|
||||
name: NSNotification.Name(rawValue: kNSNotificationName_OtherUsersProfileDidChange),
|
||||
object: nil
|
||||
)
|
||||
NotificationCenter.default.addObserver(
|
||||
self,
|
||||
selector: #selector(handleBlockedContactsUpdatedNotification(_:)),
|
||||
name: .blockedContactsUpdated,
|
||||
object: nil
|
||||
)
|
||||
|
||||
// Threads (part 2)
|
||||
threads = YapDatabaseViewMappings(groups: [ TSMessageRequestGroup ], view: TSThreadDatabaseViewExtensionName) // The extension should be registered at this point
|
||||
dbConnection.read { transaction in
|
||||
self.threads.update(with: transaction) // Perform the initial update
|
||||
}
|
||||
|
||||
setupLayout()
|
||||
}
|
||||
|
||||
override func viewDidAppear(_ animated: Bool) {
|
||||
super.viewDidAppear(animated)
|
||||
|
||||
reload()
|
||||
}
|
||||
|
||||
deinit {
|
||||
NotificationCenter.default.removeObserver(self)
|
||||
}
|
||||
|
||||
// MARK: - Layout
|
||||
|
||||
private func setupLayout() {
|
||||
NSLayoutConstraint.activate([
|
||||
tableView.topAnchor.constraint(equalTo: view.topAnchor, constant: Values.smallSpacing),
|
||||
tableView.leftAnchor.constraint(equalTo: view.leftAnchor),
|
||||
tableView.rightAnchor.constraint(equalTo: view.rightAnchor),
|
||||
tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
|
||||
|
||||
emptyStateLabel.topAnchor.constraint(equalTo: view.topAnchor, constant: Values.massiveSpacing),
|
||||
emptyStateLabel.leftAnchor.constraint(equalTo: view.leftAnchor, constant: Values.mediumSpacing),
|
||||
emptyStateLabel.rightAnchor.constraint(equalTo: view.rightAnchor, constant: -Values.mediumSpacing),
|
||||
emptyStateLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor),
|
||||
|
||||
fadeView.topAnchor.constraint(equalTo: view.topAnchor, constant: (0.15 * view.bounds.height)),
|
||||
fadeView.leftAnchor.constraint(equalTo: view.leftAnchor),
|
||||
fadeView.rightAnchor.constraint(equalTo: view.rightAnchor),
|
||||
fadeView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
|
||||
|
||||
clearAllButton.centerXAnchor.constraint(equalTo: view.centerXAnchor),
|
||||
clearAllButton.bottomAnchor.constraint(
|
||||
equalTo: view.safeAreaLayoutGuide.bottomAnchor,
|
||||
constant: -Values.largeSpacing
|
||||
),
|
||||
// Note: The '182' is to match the 'Next' button on the New DM page (which doesn't have a fixed width)
|
||||
clearAllButton.widthAnchor.constraint(equalToConstant: 182),
|
||||
clearAllButton.heightAnchor.constraint(equalToConstant: NewConversationButtonSet.collapsedButtonSize)
|
||||
])
|
||||
}
|
||||
|
||||
// MARK: - UITableViewDataSource
|
||||
|
||||
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||
return Int(messageRequestCount)
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: ConversationCell.reuseIdentifier) as! ConversationCell
|
||||
cell.threadViewModel = threadViewModel(at: indexPath.row)
|
||||
return cell
|
||||
}
|
||||
|
||||
// MARK: - Updating
|
||||
|
||||
private func reload() {
|
||||
AssertIsOnMainThread()
|
||||
dbConnection.beginLongLivedReadTransaction() // Jump to the latest commit
|
||||
dbConnection.read { transaction in
|
||||
self.threads.update(with: transaction)
|
||||
}
|
||||
threadViewModelCache.removeAll()
|
||||
tableView.reloadData()
|
||||
clearAllButton.isHidden = (messageRequestCount == 0)
|
||||
emptyStateLabel.isHidden = (messageRequestCount != 0)
|
||||
emptyStateLabel.isHidden = (messageRequestCount != 0)
|
||||
}
|
||||
|
||||
@objc private func handleYapDatabaseModifiedNotification(_ yapDatabase: YapDatabase) {
|
||||
// NOTE: This code is very finicky and crashes easily. Modify with care.
|
||||
AssertIsOnMainThread()
|
||||
|
||||
// If we don't capture `threads` here, a race condition can occur where the
|
||||
// `thread.snapshotOfLastUpdate != firstSnapshot - 1` check below evaluates to
|
||||
// `false`, but `threads` then changes between that check and the
|
||||
// `ext.getSectionChanges(§ionChanges, rowChanges: &rowChanges, for: notifications, with: threads)`
|
||||
// line. This causes `tableView.endUpdates()` to crash with an `NSInternalInconsistencyException`.
|
||||
let threads = threads!
|
||||
|
||||
// Create a stable state for the connection and jump to the latest commit
|
||||
let notifications = dbConnection.beginLongLivedReadTransaction()
|
||||
|
||||
guard !notifications.isEmpty else { return }
|
||||
|
||||
let ext = dbConnection.ext(TSThreadDatabaseViewExtensionName) as! YapDatabaseViewConnection
|
||||
let hasChanges = ext.hasChanges(forGroup: TSMessageRequestGroup, in: notifications)
|
||||
|
||||
guard hasChanges else { return }
|
||||
|
||||
if let firstChangeSet = notifications[0].userInfo {
|
||||
let firstSnapshot = firstChangeSet[YapDatabaseSnapshotKey] as! UInt64
|
||||
|
||||
if threads.snapshotOfLastUpdate != firstSnapshot - 1 {
|
||||
return reload() // The code below will crash if we try to process multiple commits at once
|
||||
}
|
||||
}
|
||||
|
||||
var sectionChanges = NSArray()
|
||||
var rowChanges = NSArray()
|
||||
ext.getSectionChanges(§ionChanges, rowChanges: &rowChanges, for: notifications, with: threads)
|
||||
|
||||
guard sectionChanges.count > 0 || rowChanges.count > 0 else { return }
|
||||
|
||||
tableView.beginUpdates()
|
||||
|
||||
rowChanges.forEach { rowChange in
|
||||
let rowChange = rowChange as! YapDatabaseViewRowChange
|
||||
let key = rowChange.collectionKey.key
|
||||
threadViewModelCache[key] = nil
|
||||
switch rowChange.type {
|
||||
case .delete: tableView.deleteRows(at: [ rowChange.indexPath! ], with: UITableView.RowAnimation.automatic)
|
||||
case .insert: tableView.insertRows(at: [ rowChange.newIndexPath! ], with: UITableView.RowAnimation.automatic)
|
||||
case .update: tableView.reloadRows(at: [ rowChange.indexPath! ], with: UITableView.RowAnimation.automatic)
|
||||
default: break
|
||||
}
|
||||
}
|
||||
tableView.endUpdates()
|
||||
|
||||
// HACK: Moves can have conflicts with the other 3 types of change.
|
||||
// Just batch perform all the moves separately to prevent crashing.
|
||||
// Since all the changes are from the original state to the final state,
|
||||
// it will still be correct if we pick the moves out.
|
||||
|
||||
tableView.beginUpdates()
|
||||
|
||||
rowChanges.forEach { rowChange in
|
||||
let rowChange = rowChange as! YapDatabaseViewRowChange
|
||||
let key = rowChange.collectionKey.key
|
||||
threadViewModelCache[key] = nil
|
||||
|
||||
switch rowChange.type {
|
||||
case .move: tableView.moveRow(at: rowChange.indexPath!, to: rowChange.newIndexPath!)
|
||||
default: break
|
||||
}
|
||||
}
|
||||
|
||||
tableView.endUpdates()
|
||||
clearAllButton.isHidden = (messageRequestCount == 0)
|
||||
emptyStateLabel.isHidden = (messageRequestCount != 0)
|
||||
}
|
||||
|
||||
@objc private func handleProfileDidChangeNotification(_ notification: Notification) {
|
||||
tableView.reloadData() // TODO: Just reload the affected cell
|
||||
}
|
||||
|
||||
@objc private func handleBlockedContactsUpdatedNotification(_ notification: Notification) {
|
||||
tableView.reloadData() // TODO: Just reload the affected cell
|
||||
}
|
||||
|
||||
@objc override internal func handleAppModeChangedNotification(_ notification: Notification) {
|
||||
super.handleAppModeChangedNotification(notification)
|
||||
|
||||
let gradient = Gradients.homeVCFade
|
||||
fadeView.setGradient(gradient) // Re-do the gradient
|
||||
tableView.reloadData()
|
||||
}
|
||||
|
||||
// MARK: - UITableViewDelegate
|
||||
|
||||
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||
tableView.deselectRow(at: indexPath, animated: true)
|
||||
|
||||
guard let thread = self.thread(at: indexPath.row) else { return }
|
||||
|
||||
let conversationVC = ConversationVC(thread: thread)
|
||||
self.navigationController?.pushViewController(conversationVC, animated: true)
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, editActionsForRowAt indexPath: IndexPath) -> [UITableViewRowAction]? {
|
||||
guard let thread = self.thread(at: indexPath.row) else { return [] }
|
||||
|
||||
let delete = UITableViewRowAction(style: .destructive, title: NSLocalizedString("TXT_DELETE_TITLE", comment: "")) { [weak self] _, _ in
|
||||
self?.delete(thread)
|
||||
}
|
||||
delete.backgroundColor = Colors.destructive
|
||||
|
||||
return [ delete ]
|
||||
}
|
||||
|
||||
// MARK: - Interaction
|
||||
|
||||
private func updateContactAndThread(thread: TSThread, with transaction: YapDatabaseReadWriteTransaction, onComplete: ((Bool) -> ())? = nil) {
|
||||
guard let contactThread: TSContactThread = thread as? TSContactThread else {
|
||||
onComplete?(false)
|
||||
return
|
||||
}
|
||||
|
||||
var needsSync: Bool = false
|
||||
|
||||
// Update the contact
|
||||
let sessionId: String = contactThread.contactSessionID()
|
||||
|
||||
if let contact: Contact = Storage.shared.getContact(with: sessionId), (contact.isApproved || !contact.isBlocked) {
|
||||
contact.isApproved = false
|
||||
contact.isBlocked = true
|
||||
|
||||
Storage.shared.setContact(contact, using: transaction)
|
||||
needsSync = true
|
||||
}
|
||||
|
||||
// Delete all thread content
|
||||
thread.removeAllThreadInteractions(with: transaction)
|
||||
thread.remove(with: transaction)
|
||||
|
||||
onComplete?(needsSync)
|
||||
}
|
||||
|
||||
@objc private func clearAllTapped() {
|
||||
let threadCount: Int = Int(messageRequestCount)
|
||||
let threads: [TSThread] = (0..<threadCount).compactMap { self.thread(at: $0) }
|
||||
var needsSync: Bool = false
|
||||
|
||||
let alertVC: UIAlertController = UIAlertController(title: NSLocalizedString("MESSAGE_REQUESTS_CLEAR_ALL_CONFIRMATION_TITLE", comment: ""), message: nil, preferredStyle: .actionSheet)
|
||||
alertVC.addAction(UIAlertAction(title: NSLocalizedString("MESSAGE_REQUESTS_CLEAR_ALL_CONFIRMATION_ACTON", comment: ""), style: .destructive) { _ in
|
||||
// Clear the requests
|
||||
Storage.write(
|
||||
with: { [weak self] transaction in
|
||||
threads.forEach { thread in
|
||||
if let uniqueId: String = thread.uniqueId {
|
||||
Storage.shared.cancelPendingMessageSendJobs(for: uniqueId, using: transaction)
|
||||
}
|
||||
|
||||
self?.updateContactAndThread(thread: thread, with: transaction) { threadNeedsSync in
|
||||
if threadNeedsSync {
|
||||
needsSync = true
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
completion: {
|
||||
// Block all the contacts
|
||||
threads.forEach { thread in
|
||||
if let sessionId: String = (thread as? TSContactThread)?.contactSessionID(), !OWSBlockingManager.shared().isRecipientIdBlocked(sessionId) {
|
||||
OWSBlockingManager.shared().addBlockedPhoneNumber(sessionId)
|
||||
}
|
||||
}
|
||||
|
||||
// Force a config sync (must run on the main thread)
|
||||
if needsSync {
|
||||
DispatchQueue.main.async {
|
||||
if let appDelegate = UIApplication.shared.delegate as? AppDelegate {
|
||||
appDelegate.forceSyncConfigurationNowIfNeeded().retainUntilComplete()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
})
|
||||
alertVC.addAction(UIAlertAction(title: NSLocalizedString("TXT_CANCEL_TITLE", comment: ""), style: .cancel, handler: nil))
|
||||
self.present(alertVC, animated: true, completion: nil)
|
||||
}
|
||||
|
||||
private func delete(_ thread: TSThread) {
|
||||
guard let uniqueId: String = thread.uniqueId else { return }
|
||||
|
||||
let alertVC: UIAlertController = UIAlertController(title: NSLocalizedString("MESSAGE_REQUESTS_DELETE_CONFIRMATION_ACTON", comment: ""), message: nil, preferredStyle: .actionSheet)
|
||||
alertVC.addAction(UIAlertAction(title: NSLocalizedString("TXT_DELETE_TITLE", comment: ""), style: .destructive) { _ in
|
||||
Storage.write(
|
||||
with: { [weak self] transaction in
|
||||
Storage.shared.cancelPendingMessageSendJobs(for: uniqueId, using: transaction)
|
||||
self?.updateContactAndThread(thread: thread, with: transaction)
|
||||
},
|
||||
completion: {
|
||||
// Block the contact
|
||||
if let sessionId: String = (thread as? TSContactThread)?.contactSessionID(), !OWSBlockingManager.shared().isRecipientIdBlocked(sessionId) {
|
||||
OWSBlockingManager.shared().addBlockedPhoneNumber(sessionId)
|
||||
}
|
||||
|
||||
// Force a config sync (must run on the main thread)
|
||||
DispatchQueue.main.async {
|
||||
if let appDelegate = UIApplication.shared.delegate as? AppDelegate {
|
||||
appDelegate.forceSyncConfigurationNowIfNeeded().retainUntilComplete()
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
})
|
||||
alertVC.addAction(UIAlertAction(title: NSLocalizedString("TXT_CANCEL_TITLE", comment: ""), style: .cancel, handler: nil))
|
||||
self.present(alertVC, animated: true, completion: nil)
|
||||
}
|
||||
|
||||
// MARK: - Convenience
|
||||
|
||||
private func thread(at index: Int) -> TSThread? {
|
||||
var thread: TSThread? = nil
|
||||
|
||||
dbConnection.read { transaction in
|
||||
let ext: YapDatabaseViewTransaction? = transaction.ext(TSThreadDatabaseViewExtensionName) as? YapDatabaseViewTransaction
|
||||
thread = ext?.object(atRow: UInt(index), inSection: 0, with: self.threads) as? TSThread
|
||||
}
|
||||
|
||||
return thread
|
||||
}
|
||||
|
||||
private func threadViewModel(at index: Int) -> ThreadViewModel? {
|
||||
guard let thread = thread(at: index), let uniqueId: String = thread.uniqueId else { return nil }
|
||||
|
||||
if let cachedThreadViewModel = threadViewModelCache[uniqueId] {
|
||||
return cachedThreadViewModel
|
||||
}
|
||||
else {
|
||||
var threadViewModel: ThreadViewModel? = nil
|
||||
dbConnection.read { transaction in
|
||||
threadViewModel = ThreadViewModel(thread: thread, transaction: transaction)
|
||||
}
|
||||
threadViewModelCache[uniqueId] = threadViewModel
|
||||
|
||||
return threadViewModel
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,4 +1,5 @@
|
|||
import UIKit
|
||||
import SessionUIKit
|
||||
|
||||
final class NewConversationButtonSet : UIView {
|
||||
private var isUserDragging = false
|
||||
|
@ -8,7 +9,7 @@ final class NewConversationButtonSet : UIView {
|
|||
var delegate: NewConversationButtonSetDelegate?
|
||||
|
||||
// MARK: Settings
|
||||
private let spacing = Values.largeSpacing
|
||||
private let spacing = Values.veryLargeSpacing
|
||||
private let iconSize = CGFloat(24)
|
||||
private let maxDragDistance = CGFloat(56)
|
||||
private let dragMargin = CGFloat(16)
|
||||
|
@ -21,6 +22,39 @@ final class NewConversationButtonSet : UIView {
|
|||
private lazy var createClosedGroupButton = NewConversationButton(isMainButton: false, icon: #imageLiteral(resourceName: "Group").scaled(to: CGSize(width: iconSize, height: iconSize)))
|
||||
private lazy var joinOpenGroupButton = NewConversationButton(isMainButton: false, icon: #imageLiteral(resourceName: "Globe").scaled(to: CGSize(width: iconSize, height: iconSize)))
|
||||
|
||||
private lazy var newDMLabel: UILabel = {
|
||||
let result: UILabel = UILabel()
|
||||
result.translatesAutoresizingMaskIntoConstraints = false
|
||||
result.font = UIFont.systemFont(ofSize: Values.verySmallFontSize)
|
||||
result.text = NSLocalizedString("NEW_CONVERSATION_MENU_DIRECT_MESSAGE", comment: "").uppercased()
|
||||
result.textColor = Colors.grey
|
||||
result.textAlignment = .center
|
||||
|
||||
return result
|
||||
}()
|
||||
|
||||
private lazy var createClosedGroupLabel: UILabel = {
|
||||
let result: UILabel = UILabel()
|
||||
result.translatesAutoresizingMaskIntoConstraints = false
|
||||
result.font = UIFont.systemFont(ofSize: Values.verySmallFontSize)
|
||||
result.text = NSLocalizedString("NEW_CONVERSATION_MENU_CLOSED_GROUP", comment: "").uppercased()
|
||||
result.textColor = Colors.grey
|
||||
result.textAlignment = .center
|
||||
|
||||
return result
|
||||
}()
|
||||
|
||||
private lazy var joinOpenGroupLabel: UILabel = {
|
||||
let result: UILabel = UILabel()
|
||||
result.translatesAutoresizingMaskIntoConstraints = false
|
||||
result.font = UIFont.systemFont(ofSize: Values.verySmallFontSize)
|
||||
result.text = NSLocalizedString("NEW_CONVERSATION_MENU_OPEN_GROUP", comment: "").uppercased()
|
||||
result.textColor = Colors.grey
|
||||
result.textAlignment = .center
|
||||
|
||||
return result
|
||||
}()
|
||||
|
||||
// MARK: Initialization
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
|
@ -42,15 +76,24 @@ final class NewConversationButtonSet : UIView {
|
|||
joinOpenGroupButton.accessibilityLabel = "Join open group button"
|
||||
joinOpenGroupButton.isAccessibilityElement = true
|
||||
let inset = (NewConversationButtonSet.expandedButtonSize - NewConversationButtonSet.collapsedButtonSize) / 2
|
||||
addSubview(joinOpenGroupLabel)
|
||||
addSubview(joinOpenGroupButton)
|
||||
horizontalButtonConstraints[joinOpenGroupButton] = joinOpenGroupButton.pin(.left, to: .left, of: self, withInset: inset)
|
||||
verticalButtonConstraints[joinOpenGroupButton] = joinOpenGroupButton.pin(.bottom, to: .bottom, of: self, withInset: -inset)
|
||||
joinOpenGroupLabel.center(.horizontal, in: joinOpenGroupButton)
|
||||
joinOpenGroupLabel.pin(.top, to: .bottom, of: joinOpenGroupButton, withInset: 8)
|
||||
addSubview(newDMLabel)
|
||||
addSubview(newDMButton)
|
||||
newDMButton.center(.horizontal, in: self)
|
||||
verticalButtonConstraints[newDMButton] = newDMButton.pin(.top, to: .top, of: self, withInset: inset)
|
||||
newDMLabel.center(.horizontal, in: newDMButton)
|
||||
newDMLabel.pin(.top, to: .bottom, of: newDMButton, withInset: 8)
|
||||
addSubview(createClosedGroupLabel)
|
||||
addSubview(createClosedGroupButton)
|
||||
horizontalButtonConstraints[createClosedGroupButton] = createClosedGroupButton.pin(.right, to: .right, of: self, withInset: -inset)
|
||||
verticalButtonConstraints[createClosedGroupButton] = createClosedGroupButton.pin(.bottom, to: .bottom, of: self, withInset: -inset)
|
||||
createClosedGroupLabel.center(.horizontal, in: createClosedGroupButton)
|
||||
createClosedGroupLabel.pin(.top, to: .bottom, of: createClosedGroupButton, withInset: 8)
|
||||
addSubview(mainButton)
|
||||
mainButton.center(.horizontal, in: self)
|
||||
mainButton.pin(.bottom, to: .bottom, of: self, withInset: -inset)
|
||||
|
@ -74,14 +117,17 @@ final class NewConversationButtonSet : UIView {
|
|||
@objc private func handleCreateNewClosedGroupButtonTapped() { delegate?.createClosedGroup() }
|
||||
|
||||
private func expand(isUserDragging: Bool) {
|
||||
let buttons = [ joinOpenGroupButton, newDMButton, createClosedGroupButton ]
|
||||
let views = [ joinOpenGroupButton, joinOpenGroupLabel, newDMButton, newDMLabel, createClosedGroupButton, createClosedGroupLabel ]
|
||||
UIView.animate(withDuration: 0.25, animations: {
|
||||
buttons.forEach { $0.alpha = 1 }
|
||||
views.forEach { $0.alpha = 1 }
|
||||
let inset = (NewConversationButtonSet.expandedButtonSize - NewConversationButtonSet.collapsedButtonSize) / 2
|
||||
let size = NewConversationButtonSet.collapsedButtonSize
|
||||
self.joinOpenGroupButton.frame = CGRect(origin: CGPoint(x: inset, y: self.height() - size - inset), size: CGSize(width: size, height: size))
|
||||
self.joinOpenGroupLabel.center = CGPoint(x: self.joinOpenGroupButton.center.x, y: self.joinOpenGroupButton.frame.maxY + 8 + (self.joinOpenGroupLabel.bounds.height / 2))
|
||||
self.newDMButton.frame = CGRect(center: CGPoint(x: self.bounds.center.x, y: inset + size / 2), size: CGSize(width: size, height: size))
|
||||
self.newDMLabel.center = CGPoint(x: self.newDMButton.center.x, y: self.newDMButton.frame.maxY + 8 + (self.newDMLabel.bounds.height / 2))
|
||||
self.createClosedGroupButton.frame = CGRect(origin: CGPoint(x: self.width() - size - inset, y: self.height() - size - inset), size: CGSize(width: size, height: size))
|
||||
self.createClosedGroupLabel.center = CGPoint(x: self.createClosedGroupButton.center.x, y: self.createClosedGroupButton.frame.maxY + 8 + (self.createClosedGroupLabel.bounds.height / 2))
|
||||
}, completion: { _ in
|
||||
self.isUserDragging = isUserDragging
|
||||
})
|
||||
|
@ -90,7 +136,12 @@ final class NewConversationButtonSet : UIView {
|
|||
private func collapse(withAnimation isAnimated: Bool) {
|
||||
isUserDragging = false
|
||||
let buttons = [ joinOpenGroupButton, newDMButton, createClosedGroupButton ]
|
||||
let labels = [ joinOpenGroupLabel, newDMLabel, createClosedGroupLabel ]
|
||||
UIView.animate(withDuration: isAnimated ? 0.25 : 0) {
|
||||
labels.forEach { label in
|
||||
label.alpha = 0
|
||||
label.center = self.mainButton.center
|
||||
}
|
||||
buttons.forEach { button in
|
||||
button.alpha = 0
|
||||
let size = NewConversationButtonSet.collapsedButtonSize
|
||||
|
|
|
@ -0,0 +1,134 @@
|
|||
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
|
||||
|
||||
import UIKit
|
||||
import SessionUIKit
|
||||
|
||||
class MessageRequestsCell: UITableViewCell {
|
||||
static let reuseIdentifier = "MessageRequestsCell"
|
||||
|
||||
// MARK: - Initialization
|
||||
|
||||
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
||||
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
||||
|
||||
setUpViewHierarchy()
|
||||
setupLayout()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
super.init(coder: coder)
|
||||
|
||||
setUpViewHierarchy()
|
||||
setupLayout()
|
||||
}
|
||||
|
||||
// MARK: - UI
|
||||
|
||||
private let iconContainerView: UIView = {
|
||||
let result: UIView = UIView()
|
||||
result.translatesAutoresizingMaskIntoConstraints = false
|
||||
result.clipsToBounds = true
|
||||
result.backgroundColor = Colors.sessionMessageRequestsBubble
|
||||
result.layer.cornerRadius = (Values.mediumProfilePictureSize / 2)
|
||||
|
||||
return result
|
||||
}()
|
||||
|
||||
private let iconImageView: UIImageView = {
|
||||
let result: UIImageView = UIImageView(image: #imageLiteral(resourceName: "message_requests").withRenderingMode(.alwaysTemplate))
|
||||
result.translatesAutoresizingMaskIntoConstraints = false
|
||||
result.tintColor = Colors.sessionMessageRequestsIcon
|
||||
|
||||
return result
|
||||
}()
|
||||
|
||||
private let titleLabel: UILabel = {
|
||||
let result: UILabel = UILabel()
|
||||
result.translatesAutoresizingMaskIntoConstraints = false
|
||||
result.setContentHuggingPriority(.defaultHigh, for: .horizontal)
|
||||
result.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
|
||||
result.font = .boldSystemFont(ofSize: Values.mediumFontSize)
|
||||
result.text = NSLocalizedString("MESSAGE_REQUESTS_TITLE", comment: "")
|
||||
result.textColor = Colors.sessionMessageRequestsTitle
|
||||
result.lineBreakMode = .byTruncatingTail
|
||||
|
||||
return result
|
||||
}()
|
||||
|
||||
private let unreadCountView: UIView = {
|
||||
let result: UIView = UIView()
|
||||
result.translatesAutoresizingMaskIntoConstraints = false
|
||||
result.clipsToBounds = true
|
||||
result.backgroundColor = Colors.text.withAlphaComponent(Values.veryLowOpacity)
|
||||
result.layer.cornerRadius = (ConversationCell.unreadCountViewSize / 2)
|
||||
|
||||
return result
|
||||
}()
|
||||
|
||||
private let unreadCountLabel: UILabel = {
|
||||
let result = UILabel()
|
||||
result.translatesAutoresizingMaskIntoConstraints = false
|
||||
result.font = .boldSystemFont(ofSize: Values.verySmallFontSize)
|
||||
result.textColor = Colors.text
|
||||
result.textAlignment = .center
|
||||
|
||||
return result
|
||||
}()
|
||||
|
||||
private func setUpViewHierarchy() {
|
||||
backgroundColor = Colors.cellPinned
|
||||
selectedBackgroundView = UIView()
|
||||
selectedBackgroundView?.backgroundColor = Colors.cellSelected
|
||||
|
||||
|
||||
contentView.addSubview(iconContainerView)
|
||||
contentView.addSubview(titleLabel)
|
||||
contentView.addSubview(unreadCountView)
|
||||
|
||||
iconContainerView.addSubview(iconImageView)
|
||||
unreadCountView.addSubview(unreadCountLabel)
|
||||
}
|
||||
|
||||
// MARK: - Layout
|
||||
|
||||
private func setupLayout() {
|
||||
NSLayoutConstraint.activate([
|
||||
contentView.heightAnchor.constraint(equalToConstant: 68),
|
||||
|
||||
iconContainerView.leftAnchor.constraint(
|
||||
equalTo: contentView.leftAnchor,
|
||||
// Need 'accentLineThickness' to line up correctly with the 'ConversationCell'
|
||||
constant: (Values.accentLineThickness + Values.mediumSpacing)
|
||||
),
|
||||
iconContainerView.centerYAnchor.constraint(equalTo: contentView.centerYAnchor),
|
||||
iconContainerView.widthAnchor.constraint(equalToConstant: Values.mediumProfilePictureSize),
|
||||
iconContainerView.heightAnchor.constraint(equalToConstant: Values.mediumProfilePictureSize),
|
||||
|
||||
iconImageView.centerXAnchor.constraint(equalTo: iconContainerView.centerXAnchor),
|
||||
iconImageView.centerYAnchor.constraint(equalTo: iconContainerView.centerYAnchor),
|
||||
iconImageView.widthAnchor.constraint(equalToConstant: 25),
|
||||
iconImageView.heightAnchor.constraint(equalToConstant: 22),
|
||||
|
||||
titleLabel.leftAnchor.constraint(equalTo: iconContainerView.rightAnchor, constant: Values.mediumSpacing),
|
||||
titleLabel.rightAnchor.constraint(lessThanOrEqualTo: contentView.rightAnchor, constant: -Values.mediumSpacing),
|
||||
titleLabel.centerYAnchor.constraint(equalTo: contentView.centerYAnchor),
|
||||
|
||||
unreadCountView.leftAnchor.constraint(equalTo: titleLabel.rightAnchor, constant: (Values.smallSpacing / 2)),
|
||||
unreadCountView.centerYAnchor.constraint(equalTo: titleLabel.centerYAnchor),
|
||||
unreadCountView.widthAnchor.constraint(equalToConstant: ConversationCell.unreadCountViewSize),
|
||||
unreadCountView.heightAnchor.constraint(equalToConstant: ConversationCell.unreadCountViewSize),
|
||||
|
||||
unreadCountLabel.topAnchor.constraint(equalTo: unreadCountView.topAnchor),
|
||||
unreadCountLabel.leftAnchor.constraint(equalTo: unreadCountView.leftAnchor),
|
||||
unreadCountLabel.rightAnchor.constraint(equalTo: unreadCountView.rightAnchor),
|
||||
unreadCountLabel.bottomAnchor.constraint(equalTo: unreadCountView.bottomAnchor)
|
||||
])
|
||||
}
|
||||
|
||||
// MARK: - Content
|
||||
|
||||
func update(with count: Int) {
|
||||
unreadCountLabel.text = "\(count)"
|
||||
unreadCountView.isHidden = (count <= 0)
|
||||
}
|
||||
}
|
|
@ -3,7 +3,6 @@
|
|||
//
|
||||
|
||||
#import "MediaDetailViewController.h"
|
||||
#import "AttachmentSharing.h"
|
||||
#import "ConversationViewItem.h"
|
||||
#import "Session-Swift.h"
|
||||
#import "TSAttachmentStream.h"
|
||||
|
|
|
@ -380,8 +380,20 @@ class MediaPageViewController: UIPageViewController, UIPageViewControllerDataSou
|
|||
}
|
||||
|
||||
let attachmentStream = currentViewController.galleryItem.attachmentStream
|
||||
|
||||
AttachmentSharing.showShareUI(forAttachment: attachmentStream) { activityType in
|
||||
|
||||
let shareVC = UIActivityViewController(activityItems: [ attachmentStream.originalMediaURL! ], applicationActivities: nil)
|
||||
if UIDevice.current.isIPad {
|
||||
shareVC.excludedActivityTypes = []
|
||||
shareVC.popoverPresentationController?.permittedArrowDirections = []
|
||||
shareVC.popoverPresentationController?.sourceView = self.view
|
||||
shareVC.popoverPresentationController?.sourceRect = self.view.bounds
|
||||
}
|
||||
shareVC.completionWithItemsHandler = { activityType, completed, returnedItems, activityError in
|
||||
if let activityError = activityError {
|
||||
SNLog("Failed to share with activityError: \(activityError)")
|
||||
} else if completed {
|
||||
SNLog("Did share with activityType: \(activityType.debugDescription)")
|
||||
}
|
||||
guard let activityType = activityType, activityType == .saveToCameraRoll,
|
||||
let tsMessage = currentViewController.galleryItem.message as? TSIncomingMessage, let thread = tsMessage.thread as? TSContactThread else { return }
|
||||
let message = DataExtractionNotification()
|
||||
|
@ -389,8 +401,8 @@ class MediaPageViewController: UIPageViewController, UIPageViewControllerDataSou
|
|||
Storage.write { transaction in
|
||||
MessageSender.send(message, in: thread, using: transaction)
|
||||
}
|
||||
|
||||
}
|
||||
self.present(shareVC, animated: true, completion: nil)
|
||||
}
|
||||
|
||||
@objc
|
||||
|
|
|
@ -151,12 +151,19 @@ extension AppDelegate {
|
|||
let job = MessageSendJob(message: configurationMessage, destination: destination)
|
||||
JobQueue.shared.add(job, using: transaction)
|
||||
}
|
||||
userDefaults[.lastConfigurationSync] = Date()
|
||||
|
||||
// Only update the 'lastConfigurationSync' timestamp if we have done the first sync (Don't want
|
||||
// a new device config sync to override config syncs from other devices)
|
||||
if userDefaults[.hasSyncedInitialConfiguration] {
|
||||
userDefaults[.lastConfigurationSync] = Date()
|
||||
}
|
||||
}
|
||||
|
||||
func forceSyncConfigurationNowIfNeeded() -> Promise<Void> {
|
||||
guard Storage.shared.getUser()?.name != nil,
|
||||
let configurationMessage = ConfigurationMessage.getCurrent() else { return Promise.value(()) }
|
||||
func forceSyncConfigurationNowIfNeeded(with transaction: YapDatabaseReadWriteTransaction? = nil) -> Promise<Void> {
|
||||
guard Storage.shared.getUser()?.name != nil, let configurationMessage = ConfigurationMessage.getCurrent(with: transaction) else {
|
||||
return Promise.value(())
|
||||
}
|
||||
|
||||
let destination = Message.Destination.contact(publicKey: getUserHexEncodedPublicKey())
|
||||
let (promise, seal) = Promise<Void>.pending()
|
||||
Storage.writeSync { transaction in
|
||||
|
|
12
Session/Meta/Images.xcassets/Session/message_requests.imageset/Contents.json
vendored
Normal file
12
Session/Meta/Images.xcassets/Session/message_requests.imageset/Contents.json
vendored
Normal file
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "message_requests.pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
125
Session/Meta/Images.xcassets/Session/message_requests.imageset/message_requests.pdf
vendored
Normal file
125
Session/Meta/Images.xcassets/Session/message_requests.imageset/message_requests.pdf
vendored
Normal file
|
@ -0,0 +1,125 @@
|
|||
%PDF-1.7
|
||||
|
||||
1 0 obj
|
||||
<< >>
|
||||
endobj
|
||||
|
||||
2 0 obj
|
||||
<< /Length 3 0 R >>
|
||||
stream
|
||||
/DeviceRGB CS
|
||||
/DeviceRGB cs
|
||||
q
|
||||
1.000000 0.000000 -0.000000 1.000000 0.000000 -0.001099 cm
|
||||
0.000000 0.000000 0.000000 scn
|
||||
21.607576 22.001099 m
|
||||
3.393768 22.001099 l
|
||||
1.522707 22.001099 0.000053 20.440092 0.000053 18.520739 c
|
||||
0.000053 1.402527 l
|
||||
-0.002093 1.156628 0.061090 0.914558 0.183163 0.701015 c
|
||||
0.305237 0.487473 0.481828 0.310095 0.694923 0.186979 c
|
||||
0.900043 0.066381 1.133498 0.002249 1.371506 0.001116 c
|
||||
1.609514 -0.000017 1.843569 0.061890 2.049829 0.180531 c
|
||||
6.724900 2.833986 l
|
||||
7.053913 3.022442 7.426339 3.122168 7.805599 3.123371 c
|
||||
21.606285 3.123371 l
|
||||
23.477345 3.123371 25.000000 4.684376 25.000000 6.603730 c
|
||||
25.000000 18.514294 l
|
||||
25.001291 20.436871 23.478638 22.001099 21.607576 22.001099 c
|
||||
h
|
||||
23.775423 6.603730 m
|
||||
23.775423 5.359823 22.803120 4.347942 21.607576 4.347942 c
|
||||
7.806890 4.347942 l
|
||||
7.216724 4.345131 6.637146 4.191124 6.123580 3.900650 c
|
||||
1.445283 1.244619 l
|
||||
1.425066 1.232325 1.401854 1.225824 1.378185 1.225824 c
|
||||
1.354515 1.225824 1.331300 1.232325 1.311082 1.244619 c
|
||||
1.283574 1.260406 1.261027 1.283550 1.245980 1.311449 c
|
||||
1.230933 1.339348 1.223987 1.370895 1.225920 1.402527 c
|
||||
1.225920 18.520739 l
|
||||
1.225920 19.764645 2.198225 20.776527 3.393768 20.776527 c
|
||||
21.607576 20.776527 l
|
||||
22.803120 20.776527 23.775423 19.761423 23.775423 18.514294 c
|
||||
23.775423 6.603730 l
|
||||
h
|
||||
f
|
||||
n
|
||||
Q
|
||||
q
|
||||
1.000000 0.000000 -0.000000 1.000000 0.000000 15.369263 cm
|
||||
0.000000 0.000000 0.000000 scn
|
||||
12.584548 1.987679 m
|
||||
10.633485 1.987679 9.374066 1.053783 9.333419 -1.165269 c
|
||||
11.107699 -1.165269 l
|
||||
11.202542 0.052213 11.785154 0.377046 12.584548 0.377046 c
|
||||
13.383942 0.377046 13.789767 -0.123739 13.789767 -0.718623 c
|
||||
13.789767 -1.733728 13.467169 -1.936749 11.730955 -3.073668 c
|
||||
11.730955 -4.643053 l
|
||||
13.519432 -4.643053 l
|
||||
13.519432 -3.303759 l
|
||||
14.521417 -2.802974 15.648563 -1.990891 15.648563 -0.516248 c
|
||||
15.648563 0.958394 14.589163 1.987679 12.584548 1.987679 c
|
||||
h
|
||||
f
|
||||
n
|
||||
Q
|
||||
q
|
||||
1.000000 0.000000 -0.000000 1.000000 0.000000 20.065186 cm
|
||||
0.000000 0.000000 0.000000 scn
|
||||
13.627180 -10.381148 m
|
||||
11.568368 -10.381148 l
|
||||
11.568368 -12.315970 l
|
||||
13.627180 -12.315970 l
|
||||
13.627180 -10.381148 l
|
||||
h
|
||||
f
|
||||
n
|
||||
Q
|
||||
|
||||
endstream
|
||||
endobj
|
||||
|
||||
3 0 obj
|
||||
2088
|
||||
endobj
|
||||
|
||||
4 0 obj
|
||||
<< /Annots []
|
||||
/Type /Page
|
||||
/MediaBox [ 0.000000 0.000000 25.000000 22.000000 ]
|
||||
/Resources 1 0 R
|
||||
/Contents 2 0 R
|
||||
/Parent 5 0 R
|
||||
>>
|
||||
endobj
|
||||
|
||||
5 0 obj
|
||||
<< /Kids [ 4 0 R ]
|
||||
/Count 1
|
||||
/Type /Pages
|
||||
>>
|
||||
endobj
|
||||
|
||||
6 0 obj
|
||||
<< /Pages 5 0 R
|
||||
/Type /Catalog
|
||||
>>
|
||||
endobj
|
||||
|
||||
xref
|
||||
0 7
|
||||
0000000000 65535 f
|
||||
0000000010 00000 n
|
||||
0000000034 00000 n
|
||||
0000002178 00000 n
|
||||
0000002201 00000 n
|
||||
0000002374 00000 n
|
||||
0000002448 00000 n
|
||||
trailer
|
||||
<< /ID [ (some) (id) ]
|
||||
/Root 6 0 R
|
||||
/Size 7
|
||||
>>
|
||||
startxref
|
||||
2507
|
||||
%%EOF
|
|
@ -107,6 +107,11 @@
|
|||
<string>SpaceMono-Bold.ttf</string>
|
||||
<string>SpaceMono-Regular.ttf</string>
|
||||
</array>
|
||||
<key>UIApplicationSceneManifest</key>
|
||||
<dict>
|
||||
<key>UIApplicationSupportsMultipleScenes</key>
|
||||
<false/>
|
||||
</dict>
|
||||
<key>UIApplicationShortcutItems</key>
|
||||
<array>
|
||||
<dict>
|
||||
|
|
|
@ -40,7 +40,6 @@
|
|||
#import <SignalCoreKit/OWSAsserts.h>
|
||||
#import <SignalCoreKit/OWSLogs.h>
|
||||
#import <SignalCoreKit/Threading.h>
|
||||
#import <SignalUtilitiesKit/AttachmentSharing.h>
|
||||
#import <SignalUtilitiesKit/ContactTableViewCell.h>
|
||||
#import <SessionMessagingKit/Environment.h>
|
||||
#import <SessionMessagingKit/OWSAudioPlayer.h>
|
||||
|
|
|
@ -94,12 +94,18 @@
|
|||
"BLOCK_LIST_BLOCK_BUTTON" = "Blockieren";
|
||||
/* A format for the 'block user' action sheet title. Embeds {{the blocked user's name or phone number}}. */
|
||||
"BLOCK_LIST_BLOCK_USER_TITLE_FORMAT" = "%@ blockieren?";
|
||||
/* A format for the 'unblock conversation' action sheet title. Embeds the {{conversation title}}. */
|
||||
"BLOCK_LIST_UNBLOCK_TITLE_FORMAT" = "%@ freigeben?";
|
||||
/* Button label for the 'unblock' button */
|
||||
"BLOCK_LIST_UNBLOCK_BUTTON" = "Freigeben";
|
||||
/* The message format of the 'conversation blocked' alert. Embeds the {{conversation title}}. */
|
||||
"BLOCK_LIST_VIEW_BLOCKED_ALERT_MESSAGE_FORMAT" = "%@ wurde blockiert.";
|
||||
/* The title of the 'user blocked' alert. */
|
||||
"BLOCK_LIST_VIEW_BLOCKED_ALERT_TITLE" = "Benutzer blockiert";
|
||||
/* Alert title after unblocking a group or 1:1 chat. Embeds the {{conversation title}}. */
|
||||
"BLOCK_LIST_VIEW_UNBLOCKED_ALERT_TITLE_FORMAT" = "%@ wurde freigegeben.";
|
||||
/* Alert body after unblocking a group. */
|
||||
"BLOCK_LIST_VIEW_UNBLOCKED_GROUP_ALERT_BODY" = "Vorhandene Mitglieder können dich jetzt wieder zur Gruppe hinzufügen.";
|
||||
/* An explanation of the consequences of blocking another user. */
|
||||
"BLOCK_USER_BEHAVIOR_EXPLANATION" = "Blockierte Benutzer können dich nicht mehr anrufen oder dir Nachrichten senden.";
|
||||
/* Label for generic done button. */
|
||||
|
@ -600,3 +606,18 @@
|
|||
"system_mode_theme" = "System";
|
||||
"dark_mode_theme" = "Dark";
|
||||
"light_mode_theme" = "Light";
|
||||
"MESSAGE_REQUESTS_TITLE" = "Message Requests";
|
||||
"MESSAGE_REQUESTS_EMPTY_TEXT" = "No pending message requests";
|
||||
"MESSAGE_REQUESTS_CLEAR_ALL" = "Clear All";
|
||||
"MESSAGE_REQUESTS_CLEAR_ALL_CONFIRMATION_TITLE" = "Are you sure you want to clear all message requests?";
|
||||
"MESSAGE_REQUESTS_CLEAR_ALL_CONFIRMATION_ACTON" = "Clear";
|
||||
"MESSAGE_REQUESTS_DELETE_CONFIRMATION_ACTON" = "Are you sure you want to delete this message request?";
|
||||
"MESSAGE_REQUESTS_APPROVAL_ERROR_MESSAGE" = "An error occurred when trying to accept this message request";
|
||||
"MESSAGE_REQUESTS_INFO" = "Sending a message to this user will automatically accept their message request.";
|
||||
"MESSAGE_REQUESTS_ACCEPTED" = "Your message request has been accepted.";
|
||||
"MESSAGE_REQUESTS_NOTIFICATION" = "You have a new message request";
|
||||
"TXT_HIDE_TITLE" = "Hide";
|
||||
"TXT_DELETE_ACCEPT" = "Accept";
|
||||
"NEW_CONVERSATION_MENU_OPEN_GROUP" = "Open Group";
|
||||
"NEW_CONVERSATION_MENU_DIRECT_MESSAGE" = "Direct Message";
|
||||
"NEW_CONVERSATION_MENU_CLOSED_GROUP" = "Closed Group";
|
||||
|
|
|
@ -94,12 +94,18 @@
|
|||
"BLOCK_LIST_BLOCK_BUTTON" = "Block";
|
||||
/* A format for the 'block user' action sheet title. Embeds {{the blocked user's name or phone number}}. */
|
||||
"BLOCK_LIST_BLOCK_USER_TITLE_FORMAT" = "Block %@?";
|
||||
/* A format for the 'unblock user' action sheet title. Embeds {{the unblocked user's name or phone number}}. */
|
||||
"BLOCK_LIST_UNBLOCK_TITLE_FORMAT" = "Unblock %@?";
|
||||
/* Button label for the 'unblock' button */
|
||||
"BLOCK_LIST_UNBLOCK_BUTTON" = "Unblock";
|
||||
/* The message format of the 'conversation blocked' alert. Embeds the {{conversation title}}. */
|
||||
"BLOCK_LIST_VIEW_BLOCKED_ALERT_MESSAGE_FORMAT" = "%@ has been blocked.";
|
||||
/* The title of the 'user blocked' alert. */
|
||||
"BLOCK_LIST_VIEW_BLOCKED_ALERT_TITLE" = "User Blocked";
|
||||
/* Alert title after unblocking a group or 1:1 chat. Embeds the {{conversation title}}. */
|
||||
"BLOCK_LIST_VIEW_UNBLOCKED_ALERT_TITLE_FORMAT" = "%@ has been unblocked.";
|
||||
/* Alert body after unblocking a group. */
|
||||
"BLOCK_LIST_VIEW_UNBLOCKED_GROUP_ALERT_BODY" = "Existing members can now add you to the group again.";
|
||||
/* An explanation of the consequences of blocking another user. */
|
||||
"BLOCK_USER_BEHAVIOR_EXPLANATION" = "Blocked users will not be able to call you or send you messages.";
|
||||
/* Label for generic done button. */
|
||||
|
@ -626,3 +632,18 @@
|
|||
"SEARCH_SECTION_MESSAGES" = "Messages";
|
||||
"SEARCH_SECTION_RECENT" = "Recent";
|
||||
"RECENT_SEARCH_LAST_MESSAGE_DATETIME" = "last message: %@";
|
||||
"MESSAGE_REQUESTS_TITLE" = "Message Requests";
|
||||
"MESSAGE_REQUESTS_EMPTY_TEXT" = "No pending message requests";
|
||||
"MESSAGE_REQUESTS_CLEAR_ALL" = "Clear All";
|
||||
"MESSAGE_REQUESTS_CLEAR_ALL_CONFIRMATION_TITLE" = "Are you sure you want to clear all message requests?";
|
||||
"MESSAGE_REQUESTS_CLEAR_ALL_CONFIRMATION_ACTON" = "Clear";
|
||||
"MESSAGE_REQUESTS_DELETE_CONFIRMATION_ACTON" = "Are you sure you want to delete this message request?";
|
||||
"MESSAGE_REQUESTS_APPROVAL_ERROR_MESSAGE" = "An error occurred when trying to accept this message request";
|
||||
"MESSAGE_REQUESTS_INFO" = "Sending a message to this user will automatically accept their message request.";
|
||||
"MESSAGE_REQUESTS_ACCEPTED" = "Your message request has been accepted.";
|
||||
"MESSAGE_REQUESTS_NOTIFICATION" = "You have a new message request";
|
||||
"TXT_HIDE_TITLE" = "Hide";
|
||||
"TXT_DELETE_ACCEPT" = "Accept";
|
||||
"NEW_CONVERSATION_MENU_OPEN_GROUP" = "Open Group";
|
||||
"NEW_CONVERSATION_MENU_DIRECT_MESSAGE" = "Direct Message";
|
||||
"NEW_CONVERSATION_MENU_CLOSED_GROUP" = "Closed Group";
|
||||
|
|
|
@ -94,12 +94,18 @@
|
|||
"BLOCK_LIST_BLOCK_BUTTON" = "Bloquear";
|
||||
/* A format for the 'block user' action sheet title. Embeds {{the blocked user's name or phone number}}. */
|
||||
"BLOCK_LIST_BLOCK_USER_TITLE_FORMAT" = "¿Bloquear %@?";
|
||||
/* A format for the 'unblock conversation' action sheet title. Embeds the {{conversation title}}. */
|
||||
"BLOCK_LIST_UNBLOCK_TITLE_FORMAT" = "¿Desbloquear %@?";
|
||||
/* Button label for the 'unblock' button */
|
||||
"BLOCK_LIST_UNBLOCK_BUTTON" = "Desbloquear";
|
||||
/* The message format of the 'conversation blocked' alert. Embeds the {{conversation title}}. */
|
||||
"BLOCK_LIST_VIEW_BLOCKED_ALERT_MESSAGE_FORMAT" = "%@ ha sido bloqueado.";
|
||||
/* The title of the 'user blocked' alert. */
|
||||
"BLOCK_LIST_VIEW_BLOCKED_ALERT_TITLE" = "Usuario Bloqueado";
|
||||
/* Alert title after unblocking a group or 1:1 chat. Embeds the {{conversation title}}. */
|
||||
"BLOCK_LIST_VIEW_UNBLOCKED_ALERT_TITLE_FORMAT" = "%@ ha sido desbloqueado.";
|
||||
/* Alert body after unblocking a group. */
|
||||
"BLOCK_LIST_VIEW_UNBLOCKED_GROUP_ALERT_BODY" = "Los miembros pueden añadirte de nuevo al grupo.";
|
||||
/* An explanation of the consequences of blocking another user. */
|
||||
"BLOCK_USER_BEHAVIOR_EXPLANATION" = "Los contactos bloqueados no podrán llamarte ni enviarte mensajes.";
|
||||
/* Label for generic done button. */
|
||||
|
@ -600,3 +606,18 @@
|
|||
"system_mode_theme" = "System";
|
||||
"dark_mode_theme" = "Dark";
|
||||
"light_mode_theme" = "Light";
|
||||
"MESSAGE_REQUESTS_TITLE" = "Message Requests";
|
||||
"MESSAGE_REQUESTS_EMPTY_TEXT" = "No pending message requests";
|
||||
"MESSAGE_REQUESTS_CLEAR_ALL" = "Clear All";
|
||||
"MESSAGE_REQUESTS_CLEAR_ALL_CONFIRMATION_TITLE" = "Are you sure you want to clear all message requests?";
|
||||
"MESSAGE_REQUESTS_CLEAR_ALL_CONFIRMATION_ACTON" = "Clear";
|
||||
"MESSAGE_REQUESTS_DELETE_CONFIRMATION_ACTON" = "Are you sure you want to delete this message request?";
|
||||
"MESSAGE_REQUESTS_APPROVAL_ERROR_MESSAGE" = "An error occurred when trying to accept this message request";
|
||||
"MESSAGE_REQUESTS_INFO" = "Sending a message to this user will automatically accept their message request.";
|
||||
"MESSAGE_REQUESTS_ACCEPTED" = "Your message request has been accepted.";
|
||||
"MESSAGE_REQUESTS_NOTIFICATION" = "You have a new message request";
|
||||
"TXT_HIDE_TITLE" = "Hide";
|
||||
"TXT_DELETE_ACCEPT" = "Accept";
|
||||
"NEW_CONVERSATION_MENU_OPEN_GROUP" = "Open Group";
|
||||
"NEW_CONVERSATION_MENU_DIRECT_MESSAGE" = "Direct Message";
|
||||
"NEW_CONVERSATION_MENU_CLOSED_GROUP" = "Closed Group";
|
||||
|
|
|
@ -94,12 +94,18 @@
|
|||
"BLOCK_LIST_BLOCK_BUTTON" = "مسدود کردن";
|
||||
/* A format for the 'block user' action sheet title. Embeds {{the blocked user's name or phone number}}. */
|
||||
"BLOCK_LIST_BLOCK_USER_TITLE_FORMAT" = "%@ مسدود شود؟";
|
||||
/* A format for the 'unblock conversation' action sheet title. Embeds the {{conversation title}}. */
|
||||
"BLOCK_LIST_UNBLOCK_TITLE_FORMAT" = "%@از حالت مسدود خارج شود؟";
|
||||
/* Button label for the 'unblock' button */
|
||||
"BLOCK_LIST_UNBLOCK_BUTTON" = "رفع مسدودی";
|
||||
/* The message format of the 'conversation blocked' alert. Embeds the {{conversation title}}. */
|
||||
"BLOCK_LIST_VIEW_BLOCKED_ALERT_MESSAGE_FORMAT" = "%@ has been blocked.";
|
||||
/* The title of the 'user blocked' alert. */
|
||||
"BLOCK_LIST_VIEW_BLOCKED_ALERT_TITLE" = "User Blocked";
|
||||
"BLOCK_LIST_VIEW_BLOCKED_ALERT_TITLE" = "کاربر مسدود شده است";
|
||||
/* Alert title after unblocking a group or 1:1 chat. Embeds the {{conversation title}}. */
|
||||
"BLOCK_LIST_VIEW_UNBLOCKED_ALERT_TITLE_FORMAT" = "%@ از حالت مسدودی خارج شد.";
|
||||
/* Alert body after unblocking a group. */
|
||||
"BLOCK_LIST_VIEW_UNBLOCKED_GROUP_ALERT_BODY" = "اعضای موجود میتوانند شما را دوباره به گروه اضافه کنند.";
|
||||
/* An explanation of the consequences of blocking another user. */
|
||||
"BLOCK_USER_BEHAVIOR_EXPLANATION" = "کاربری که مسدود شده است، امکان تماس یا ارسال پیام به شما را ندارد.";
|
||||
/* Label for generic done button. */
|
||||
|
@ -600,3 +606,18 @@
|
|||
"system_mode_theme" = "System";
|
||||
"dark_mode_theme" = "Dark";
|
||||
"light_mode_theme" = "Light";
|
||||
"MESSAGE_REQUESTS_TITLE" = "Message Requests";
|
||||
"MESSAGE_REQUESTS_EMPTY_TEXT" = "No pending message requests";
|
||||
"MESSAGE_REQUESTS_CLEAR_ALL" = "Clear All";
|
||||
"MESSAGE_REQUESTS_CLEAR_ALL_CONFIRMATION_TITLE" = "Are you sure you want to clear all message requests?";
|
||||
"MESSAGE_REQUESTS_CLEAR_ALL_CONFIRMATION_ACTON" = "Clear";
|
||||
"MESSAGE_REQUESTS_DELETE_CONFIRMATION_ACTON" = "Are you sure you want to delete this message request?";
|
||||
"MESSAGE_REQUESTS_APPROVAL_ERROR_MESSAGE" = "An error occurred when trying to accept this message request";
|
||||
"MESSAGE_REQUESTS_INFO" = "Sending a message to this user will automatically accept their message request.";
|
||||
"MESSAGE_REQUESTS_ACCEPTED" = "Your message request has been accepted.";
|
||||
"MESSAGE_REQUESTS_NOTIFICATION" = "You have a new message request";
|
||||
"TXT_HIDE_TITLE" = "Hide";
|
||||
"TXT_DELETE_ACCEPT" = "Accept";
|
||||
"NEW_CONVERSATION_MENU_OPEN_GROUP" = "Open Group";
|
||||
"NEW_CONVERSATION_MENU_DIRECT_MESSAGE" = "Direct Message";
|
||||
"NEW_CONVERSATION_MENU_CLOSED_GROUP" = "Closed Group";
|
||||
|
|
|
@ -94,12 +94,18 @@
|
|||
"BLOCK_LIST_BLOCK_BUTTON" = "Estä";
|
||||
/* A format for the 'block user' action sheet title. Embeds {{the blocked user's name or phone number}}. */
|
||||
"BLOCK_LIST_BLOCK_USER_TITLE_FORMAT" = "Estä %@?";
|
||||
/* A format for the 'unblock conversation' action sheet title. Embeds the {{conversation title}}. */
|
||||
"BLOCK_LIST_UNBLOCK_TITLE_FORMAT" = "Poista esto yhteystiedolta %@?";
|
||||
/* Button label for the 'unblock' button */
|
||||
"BLOCK_LIST_UNBLOCK_BUTTON" = "Poista esto";
|
||||
/* The message format of the 'conversation blocked' alert. Embeds the {{conversation title}}. */
|
||||
"BLOCK_LIST_VIEW_BLOCKED_ALERT_MESSAGE_FORMAT" = "%@ on estetty.";
|
||||
/* The title of the 'user blocked' alert. */
|
||||
"BLOCK_LIST_VIEW_BLOCKED_ALERT_TITLE" = "Käyttäjä estetty";
|
||||
/* Alert title after unblocking a group or 1:1 chat. Embeds the {{conversation title}}. */
|
||||
"BLOCK_LIST_VIEW_UNBLOCKED_ALERT_TITLE_FORMAT" = "%@ esto on poistettu.";
|
||||
/* Alert body after unblocking a group. */
|
||||
"BLOCK_LIST_VIEW_UNBLOCKED_GROUP_ALERT_BODY" = "Ryhmän jäsenet voivat nyt lisätä sinut takaisin ryhmään.";
|
||||
/* An explanation of the consequences of blocking another user. */
|
||||
"BLOCK_USER_BEHAVIOR_EXPLANATION" = "Estetyt käyttäjät eivät voi soittaa sinulle tai lähettää sinulle viestejä.";
|
||||
/* Label for generic done button. */
|
||||
|
@ -600,3 +606,18 @@
|
|||
"system_mode_theme" = "System";
|
||||
"dark_mode_theme" = "Dark";
|
||||
"light_mode_theme" = "Light";
|
||||
"MESSAGE_REQUESTS_TITLE" = "Message Requests";
|
||||
"MESSAGE_REQUESTS_EMPTY_TEXT" = "No pending message requests";
|
||||
"MESSAGE_REQUESTS_CLEAR_ALL" = "Clear All";
|
||||
"MESSAGE_REQUESTS_CLEAR_ALL_CONFIRMATION_TITLE" = "Are you sure you want to clear all message requests?";
|
||||
"MESSAGE_REQUESTS_CLEAR_ALL_CONFIRMATION_ACTON" = "Clear";
|
||||
"MESSAGE_REQUESTS_DELETE_CONFIRMATION_ACTON" = "Are you sure you want to delete this message request?";
|
||||
"MESSAGE_REQUESTS_APPROVAL_ERROR_MESSAGE" = "An error occurred when trying to accept this message request";
|
||||
"MESSAGE_REQUESTS_INFO" = "Sending a message to this user will automatically accept their message request.";
|
||||
"MESSAGE_REQUESTS_ACCEPTED" = "Your message request has been accepted.";
|
||||
"MESSAGE_REQUESTS_NOTIFICATION" = "You have a new message request";
|
||||
"TXT_HIDE_TITLE" = "Hide";
|
||||
"TXT_DELETE_ACCEPT" = "Accept";
|
||||
"NEW_CONVERSATION_MENU_OPEN_GROUP" = "Open Group";
|
||||
"NEW_CONVERSATION_MENU_DIRECT_MESSAGE" = "Direct Message";
|
||||
"NEW_CONVERSATION_MENU_CLOSED_GROUP" = "Closed Group";
|
||||
|
|
|
@ -94,12 +94,18 @@
|
|||
"BLOCK_LIST_BLOCK_BUTTON" = "Bloquer";
|
||||
/* A format for the 'block user' action sheet title. Embeds {{the blocked user's name or phone number}}. */
|
||||
"BLOCK_LIST_BLOCK_USER_TITLE_FORMAT" = "Bloquer %@ ?";
|
||||
/* A format for the 'unblock conversation' action sheet title. Embeds the {{conversation title}}. */
|
||||
"BLOCK_LIST_UNBLOCK_TITLE_FORMAT" = "Débloquer %@ ?";
|
||||
/* Button label for the 'unblock' button */
|
||||
"BLOCK_LIST_UNBLOCK_BUTTON" = "Débloquer";
|
||||
/* The message format of the 'conversation blocked' alert. Embeds the {{conversation title}}. */
|
||||
"BLOCK_LIST_VIEW_BLOCKED_ALERT_MESSAGE_FORMAT" = "%@ a été bloqué.";
|
||||
/* The title of the 'user blocked' alert. */
|
||||
"BLOCK_LIST_VIEW_BLOCKED_ALERT_TITLE" = "Utilisateur Bloqué";
|
||||
/* Alert title after unblocking a group or 1:1 chat. Embeds the {{conversation title}}. */
|
||||
"BLOCK_LIST_VIEW_UNBLOCKED_ALERT_TITLE_FORMAT" = "%@ a été débloqué.";
|
||||
/* Alert body after unblocking a group. */
|
||||
"BLOCK_LIST_VIEW_UNBLOCKED_GROUP_ALERT_BODY" = "Les membres actuels peuvent désormais vous ajouter au groupe de nouveau.";
|
||||
/* An explanation of the consequences of blocking another user. */
|
||||
"BLOCK_USER_BEHAVIOR_EXPLANATION" = "Les utilisateurs bloqués ne pourront ni vous appeler ni vous envoyer des messages.";
|
||||
/* Label for generic done button. */
|
||||
|
@ -600,3 +606,18 @@
|
|||
"system_mode_theme" = "System";
|
||||
"dark_mode_theme" = "Dark";
|
||||
"light_mode_theme" = "Light";
|
||||
"MESSAGE_REQUESTS_TITLE" = "Message Requests";
|
||||
"MESSAGE_REQUESTS_EMPTY_TEXT" = "No pending message requests";
|
||||
"MESSAGE_REQUESTS_CLEAR_ALL" = "Clear All";
|
||||
"MESSAGE_REQUESTS_CLEAR_ALL_CONFIRMATION_TITLE" = "Are you sure you want to clear all message requests?";
|
||||
"MESSAGE_REQUESTS_CLEAR_ALL_CONFIRMATION_ACTON" = "Clear";
|
||||
"MESSAGE_REQUESTS_DELETE_CONFIRMATION_ACTON" = "Are you sure you want to delete this message request?";
|
||||
"MESSAGE_REQUESTS_APPROVAL_ERROR_MESSAGE" = "An error occurred when trying to accept this message request";
|
||||
"MESSAGE_REQUESTS_INFO" = "Sending a message to this user will automatically accept their message request.";
|
||||
"MESSAGE_REQUESTS_ACCEPTED" = "Your message request has been accepted.";
|
||||
"MESSAGE_REQUESTS_NOTIFICATION" = "You have a new message request";
|
||||
"TXT_HIDE_TITLE" = "Hide";
|
||||
"TXT_DELETE_ACCEPT" = "Accept";
|
||||
"NEW_CONVERSATION_MENU_OPEN_GROUP" = "Open Group";
|
||||
"NEW_CONVERSATION_MENU_DIRECT_MESSAGE" = "Direct Message";
|
||||
"NEW_CONVERSATION_MENU_CLOSED_GROUP" = "Closed Group";
|
||||
|
|
|
@ -94,12 +94,18 @@
|
|||
"BLOCK_LIST_BLOCK_BUTTON" = "ब्लॉक";
|
||||
/* A format for the 'block user' action sheet title. Embeds {{the blocked user's name or phone number}}. */
|
||||
"BLOCK_LIST_BLOCK_USER_TITLE_FORMAT" = "%@ को ब्लॉक करें?";
|
||||
/* A format for the 'unblock user' action sheet title. Embeds {{the unblocked user's name or phone number}}. */
|
||||
"BLOCK_LIST_UNBLOCK_TITLE_FORMAT" = "Unblock %@?";
|
||||
/* Button label for the 'unblock' button */
|
||||
"BLOCK_LIST_UNBLOCK_BUTTON" = "अनब्लॉक करें";
|
||||
/* The message format of the 'conversation blocked' alert. Embeds the {{conversation title}}. */
|
||||
"BLOCK_LIST_VIEW_BLOCKED_ALERT_MESSAGE_FORMAT" = "%@ को ब्लॉक कर दिया गया है";
|
||||
/* The title of the 'user blocked' alert. */
|
||||
"BLOCK_LIST_VIEW_BLOCKED_ALERT_TITLE" = "यूजर ब्लॉक किया हुआ है";
|
||||
/* Alert title after unblocking a group or 1:1 chat. Embeds the {{conversation title}}. */
|
||||
"BLOCK_LIST_VIEW_UNBLOCKED_ALERT_TITLE_FORMAT" = "%@ has been unblocked.";
|
||||
/* Alert body after unblocking a group. */
|
||||
"BLOCK_LIST_VIEW_UNBLOCKED_GROUP_ALERT_BODY" = "Existing members can now add you to the group again.";
|
||||
/* An explanation of the consequences of blocking another user. */
|
||||
"BLOCK_USER_BEHAVIOR_EXPLANATION" = "अवरुद्ध उपयोगकर्ता आपको कॉल नहीं कर पाएंगे या आपको संदेश नहीं भेज पाएंगे।";
|
||||
/* Label for generic done button. */
|
||||
|
@ -600,3 +606,18 @@
|
|||
"system_mode_theme" = "System";
|
||||
"dark_mode_theme" = "Dark";
|
||||
"light_mode_theme" = "Light";
|
||||
"MESSAGE_REQUESTS_TITLE" = "Message Requests";
|
||||
"MESSAGE_REQUESTS_EMPTY_TEXT" = "No pending message requests";
|
||||
"MESSAGE_REQUESTS_CLEAR_ALL" = "Clear All";
|
||||
"MESSAGE_REQUESTS_CLEAR_ALL_CONFIRMATION_TITLE" = "Are you sure you want to clear all message requests?";
|
||||
"MESSAGE_REQUESTS_CLEAR_ALL_CONFIRMATION_ACTON" = "Clear";
|
||||
"MESSAGE_REQUESTS_DELETE_CONFIRMATION_ACTON" = "Are you sure you want to delete this message request?";
|
||||
"MESSAGE_REQUESTS_APPROVAL_ERROR_MESSAGE" = "An error occurred when trying to accept this message request";
|
||||
"MESSAGE_REQUESTS_INFO" = "Sending a message to this user will automatically accept their message request.";
|
||||
"MESSAGE_REQUESTS_ACCEPTED" = "Your message request has been accepted.";
|
||||
"MESSAGE_REQUESTS_NOTIFICATION" = "You have a new message request";
|
||||
"TXT_HIDE_TITLE" = "Hide";
|
||||
"TXT_DELETE_ACCEPT" = "Accept";
|
||||
"NEW_CONVERSATION_MENU_OPEN_GROUP" = "Open Group";
|
||||
"NEW_CONVERSATION_MENU_DIRECT_MESSAGE" = "Direct Message";
|
||||
"NEW_CONVERSATION_MENU_CLOSED_GROUP" = "Closed Group";
|
||||
|
|
|
@ -94,12 +94,18 @@
|
|||
"BLOCK_LIST_BLOCK_BUTTON" = "Blokiraj";
|
||||
/* A format for the 'block user' action sheet title. Embeds {{the blocked user's name or phone number}}. */
|
||||
"BLOCK_LIST_BLOCK_USER_TITLE_FORMAT" = "Blokiraj %@?";
|
||||
/* A format for the 'unblock conversation' action sheet title. Embeds the {{conversation title}}. */
|
||||
"BLOCK_LIST_UNBLOCK_TITLE_FORMAT" = "Deblokiraj %@?";
|
||||
/* Button label for the 'unblock' button */
|
||||
"BLOCK_LIST_UNBLOCK_BUTTON" = "Deblokiraj";
|
||||
/* The message format of the 'conversation blocked' alert. Embeds the {{conversation title}}. */
|
||||
"BLOCK_LIST_VIEW_BLOCKED_ALERT_MESSAGE_FORMAT" = "%@ je blokiran.";
|
||||
/* The title of the 'user blocked' alert. */
|
||||
"BLOCK_LIST_VIEW_BLOCKED_ALERT_TITLE" = "Korisnik blokiran";
|
||||
/* Alert title after unblocking a group or 1:1 chat. Embeds the {{conversation title}}. */
|
||||
"BLOCK_LIST_VIEW_UNBLOCKED_ALERT_TITLE_FORMAT" = "%@ je deblokiran.";
|
||||
/* Alert body after unblocking a group. */
|
||||
"BLOCK_LIST_VIEW_UNBLOCKED_GROUP_ALERT_BODY" = "Sadašnji članovi sada vas mogu dodati u grupu.";
|
||||
/* An explanation of the consequences of blocking another user. */
|
||||
"BLOCK_USER_BEHAVIOR_EXPLANATION" = "Blokirani korisnici neće vas moći nazvati niti poslati poruke.";
|
||||
/* Label for generic done button. */
|
||||
|
@ -600,3 +606,18 @@
|
|||
"system_mode_theme" = "System";
|
||||
"dark_mode_theme" = "Dark";
|
||||
"light_mode_theme" = "Light";
|
||||
"MESSAGE_REQUESTS_TITLE" = "Message Requests";
|
||||
"MESSAGE_REQUESTS_EMPTY_TEXT" = "No pending message requests";
|
||||
"MESSAGE_REQUESTS_CLEAR_ALL" = "Clear All";
|
||||
"MESSAGE_REQUESTS_CLEAR_ALL_CONFIRMATION_TITLE" = "Are you sure you want to clear all message requests?";
|
||||
"MESSAGE_REQUESTS_CLEAR_ALL_CONFIRMATION_ACTON" = "Clear";
|
||||
"MESSAGE_REQUESTS_DELETE_CONFIRMATION_ACTON" = "Are you sure you want to delete this message request?";
|
||||
"MESSAGE_REQUESTS_APPROVAL_ERROR_MESSAGE" = "An error occurred when trying to accept this message request";
|
||||
"MESSAGE_REQUESTS_INFO" = "Sending a message to this user will automatically accept their message request.";
|
||||
"MESSAGE_REQUESTS_ACCEPTED" = "Your message request has been accepted.";
|
||||
"MESSAGE_REQUESTS_NOTIFICATION" = "You have a new message request";
|
||||
"TXT_HIDE_TITLE" = "Hide";
|
||||
"TXT_DELETE_ACCEPT" = "Accept";
|
||||
"NEW_CONVERSATION_MENU_OPEN_GROUP" = "Open Group";
|
||||
"NEW_CONVERSATION_MENU_DIRECT_MESSAGE" = "Direct Message";
|
||||
"NEW_CONVERSATION_MENU_CLOSED_GROUP" = "Closed Group";
|
||||
|
|
|
@ -94,12 +94,18 @@
|
|||
"BLOCK_LIST_BLOCK_BUTTON" = "Blokir";
|
||||
/* A format for the 'block user' action sheet title. Embeds {{the blocked user's name or phone number}}. */
|
||||
"BLOCK_LIST_BLOCK_USER_TITLE_FORMAT" = "Blokir %@?";
|
||||
/* A format for the 'unblock conversation' action sheet title. Embeds the {{conversation title}}. */
|
||||
"BLOCK_LIST_UNBLOCK_TITLE_FORMAT" = "Buka blokir %@?";
|
||||
/* Button label for the 'unblock' button */
|
||||
"BLOCK_LIST_UNBLOCK_BUTTON" = "Buka blokir";
|
||||
/* The message format of the 'conversation blocked' alert. Embeds the {{conversation title}}. */
|
||||
"BLOCK_LIST_VIEW_BLOCKED_ALERT_MESSAGE_FORMAT" = "%@ telah diblokir.";
|
||||
/* The title of the 'user blocked' alert. */
|
||||
"BLOCK_LIST_VIEW_BLOCKED_ALERT_TITLE" = "Pengguna diblokir";
|
||||
/* Alert title after unblocking a group or 1:1 chat. Embeds the {{conversation title}}. */
|
||||
"BLOCK_LIST_VIEW_UNBLOCKED_ALERT_TITLE_FORMAT" = "%@ telah dibuka blokirnya";
|
||||
/* Alert body after unblocking a group. */
|
||||
"BLOCK_LIST_VIEW_UNBLOCKED_GROUP_ALERT_BODY" = "Existing members can now add you to the group again.";
|
||||
/* An explanation of the consequences of blocking another user. */
|
||||
"BLOCK_USER_BEHAVIOR_EXPLANATION" = "Pengguna terblokir tidak bisa menghubungi atau mengirimkan pesan kepada Anda.";
|
||||
/* Label for generic done button. */
|
||||
|
@ -600,3 +606,18 @@
|
|||
"system_mode_theme" = "System";
|
||||
"dark_mode_theme" = "Dark";
|
||||
"light_mode_theme" = "Light";
|
||||
"MESSAGE_REQUESTS_TITLE" = "Message Requests";
|
||||
"MESSAGE_REQUESTS_EMPTY_TEXT" = "No pending message requests";
|
||||
"MESSAGE_REQUESTS_CLEAR_ALL" = "Clear All";
|
||||
"MESSAGE_REQUESTS_CLEAR_ALL_CONFIRMATION_TITLE" = "Are you sure you want to clear all message requests?";
|
||||
"MESSAGE_REQUESTS_CLEAR_ALL_CONFIRMATION_ACTON" = "Clear";
|
||||
"MESSAGE_REQUESTS_DELETE_CONFIRMATION_ACTON" = "Are you sure you want to delete this message request?";
|
||||
"MESSAGE_REQUESTS_APPROVAL_ERROR_MESSAGE" = "An error occurred when trying to accept this message request";
|
||||
"MESSAGE_REQUESTS_INFO" = "Sending a message to this user will automatically accept their message request.";
|
||||
"MESSAGE_REQUESTS_ACCEPTED" = "Your message request has been accepted.";
|
||||
"MESSAGE_REQUESTS_NOTIFICATION" = "You have a new message request";
|
||||
"TXT_HIDE_TITLE" = "Hide";
|
||||
"TXT_DELETE_ACCEPT" = "Accept";
|
||||
"NEW_CONVERSATION_MENU_OPEN_GROUP" = "Open Group";
|
||||
"NEW_CONVERSATION_MENU_DIRECT_MESSAGE" = "Direct Message";
|
||||
"NEW_CONVERSATION_MENU_CLOSED_GROUP" = "Closed Group";
|
||||
|
|
|
@ -94,12 +94,18 @@
|
|||
"BLOCK_LIST_BLOCK_BUTTON" = "Blocca";
|
||||
/* A format for the 'block user' action sheet title. Embeds {{the blocked user's name or phone number}}. */
|
||||
"BLOCK_LIST_BLOCK_USER_TITLE_FORMAT" = "Bloccare %@?";
|
||||
/* A format for the 'unblock conversation' action sheet title. Embeds the {{conversation title}}. */
|
||||
"BLOCK_LIST_UNBLOCK_TITLE_FORMAT" = "Sbloccare %@?";
|
||||
/* Button label for the 'unblock' button */
|
||||
"BLOCK_LIST_UNBLOCK_BUTTON" = "Sblocca";
|
||||
/* The message format of the 'conversation blocked' alert. Embeds the {{conversation title}}. */
|
||||
"BLOCK_LIST_VIEW_BLOCKED_ALERT_MESSAGE_FORMAT" = "%@ è stato bloccato.";
|
||||
/* The title of the 'user blocked' alert. */
|
||||
"BLOCK_LIST_VIEW_BLOCKED_ALERT_TITLE" = "Utente bloccato";
|
||||
/* Alert title after unblocking a group or 1:1 chat. Embeds the {{conversation title}}. */
|
||||
"BLOCK_LIST_VIEW_UNBLOCKED_ALERT_TITLE_FORMAT" = "%@ buvo atblokuota(-as).";
|
||||
/* Alert body after unblocking a group. */
|
||||
"BLOCK_LIST_VIEW_UNBLOCKED_GROUP_ALERT_BODY" = "Dabar, esami dalyviai gali ir vėl pridėti jus į grupę.";
|
||||
/* An explanation of the consequences of blocking another user. */
|
||||
"BLOCK_USER_BEHAVIOR_EXPLANATION" = "Gli utenti bloccati non potranno chiamarti o inviarti messaggi.";
|
||||
/* Label for generic done button. */
|
||||
|
@ -600,3 +606,18 @@
|
|||
"system_mode_theme" = "System";
|
||||
"dark_mode_theme" = "Dark";
|
||||
"light_mode_theme" = "Light";
|
||||
"MESSAGE_REQUESTS_TITLE" = "Message Requests";
|
||||
"MESSAGE_REQUESTS_EMPTY_TEXT" = "No pending message requests";
|
||||
"MESSAGE_REQUESTS_CLEAR_ALL" = "Clear All";
|
||||
"MESSAGE_REQUESTS_CLEAR_ALL_CONFIRMATION_TITLE" = "Are you sure you want to clear all message requests?";
|
||||
"MESSAGE_REQUESTS_CLEAR_ALL_CONFIRMATION_ACTON" = "Clear";
|
||||
"MESSAGE_REQUESTS_DELETE_CONFIRMATION_ACTON" = "Are you sure you want to delete this message request?";
|
||||
"MESSAGE_REQUESTS_APPROVAL_ERROR_MESSAGE" = "An error occurred when trying to accept this message request";
|
||||
"MESSAGE_REQUESTS_INFO" = "Sending a message to this user will automatically accept their message request.";
|
||||
"MESSAGE_REQUESTS_ACCEPTED" = "Your message request has been accepted.";
|
||||
"MESSAGE_REQUESTS_NOTIFICATION" = "You have a new message request";
|
||||
"TXT_HIDE_TITLE" = "Hide";
|
||||
"TXT_DELETE_ACCEPT" = "Accept";
|
||||
"NEW_CONVERSATION_MENU_OPEN_GROUP" = "Open Group";
|
||||
"NEW_CONVERSATION_MENU_DIRECT_MESSAGE" = "Direct Message";
|
||||
"NEW_CONVERSATION_MENU_CLOSED_GROUP" = "Closed Group";
|
||||
|
|
|
@ -94,12 +94,18 @@
|
|||
"BLOCK_LIST_BLOCK_BUTTON" = "ブロックする";
|
||||
/* A format for the 'block user' action sheet title. Embeds {{the blocked user's name or phone number}}. */
|
||||
"BLOCK_LIST_BLOCK_USER_TITLE_FORMAT" = "%@ をブロックしますか?";
|
||||
/* A format for the 'unblock conversation' action sheet title. Embeds the {{conversation title}}. */
|
||||
"BLOCK_LIST_UNBLOCK_TITLE_FORMAT" = "%@のブロックを解除しますか?";
|
||||
/* Button label for the 'unblock' button */
|
||||
"BLOCK_LIST_UNBLOCK_BUTTON" = "ブロックを解除する";
|
||||
/* The message format of the 'conversation blocked' alert. Embeds the {{conversation title}}. */
|
||||
"BLOCK_LIST_VIEW_BLOCKED_ALERT_MESSAGE_FORMAT" = "%@はブロックされました。";
|
||||
/* The title of the 'user blocked' alert. */
|
||||
"BLOCK_LIST_VIEW_BLOCKED_ALERT_TITLE" = "ユーザがブロックされました";
|
||||
/* Alert title after unblocking a group or 1:1 chat. Embeds the {{conversation title}}. */
|
||||
"BLOCK_LIST_VIEW_UNBLOCKED_ALERT_TITLE_FORMAT" = "%@ のブロックは解除されています。";
|
||||
/* Alert body after unblocking a group. */
|
||||
"BLOCK_LIST_VIEW_UNBLOCKED_GROUP_ALERT_BODY" = "既存のメンバーは、あなたをグループに再加入させることができます。";
|
||||
/* An explanation of the consequences of blocking another user. */
|
||||
"BLOCK_USER_BEHAVIOR_EXPLANATION" = "ブロックされたユーザは、あなたにメッセージや通話を発信することができなくなります。";
|
||||
/* Label for generic done button. */
|
||||
|
@ -600,3 +606,18 @@
|
|||
"system_mode_theme" = "System";
|
||||
"dark_mode_theme" = "Dark";
|
||||
"light_mode_theme" = "Light";
|
||||
"MESSAGE_REQUESTS_TITLE" = "Message Requests";
|
||||
"MESSAGE_REQUESTS_EMPTY_TEXT" = "No pending message requests";
|
||||
"MESSAGE_REQUESTS_CLEAR_ALL" = "Clear All";
|
||||
"MESSAGE_REQUESTS_CLEAR_ALL_CONFIRMATION_TITLE" = "Are you sure you want to clear all message requests?";
|
||||
"MESSAGE_REQUESTS_CLEAR_ALL_CONFIRMATION_ACTON" = "Clear";
|
||||
"MESSAGE_REQUESTS_DELETE_CONFIRMATION_ACTON" = "Are you sure you want to delete this message request?";
|
||||
"MESSAGE_REQUESTS_APPROVAL_ERROR_MESSAGE" = "An error occurred when trying to accept this message request";
|
||||
"MESSAGE_REQUESTS_INFO" = "Sending a message to this user will automatically accept their message request.";
|
||||
"MESSAGE_REQUESTS_ACCEPTED" = "Your message request has been accepted.";
|
||||
"MESSAGE_REQUESTS_NOTIFICATION" = "You have a new message request";
|
||||
"TXT_HIDE_TITLE" = "Hide";
|
||||
"TXT_DELETE_ACCEPT" = "Accept";
|
||||
"NEW_CONVERSATION_MENU_OPEN_GROUP" = "Open Group";
|
||||
"NEW_CONVERSATION_MENU_DIRECT_MESSAGE" = "Direct Message";
|
||||
"NEW_CONVERSATION_MENU_CLOSED_GROUP" = "Closed Group";
|
||||
|
|
|
@ -94,12 +94,18 @@
|
|||
"BLOCK_LIST_BLOCK_BUTTON" = "Blokkeren";
|
||||
/* A format for the 'block user' action sheet title. Embeds {{the blocked user's name or phone number}}. */
|
||||
"BLOCK_LIST_BLOCK_USER_TITLE_FORMAT" = "%@ blokkeren?";
|
||||
/* A format for the 'unblock conversation' action sheet title. Embeds the {{conversation title}}. */
|
||||
"BLOCK_LIST_UNBLOCK_TITLE_FORMAT" = "%@ deblokkeren?";
|
||||
/* Button label for the 'unblock' button */
|
||||
"BLOCK_LIST_UNBLOCK_BUTTON" = "Deblokkeren";
|
||||
/* The message format of the 'conversation blocked' alert. Embeds the {{conversation title}}. */
|
||||
"BLOCK_LIST_VIEW_BLOCKED_ALERT_MESSAGE_FORMAT" = "%@ is geblokkeerd.";
|
||||
/* The title of the 'user blocked' alert. */
|
||||
"BLOCK_LIST_VIEW_BLOCKED_ALERT_TITLE" = "Gebruiker Geblokkeerd";
|
||||
/* Alert title after unblocking a group or 1:1 chat. Embeds the {{conversation title}}. */
|
||||
"BLOCK_LIST_VIEW_UNBLOCKED_ALERT_TITLE_FORMAT" = "%@ is gedeblokkeerd.";
|
||||
/* Alert body after unblocking a group. */
|
||||
"BLOCK_LIST_VIEW_UNBLOCKED_GROUP_ALERT_BODY" = "Bestaande leden kunnen je nu opnieuw toevoegen aan de groep.";
|
||||
/* An explanation of the consequences of blocking another user. */
|
||||
"BLOCK_USER_BEHAVIOR_EXPLANATION" = "Geblokkeerde gebruikers zijn niet in staat om u te bellen of berichten te sturen.";
|
||||
/* Label for generic done button. */
|
||||
|
@ -600,3 +606,18 @@
|
|||
"system_mode_theme" = "System";
|
||||
"dark_mode_theme" = "Dark";
|
||||
"light_mode_theme" = "Light";
|
||||
"MESSAGE_REQUESTS_TITLE" = "Message Requests";
|
||||
"MESSAGE_REQUESTS_EMPTY_TEXT" = "No pending message requests";
|
||||
"MESSAGE_REQUESTS_CLEAR_ALL" = "Clear All";
|
||||
"MESSAGE_REQUESTS_CLEAR_ALL_CONFIRMATION_TITLE" = "Are you sure you want to clear all message requests?";
|
||||
"MESSAGE_REQUESTS_CLEAR_ALL_CONFIRMATION_ACTON" = "Clear";
|
||||
"MESSAGE_REQUESTS_DELETE_CONFIRMATION_ACTON" = "Are you sure you want to delete this message request?";
|
||||
"MESSAGE_REQUESTS_APPROVAL_ERROR_MESSAGE" = "An error occurred when trying to accept this message request";
|
||||
"MESSAGE_REQUESTS_INFO" = "Sending a message to this user will automatically accept their message request.";
|
||||
"MESSAGE_REQUESTS_ACCEPTED" = "Your message request has been accepted.";
|
||||
"MESSAGE_REQUESTS_NOTIFICATION" = "You have a new message request";
|
||||
"TXT_HIDE_TITLE" = "Hide";
|
||||
"TXT_DELETE_ACCEPT" = "Accept";
|
||||
"NEW_CONVERSATION_MENU_OPEN_GROUP" = "Open Group";
|
||||
"NEW_CONVERSATION_MENU_DIRECT_MESSAGE" = "Direct Message";
|
||||
"NEW_CONVERSATION_MENU_CLOSED_GROUP" = "Closed Group";
|
||||
|
|
|
@ -94,12 +94,18 @@
|
|||
"BLOCK_LIST_BLOCK_BUTTON" = "Zablokuj";
|
||||
/* A format for the 'block user' action sheet title. Embeds {{the blocked user's name or phone number}}. */
|
||||
"BLOCK_LIST_BLOCK_USER_TITLE_FORMAT" = "Zablokować %@?";
|
||||
/* A format for the 'unblock conversation' action sheet title. Embeds the {{conversation title}}. */
|
||||
"BLOCK_LIST_UNBLOCK_TITLE_FORMAT" = "Odblokować %@?";
|
||||
/* Button label for the 'unblock' button */
|
||||
"BLOCK_LIST_UNBLOCK_BUTTON" = "Odblokuj";
|
||||
/* The message format of the 'conversation blocked' alert. Embeds the {{conversation title}}. */
|
||||
"BLOCK_LIST_VIEW_BLOCKED_ALERT_MESSAGE_FORMAT" = "%@ został zablokowany.";
|
||||
/* The title of the 'user blocked' alert. */
|
||||
"BLOCK_LIST_VIEW_BLOCKED_ALERT_TITLE" = "Użytkownik zablokowany";
|
||||
/* Alert title after unblocking a group or 1:1 chat. Embeds the {{conversation title}}. */
|
||||
"BLOCK_LIST_VIEW_UNBLOCKED_ALERT_TITLE_FORMAT" = "Odblokowano %@.";
|
||||
/* Alert body after unblocking a group. */
|
||||
"BLOCK_LIST_VIEW_UNBLOCKED_GROUP_ALERT_BODY" = "Istniejący członkowie mogą teraz ponownie dodać Cię do grupy.";
|
||||
/* An explanation of the consequences of blocking another user. */
|
||||
"BLOCK_USER_BEHAVIOR_EXPLANATION" = "Zablokowani użytkownicy nie będą mogli do Ciebie dzwonić ani wysyłać Ci wiadomości.";
|
||||
/* Label for generic done button. */
|
||||
|
@ -600,3 +606,18 @@
|
|||
"system_mode_theme" = "System";
|
||||
"dark_mode_theme" = "Dark";
|
||||
"light_mode_theme" = "Light";
|
||||
"MESSAGE_REQUESTS_TITLE" = "Message Requests";
|
||||
"MESSAGE_REQUESTS_EMPTY_TEXT" = "No pending message requests";
|
||||
"MESSAGE_REQUESTS_CLEAR_ALL" = "Clear All";
|
||||
"MESSAGE_REQUESTS_CLEAR_ALL_CONFIRMATION_TITLE" = "Are you sure you want to clear all message requests?";
|
||||
"MESSAGE_REQUESTS_CLEAR_ALL_CONFIRMATION_ACTON" = "Clear";
|
||||
"MESSAGE_REQUESTS_DELETE_CONFIRMATION_ACTON" = "Are you sure you want to delete this message request?";
|
||||
"MESSAGE_REQUESTS_APPROVAL_ERROR_MESSAGE" = "An error occurred when trying to accept this message request";
|
||||
"MESSAGE_REQUESTS_INFO" = "Sending a message to this user will automatically accept their message request.";
|
||||
"MESSAGE_REQUESTS_ACCEPTED" = "Your message request has been accepted.";
|
||||
"MESSAGE_REQUESTS_NOTIFICATION" = "You have a new message request";
|
||||
"TXT_HIDE_TITLE" = "Hide";
|
||||
"TXT_DELETE_ACCEPT" = "Accept";
|
||||
"NEW_CONVERSATION_MENU_OPEN_GROUP" = "Open Group";
|
||||
"NEW_CONVERSATION_MENU_DIRECT_MESSAGE" = "Direct Message";
|
||||
"NEW_CONVERSATION_MENU_CLOSED_GROUP" = "Closed Group";
|
||||
|
|
|
@ -94,12 +94,18 @@
|
|||
"BLOCK_LIST_BLOCK_BUTTON" = "Bloquear";
|
||||
/* A format for the 'block user' action sheet title. Embeds {{the blocked user's name or phone number}}. */
|
||||
"BLOCK_LIST_BLOCK_USER_TITLE_FORMAT" = "Bloquear %@?";
|
||||
/* A format for the 'unblock conversation' action sheet title. Embeds the {{conversation title}}. */
|
||||
"BLOCK_LIST_UNBLOCK_TITLE_FORMAT" = "Desbloquear %@?";
|
||||
/* Button label for the 'unblock' button */
|
||||
"BLOCK_LIST_UNBLOCK_BUTTON" = "Desbloquear";
|
||||
/* The message format of the 'conversation blocked' alert. Embeds the {{conversation title}}. */
|
||||
"BLOCK_LIST_VIEW_BLOCKED_ALERT_MESSAGE_FORMAT" = "%@ foi bloqueado.";
|
||||
/* The title of the 'user blocked' alert. */
|
||||
"BLOCK_LIST_VIEW_BLOCKED_ALERT_TITLE" = "Usuário Bloqueado";
|
||||
/* Alert title after unblocking a group or 1:1 chat. Embeds the {{conversation title}}. */
|
||||
"BLOCK_LIST_VIEW_UNBLOCKED_ALERT_TITLE_FORMAT" = "Você desbloqueou %@.";
|
||||
/* Alert body after unblocking a group. */
|
||||
"BLOCK_LIST_VIEW_UNBLOCKED_GROUP_ALERT_BODY" = "Membros existentes podem te adicionar ap grupo novamente.";
|
||||
/* An explanation of the consequences of blocking another user. */
|
||||
"BLOCK_USER_BEHAVIOR_EXPLANATION" = "Você não receberá mais ligações e mensagens de quem bloquear.";
|
||||
/* Label for generic done button. */
|
||||
|
@ -600,3 +606,18 @@
|
|||
"system_mode_theme" = "System";
|
||||
"dark_mode_theme" = "Dark";
|
||||
"light_mode_theme" = "Light";
|
||||
"MESSAGE_REQUESTS_TITLE" = "Message Requests";
|
||||
"MESSAGE_REQUESTS_EMPTY_TEXT" = "No pending message requests";
|
||||
"MESSAGE_REQUESTS_CLEAR_ALL" = "Clear All";
|
||||
"MESSAGE_REQUESTS_CLEAR_ALL_CONFIRMATION_TITLE" = "Are you sure you want to clear all message requests?";
|
||||
"MESSAGE_REQUESTS_CLEAR_ALL_CONFIRMATION_ACTON" = "Clear";
|
||||
"MESSAGE_REQUESTS_DELETE_CONFIRMATION_ACTON" = "Are you sure you want to delete this message request?";
|
||||
"MESSAGE_REQUESTS_APPROVAL_ERROR_MESSAGE" = "An error occurred when trying to accept this message request";
|
||||
"MESSAGE_REQUESTS_INFO" = "Sending a message to this user will automatically accept their message request.";
|
||||
"MESSAGE_REQUESTS_ACCEPTED" = "Your message request has been accepted.";
|
||||
"MESSAGE_REQUESTS_NOTIFICATION" = "You have a new message request";
|
||||
"TXT_HIDE_TITLE" = "Hide";
|
||||
"TXT_DELETE_ACCEPT" = "Accept";
|
||||
"NEW_CONVERSATION_MENU_OPEN_GROUP" = "Open Group";
|
||||
"NEW_CONVERSATION_MENU_DIRECT_MESSAGE" = "Direct Message";
|
||||
"NEW_CONVERSATION_MENU_CLOSED_GROUP" = "Closed Group";
|
||||
|
|
|
@ -94,12 +94,18 @@
|
|||
"BLOCK_LIST_BLOCK_BUTTON" = "Заблокировать";
|
||||
/* A format for the 'block user' action sheet title. Embeds {{the blocked user's name or phone number}}. */
|
||||
"BLOCK_LIST_BLOCK_USER_TITLE_FORMAT" = "Заблокировать %@?";
|
||||
/* A format for the 'unblock conversation' action sheet title. Embeds the {{conversation title}}. */
|
||||
"BLOCK_LIST_UNBLOCK_TITLE_FORMAT" = "Разблокировать %@?";
|
||||
/* Button label for the 'unblock' button */
|
||||
"BLOCK_LIST_UNBLOCK_BUTTON" = "Разблокировать";
|
||||
/* The message format of the 'conversation blocked' alert. Embeds the {{conversation title}}. */
|
||||
"BLOCK_LIST_VIEW_BLOCKED_ALERT_MESSAGE_FORMAT" = "Пользователь %@ был заблокирован.";
|
||||
/* The title of the 'user blocked' alert. */
|
||||
"BLOCK_LIST_VIEW_BLOCKED_ALERT_TITLE" = "Пользователь заблокирован";
|
||||
/* Alert title after unblocking a group or 1:1 chat. Embeds the {{conversation title}}. */
|
||||
"BLOCK_LIST_VIEW_UNBLOCKED_ALERT_TITLE_FORMAT" = "%@ был(-a) разблокирован(-a).";
|
||||
/* Alert body after unblocking a group. */
|
||||
"BLOCK_LIST_VIEW_UNBLOCKED_GROUP_ALERT_BODY" = "Теперь участники группы могут снова добавить вас в группу.";
|
||||
/* An explanation of the consequences of blocking another user. */
|
||||
"BLOCK_USER_BEHAVIOR_EXPLANATION" = "Заблокированные пользователи не смогут звонить или отправлять сообщения Вам.";
|
||||
/* Label for generic done button. */
|
||||
|
@ -600,3 +606,18 @@
|
|||
"system_mode_theme" = "System";
|
||||
"dark_mode_theme" = "Dark";
|
||||
"light_mode_theme" = "Light";
|
||||
"MESSAGE_REQUESTS_TITLE" = "Message Requests";
|
||||
"MESSAGE_REQUESTS_EMPTY_TEXT" = "No pending message requests";
|
||||
"MESSAGE_REQUESTS_CLEAR_ALL" = "Clear All";
|
||||
"MESSAGE_REQUESTS_CLEAR_ALL_CONFIRMATION_TITLE" = "Are you sure you want to clear all message requests?";
|
||||
"MESSAGE_REQUESTS_CLEAR_ALL_CONFIRMATION_ACTON" = "Clear";
|
||||
"MESSAGE_REQUESTS_DELETE_CONFIRMATION_ACTON" = "Are you sure you want to delete this message request?";
|
||||
"MESSAGE_REQUESTS_APPROVAL_ERROR_MESSAGE" = "An error occurred when trying to accept this message request";
|
||||
"MESSAGE_REQUESTS_INFO" = "Sending a message to this user will automatically accept their message request.";
|
||||
"MESSAGE_REQUESTS_ACCEPTED" = "Your message request has been accepted.";
|
||||
"MESSAGE_REQUESTS_NOTIFICATION" = "You have a new message request";
|
||||
"TXT_HIDE_TITLE" = "Hide";
|
||||
"TXT_DELETE_ACCEPT" = "Accept";
|
||||
"NEW_CONVERSATION_MENU_OPEN_GROUP" = "Open Group";
|
||||
"NEW_CONVERSATION_MENU_DIRECT_MESSAGE" = "Direct Message";
|
||||
"NEW_CONVERSATION_MENU_CLOSED_GROUP" = "Closed Group";
|
||||
|
|
|
@ -94,12 +94,18 @@
|
|||
"BLOCK_LIST_BLOCK_BUTTON" = "Block";
|
||||
/* A format for the 'block user' action sheet title. Embeds {{the blocked user's name or phone number}}. */
|
||||
"BLOCK_LIST_BLOCK_USER_TITLE_FORMAT" = "Block %@?";
|
||||
/* A format for the 'unblock user' action sheet title. Embeds {{the unblocked user's name or phone number}}. */
|
||||
"BLOCK_LIST_UNBLOCK_TITLE_FORMAT" = "Unblock %@?";
|
||||
/* Button label for the 'unblock' button */
|
||||
"BLOCK_LIST_UNBLOCK_BUTTON" = "Unblock";
|
||||
/* The message format of the 'conversation blocked' alert. Embeds the {{conversation title}}. */
|
||||
"BLOCK_LIST_VIEW_BLOCKED_ALERT_MESSAGE_FORMAT" = "%@ has been blocked.";
|
||||
/* The title of the 'user blocked' alert. */
|
||||
"BLOCK_LIST_VIEW_BLOCKED_ALERT_TITLE" = "User Blocked";
|
||||
/* Alert title after unblocking a group or 1:1 chat. Embeds the {{conversation title}}. */
|
||||
"BLOCK_LIST_VIEW_UNBLOCKED_ALERT_TITLE_FORMAT" = "Oseba %@ je bila odblokirana";
|
||||
/* Alert body after unblocking a group. */
|
||||
"BLOCK_LIST_VIEW_UNBLOCKED_GROUP_ALERT_BODY" = "Existing members can now add you to the group again.";
|
||||
/* An explanation of the consequences of blocking another user. */
|
||||
"BLOCK_USER_BEHAVIOR_EXPLANATION" = "Blocked users will not be able to call you or send you messages.";
|
||||
/* Label for generic done button. */
|
||||
|
@ -600,3 +606,18 @@
|
|||
"system_mode_theme" = "System";
|
||||
"dark_mode_theme" = "Dark";
|
||||
"light_mode_theme" = "Light";
|
||||
"MESSAGE_REQUESTS_TITLE" = "Message Requests";
|
||||
"MESSAGE_REQUESTS_EMPTY_TEXT" = "No pending message requests";
|
||||
"MESSAGE_REQUESTS_CLEAR_ALL" = "Clear All";
|
||||
"MESSAGE_REQUESTS_CLEAR_ALL_CONFIRMATION_TITLE" = "Are you sure you want to clear all message requests?";
|
||||
"MESSAGE_REQUESTS_CLEAR_ALL_CONFIRMATION_ACTON" = "Clear";
|
||||
"MESSAGE_REQUESTS_DELETE_CONFIRMATION_ACTON" = "Are you sure you want to delete this message request?";
|
||||
"MESSAGE_REQUESTS_APPROVAL_ERROR_MESSAGE" = "An error occurred when trying to accept this message request";
|
||||
"MESSAGE_REQUESTS_INFO" = "Sending a message to this user will automatically accept their message request.";
|
||||
"MESSAGE_REQUESTS_ACCEPTED" = "Your message request has been accepted.";
|
||||
"MESSAGE_REQUESTS_NOTIFICATION" = "You have a new message request";
|
||||
"TXT_HIDE_TITLE" = "Hide";
|
||||
"TXT_DELETE_ACCEPT" = "Accept";
|
||||
"NEW_CONVERSATION_MENU_OPEN_GROUP" = "Open Group";
|
||||
"NEW_CONVERSATION_MENU_DIRECT_MESSAGE" = "Direct Message";
|
||||
"NEW_CONVERSATION_MENU_CLOSED_GROUP" = "Closed Group";
|
||||
|
|
|
@ -94,12 +94,18 @@
|
|||
"BLOCK_LIST_BLOCK_BUTTON" = "Blokovať";
|
||||
/* A format for the 'block user' action sheet title. Embeds {{the blocked user's name or phone number}}. */
|
||||
"BLOCK_LIST_BLOCK_USER_TITLE_FORMAT" = "Blokovať %@?";
|
||||
/* A format for the 'unblock user' action sheet title. Embeds {{the unblocked user's name or phone number}}. */
|
||||
"BLOCK_LIST_UNBLOCK_TITLE_FORMAT" = "Unblock %@?";
|
||||
/* Button label for the 'unblock' button */
|
||||
"BLOCK_LIST_UNBLOCK_BUTTON" = "Odblokovať";
|
||||
/* The message format of the 'conversation blocked' alert. Embeds the {{conversation title}}. */
|
||||
"BLOCK_LIST_VIEW_BLOCKED_ALERT_MESSAGE_FORMAT" = "%@ bol/a zablokovaný/á.";
|
||||
/* The title of the 'user blocked' alert. */
|
||||
"BLOCK_LIST_VIEW_BLOCKED_ALERT_TITLE" = "Používateľ blokovaný";
|
||||
/* Alert title after unblocking a group or 1:1 chat. Embeds the {{conversation title}}. */
|
||||
"BLOCK_LIST_VIEW_UNBLOCKED_ALERT_TITLE_FORMAT" = "%@ has been unblocked.";
|
||||
/* Alert body after unblocking a group. */
|
||||
"BLOCK_LIST_VIEW_UNBLOCKED_GROUP_ALERT_BODY" = "Existing members can now add you to the group again.";
|
||||
/* An explanation of the consequences of blocking another user. */
|
||||
"BLOCK_USER_BEHAVIOR_EXPLANATION" = "Blokovaný používateľ vám nebude mocť volať ani posielať správy.";
|
||||
/* Label for generic done button. */
|
||||
|
@ -600,3 +606,18 @@
|
|||
"system_mode_theme" = "System";
|
||||
"dark_mode_theme" = "Dark";
|
||||
"light_mode_theme" = "Light";
|
||||
"MESSAGE_REQUESTS_TITLE" = "Message Requests";
|
||||
"MESSAGE_REQUESTS_EMPTY_TEXT" = "No pending message requests";
|
||||
"MESSAGE_REQUESTS_CLEAR_ALL" = "Clear All";
|
||||
"MESSAGE_REQUESTS_CLEAR_ALL_CONFIRMATION_TITLE" = "Are you sure you want to clear all message requests?";
|
||||
"MESSAGE_REQUESTS_CLEAR_ALL_CONFIRMATION_ACTON" = "Clear";
|
||||
"MESSAGE_REQUESTS_DELETE_CONFIRMATION_ACTON" = "Are you sure you want to delete this message request?";
|
||||
"MESSAGE_REQUESTS_APPROVAL_ERROR_MESSAGE" = "An error occurred when trying to accept this message request";
|
||||
"MESSAGE_REQUESTS_INFO" = "Sending a message to this user will automatically accept their message request.";
|
||||
"MESSAGE_REQUESTS_ACCEPTED" = "Your message request has been accepted.";
|
||||
"MESSAGE_REQUESTS_NOTIFICATION" = "You have a new message request";
|
||||
"TXT_HIDE_TITLE" = "Hide";
|
||||
"TXT_DELETE_ACCEPT" = "Accept";
|
||||
"NEW_CONVERSATION_MENU_OPEN_GROUP" = "Open Group";
|
||||
"NEW_CONVERSATION_MENU_DIRECT_MESSAGE" = "Direct Message";
|
||||
"NEW_CONVERSATION_MENU_CLOSED_GROUP" = "Closed Group";
|
||||
|
|
|
@ -94,12 +94,18 @@
|
|||
"BLOCK_LIST_BLOCK_BUTTON" = "Blockera";
|
||||
/* A format for the 'block user' action sheet title. Embeds {{the blocked user's name or phone number}}. */
|
||||
"BLOCK_LIST_BLOCK_USER_TITLE_FORMAT" = "Blockera %@?";
|
||||
/* A format for the 'unblock conversation' action sheet title. Embeds the {{conversation title}}. */
|
||||
"BLOCK_LIST_UNBLOCK_TITLE_FORMAT" = "Avblockera %@?";
|
||||
/* Button label for the 'unblock' button */
|
||||
"BLOCK_LIST_UNBLOCK_BUTTON" = "Avblockera";
|
||||
/* The message format of the 'conversation blocked' alert. Embeds the {{conversation title}}. */
|
||||
"BLOCK_LIST_VIEW_BLOCKED_ALERT_MESSAGE_FORMAT" = "%@ har blockerats.";
|
||||
/* The title of the 'user blocked' alert. */
|
||||
"BLOCK_LIST_VIEW_BLOCKED_ALERT_TITLE" = "Användare blockerad";
|
||||
/* Alert title after unblocking a group or 1:1 chat. Embeds the {{conversation title}}. */
|
||||
"BLOCK_LIST_VIEW_UNBLOCKED_ALERT_TITLE_FORMAT" = "%@har blivit avblockerad.";
|
||||
/* Alert body after unblocking a group. */
|
||||
"BLOCK_LIST_VIEW_UNBLOCKED_GROUP_ALERT_BODY" = "Befintliga medlemmar kan nu lägga dig till gruppen igen.";
|
||||
/* An explanation of the consequences of blocking another user. */
|
||||
"BLOCK_USER_BEHAVIOR_EXPLANATION" = "Blockerade användare kommer inte att kunna ringa dig eller skicka meddelanden.";
|
||||
/* Label for generic done button. */
|
||||
|
@ -600,3 +606,18 @@
|
|||
"system_mode_theme" = "System";
|
||||
"dark_mode_theme" = "Dark";
|
||||
"light_mode_theme" = "Light";
|
||||
"MESSAGE_REQUESTS_TITLE" = "Message Requests";
|
||||
"MESSAGE_REQUESTS_EMPTY_TEXT" = "No pending message requests";
|
||||
"MESSAGE_REQUESTS_CLEAR_ALL" = "Clear All";
|
||||
"MESSAGE_REQUESTS_CLEAR_ALL_CONFIRMATION_TITLE" = "Are you sure you want to clear all message requests?";
|
||||
"MESSAGE_REQUESTS_CLEAR_ALL_CONFIRMATION_ACTON" = "Clear";
|
||||
"MESSAGE_REQUESTS_DELETE_CONFIRMATION_ACTON" = "Are you sure you want to delete this message request?";
|
||||
"MESSAGE_REQUESTS_APPROVAL_ERROR_MESSAGE" = "An error occurred when trying to accept this message request";
|
||||
"MESSAGE_REQUESTS_INFO" = "Sending a message to this user will automatically accept their message request.";
|
||||
"MESSAGE_REQUESTS_ACCEPTED" = "Your message request has been accepted.";
|
||||
"MESSAGE_REQUESTS_NOTIFICATION" = "You have a new message request";
|
||||
"TXT_HIDE_TITLE" = "Hide";
|
||||
"TXT_DELETE_ACCEPT" = "Accept";
|
||||
"NEW_CONVERSATION_MENU_OPEN_GROUP" = "Open Group";
|
||||
"NEW_CONVERSATION_MENU_DIRECT_MESSAGE" = "Direct Message";
|
||||
"NEW_CONVERSATION_MENU_CLOSED_GROUP" = "Closed Group";
|
||||
|
|
|
@ -94,12 +94,18 @@
|
|||
"BLOCK_LIST_BLOCK_BUTTON" = "บล็อก";
|
||||
/* A format for the 'block user' action sheet title. Embeds {{the blocked user's name or phone number}}. */
|
||||
"BLOCK_LIST_BLOCK_USER_TITLE_FORMAT" = "บล็อก %@ ไหม";
|
||||
/* A format for the 'unblock conversation' action sheet title. Embeds the {{conversation title}}. */
|
||||
"BLOCK_LIST_UNBLOCK_TITLE_FORMAT" = "เลิกบล็อก %@ หรือไม่";
|
||||
/* Button label for the 'unblock' button */
|
||||
"BLOCK_LIST_UNBLOCK_BUTTON" = "เลิกบล็อก";
|
||||
/* The message format of the 'conversation blocked' alert. Embeds the {{conversation title}}. */
|
||||
"BLOCK_LIST_VIEW_BLOCKED_ALERT_MESSAGE_FORMAT" = "%@ บล็อกแล้ว";
|
||||
/* The title of the 'user blocked' alert. */
|
||||
"BLOCK_LIST_VIEW_BLOCKED_ALERT_TITLE" = "คนบล็อกแล้ว";
|
||||
/* Alert title after unblocking a group or 1:1 chat. Embeds the {{conversation title}}. */
|
||||
"BLOCK_LIST_VIEW_UNBLOCKED_ALERT_TITLE_FORMAT" = "%@ has been unblocked.";
|
||||
/* Alert body after unblocking a group. */
|
||||
"BLOCK_LIST_VIEW_UNBLOCKED_GROUP_ALERT_BODY" = "Existing members can now add you to the group again.";
|
||||
/* An explanation of the consequences of blocking another user. */
|
||||
"BLOCK_USER_BEHAVIOR_EXPLANATION" = "คนที่บล็อกแล้วส่งข้อความและโทรมาหาไม่ได้";
|
||||
/* Label for generic done button. */
|
||||
|
@ -600,3 +606,18 @@
|
|||
"system_mode_theme" = "System";
|
||||
"dark_mode_theme" = "Dark";
|
||||
"light_mode_theme" = "Light";
|
||||
"MESSAGE_REQUESTS_TITLE" = "Message Requests";
|
||||
"MESSAGE_REQUESTS_EMPTY_TEXT" = "No pending message requests";
|
||||
"MESSAGE_REQUESTS_CLEAR_ALL" = "Clear All";
|
||||
"MESSAGE_REQUESTS_CLEAR_ALL_CONFIRMATION_TITLE" = "Are you sure you want to clear all message requests?";
|
||||
"MESSAGE_REQUESTS_CLEAR_ALL_CONFIRMATION_ACTON" = "Clear";
|
||||
"MESSAGE_REQUESTS_DELETE_CONFIRMATION_ACTON" = "Are you sure you want to delete this message request?";
|
||||
"MESSAGE_REQUESTS_APPROVAL_ERROR_MESSAGE" = "An error occurred when trying to accept this message request";
|
||||
"MESSAGE_REQUESTS_INFO" = "Sending a message to this user will automatically accept their message request.";
|
||||
"MESSAGE_REQUESTS_ACCEPTED" = "Your message request has been accepted.";
|
||||
"MESSAGE_REQUESTS_NOTIFICATION" = "You have a new message request";
|
||||
"TXT_HIDE_TITLE" = "Hide";
|
||||
"TXT_DELETE_ACCEPT" = "Accept";
|
||||
"NEW_CONVERSATION_MENU_OPEN_GROUP" = "Open Group";
|
||||
"NEW_CONVERSATION_MENU_DIRECT_MESSAGE" = "Direct Message";
|
||||
"NEW_CONVERSATION_MENU_CLOSED_GROUP" = "Closed Group";
|
||||
|
|
|
@ -94,12 +94,18 @@
|
|||
"BLOCK_LIST_BLOCK_BUTTON" = "Block";
|
||||
/* A format for the 'block user' action sheet title. Embeds {{the blocked user's name or phone number}}. */
|
||||
"BLOCK_LIST_BLOCK_USER_TITLE_FORMAT" = "Block %@?";
|
||||
/* A format for the 'unblock user' action sheet title. Embeds {{the unblocked user's name or phone number}}. */
|
||||
"BLOCK_LIST_UNBLOCK_TITLE_FORMAT" = "Unblock %@?";
|
||||
/* Button label for the 'unblock' button */
|
||||
"BLOCK_LIST_UNBLOCK_BUTTON" = "Unblock";
|
||||
/* The message format of the 'conversation blocked' alert. Embeds the {{conversation title}}. */
|
||||
"BLOCK_LIST_VIEW_BLOCKED_ALERT_MESSAGE_FORMAT" = "%@ has been blocked.";
|
||||
/* The title of the 'user blocked' alert. */
|
||||
"BLOCK_LIST_VIEW_BLOCKED_ALERT_TITLE" = "User Blocked";
|
||||
/* Alert title after unblocking a group or 1:1 chat. Embeds the {{conversation title}}. */
|
||||
"BLOCK_LIST_VIEW_UNBLOCKED_ALERT_TITLE_FORMAT" = "%@ has been unblocked.";
|
||||
/* Alert body after unblocking a group. */
|
||||
"BLOCK_LIST_VIEW_UNBLOCKED_GROUP_ALERT_BODY" = "Existing members can now add you to the group again.";
|
||||
/* An explanation of the consequences of blocking another user. */
|
||||
"BLOCK_USER_BEHAVIOR_EXPLANATION" = "Blocked users will not be able to call you or send you messages.";
|
||||
/* Label for generic done button. */
|
||||
|
@ -600,3 +606,18 @@
|
|||
"system_mode_theme" = "System";
|
||||
"dark_mode_theme" = "Dark";
|
||||
"light_mode_theme" = "Light";
|
||||
"MESSAGE_REQUESTS_TITLE" = "Message Requests";
|
||||
"MESSAGE_REQUESTS_EMPTY_TEXT" = "No pending message requests";
|
||||
"MESSAGE_REQUESTS_CLEAR_ALL" = "Clear All";
|
||||
"MESSAGE_REQUESTS_CLEAR_ALL_CONFIRMATION_TITLE" = "Are you sure you want to clear all message requests?";
|
||||
"MESSAGE_REQUESTS_CLEAR_ALL_CONFIRMATION_ACTON" = "Clear";
|
||||
"MESSAGE_REQUESTS_DELETE_CONFIRMATION_ACTON" = "Are you sure you want to delete this message request?";
|
||||
"MESSAGE_REQUESTS_APPROVAL_ERROR_MESSAGE" = "An error occurred when trying to accept this message request";
|
||||
"MESSAGE_REQUESTS_INFO" = "Sending a message to this user will automatically accept their message request.";
|
||||
"MESSAGE_REQUESTS_ACCEPTED" = "Your message request has been accepted.";
|
||||
"MESSAGE_REQUESTS_NOTIFICATION" = "You have a new message request";
|
||||
"TXT_HIDE_TITLE" = "Hide";
|
||||
"TXT_DELETE_ACCEPT" = "Accept";
|
||||
"NEW_CONVERSATION_MENU_OPEN_GROUP" = "Open Group";
|
||||
"NEW_CONVERSATION_MENU_DIRECT_MESSAGE" = "Direct Message";
|
||||
"NEW_CONVERSATION_MENU_CLOSED_GROUP" = "Closed Group";
|
||||
|
|
|
@ -94,12 +94,18 @@
|
|||
"BLOCK_LIST_BLOCK_BUTTON" = "封鎖";
|
||||
/* A format for the 'block user' action sheet title. Embeds {{the blocked user's name or phone number}}. */
|
||||
"BLOCK_LIST_BLOCK_USER_TITLE_FORMAT" = "封鎖 %@?";
|
||||
/* A format for the 'unblock user' action sheet title. Embeds {{the unblocked user's name or phone number}}. */
|
||||
"BLOCK_LIST_UNBLOCK_TITLE_FORMAT" = "Unblock %@?";
|
||||
/* Button label for the 'unblock' button */
|
||||
"BLOCK_LIST_UNBLOCK_BUTTON" = "解除封鎖";
|
||||
/* The message format of the 'conversation blocked' alert. Embeds the {{conversation title}}. */
|
||||
"BLOCK_LIST_VIEW_BLOCKED_ALERT_MESSAGE_FORMAT" = "已封鎖 %@。";
|
||||
/* The title of the 'user blocked' alert. */
|
||||
"BLOCK_LIST_VIEW_BLOCKED_ALERT_TITLE" = "使用者已封鎖";
|
||||
/* Alert title after unblocking a group or 1:1 chat. Embeds the {{conversation title}}. */
|
||||
"BLOCK_LIST_VIEW_UNBLOCKED_ALERT_TITLE_FORMAT" = "%@ has been unblocked.";
|
||||
/* Alert body after unblocking a group. */
|
||||
"BLOCK_LIST_VIEW_UNBLOCKED_GROUP_ALERT_BODY" = "Existing members can now add you to the group again.";
|
||||
/* An explanation of the consequences of blocking another user. */
|
||||
"BLOCK_USER_BEHAVIOR_EXPLANATION" = "被您封鎖的使用者將無法傳送訊息與撥打電話給您";
|
||||
/* Label for generic done button. */
|
||||
|
@ -600,3 +606,18 @@
|
|||
"system_mode_theme" = "System";
|
||||
"dark_mode_theme" = "Dark";
|
||||
"light_mode_theme" = "Light";
|
||||
"MESSAGE_REQUESTS_TITLE" = "Message Requests";
|
||||
"MESSAGE_REQUESTS_EMPTY_TEXT" = "No pending message requests";
|
||||
"MESSAGE_REQUESTS_CLEAR_ALL" = "Clear All";
|
||||
"MESSAGE_REQUESTS_CLEAR_ALL_CONFIRMATION_TITLE" = "Are you sure you want to clear all message requests?";
|
||||
"MESSAGE_REQUESTS_CLEAR_ALL_CONFIRMATION_ACTON" = "Clear";
|
||||
"MESSAGE_REQUESTS_DELETE_CONFIRMATION_ACTON" = "Are you sure you want to delete this message request?";
|
||||
"MESSAGE_REQUESTS_APPROVAL_ERROR_MESSAGE" = "An error occurred when trying to accept this message request";
|
||||
"MESSAGE_REQUESTS_INFO" = "Sending a message to this user will automatically accept their message request.";
|
||||
"MESSAGE_REQUESTS_ACCEPTED" = "Your message request has been accepted.";
|
||||
"MESSAGE_REQUESTS_NOTIFICATION" = "You have a new message request";
|
||||
"TXT_HIDE_TITLE" = "Hide";
|
||||
"TXT_DELETE_ACCEPT" = "Accept";
|
||||
"NEW_CONVERSATION_MENU_OPEN_GROUP" = "Open Group";
|
||||
"NEW_CONVERSATION_MENU_DIRECT_MESSAGE" = "Direct Message";
|
||||
"NEW_CONVERSATION_MENU_CLOSED_GROUP" = "Closed Group";
|
||||
|
|
|
@ -94,12 +94,18 @@
|
|||
"BLOCK_LIST_BLOCK_BUTTON" = "加入黑名单";
|
||||
/* A format for the 'block user' action sheet title. Embeds {{the blocked user's name or phone number}}. */
|
||||
"BLOCK_LIST_BLOCK_USER_TITLE_FORMAT" = "屏蔽 %@?";
|
||||
/* A format for the 'unblock conversation' action sheet title. Embeds the {{conversation title}}. */
|
||||
"BLOCK_LIST_UNBLOCK_TITLE_FORMAT" = "从黑名单中移除 %@ 吗?";
|
||||
/* Button label for the 'unblock' button */
|
||||
"BLOCK_LIST_UNBLOCK_BUTTON" = "从黑名单中移除";
|
||||
/* The message format of the 'conversation blocked' alert. Embeds the {{conversation title}}. */
|
||||
"BLOCK_LIST_VIEW_BLOCKED_ALERT_MESSAGE_FORMAT" = "已屏蔽 %@。";
|
||||
/* The title of the 'user blocked' alert. */
|
||||
"BLOCK_LIST_VIEW_BLOCKED_ALERT_TITLE" = "用户已屏蔽";
|
||||
/* Alert title after unblocking a group or 1:1 chat. Embeds the {{conversation title}}. */
|
||||
"BLOCK_LIST_VIEW_UNBLOCKED_ALERT_TITLE_FORMAT" = "已取消屏蔽 %@。";
|
||||
/* Alert body after unblocking a group. */
|
||||
"BLOCK_LIST_VIEW_UNBLOCKED_GROUP_ALERT_BODY" = "现有成员可再次将您加入群组。";
|
||||
/* An explanation of the consequences of blocking another user. */
|
||||
"BLOCK_USER_BEHAVIOR_EXPLANATION" = "被屏蔽的用户将无法向您发起通话,或发送消息。";
|
||||
/* Label for generic done button. */
|
||||
|
@ -600,3 +606,18 @@
|
|||
"system_mode_theme" = "System";
|
||||
"dark_mode_theme" = "Dark";
|
||||
"light_mode_theme" = "Light";
|
||||
"MESSAGE_REQUESTS_TITLE" = "Message Requests";
|
||||
"MESSAGE_REQUESTS_EMPTY_TEXT" = "No pending message requests";
|
||||
"MESSAGE_REQUESTS_CLEAR_ALL" = "Clear All";
|
||||
"MESSAGE_REQUESTS_CLEAR_ALL_CONFIRMATION_TITLE" = "Are you sure you want to clear all message requests?";
|
||||
"MESSAGE_REQUESTS_CLEAR_ALL_CONFIRMATION_ACTON" = "Clear";
|
||||
"MESSAGE_REQUESTS_DELETE_CONFIRMATION_ACTON" = "Are you sure you want to delete this message request?";
|
||||
"MESSAGE_REQUESTS_APPROVAL_ERROR_MESSAGE" = "An error occurred when trying to accept this message request";
|
||||
"MESSAGE_REQUESTS_INFO" = "Sending a message to this user will automatically accept their message request.";
|
||||
"MESSAGE_REQUESTS_ACCEPTED" = "Your message request has been accepted.";
|
||||
"MESSAGE_REQUESTS_NOTIFICATION" = "You have a new message request";
|
||||
"TXT_HIDE_TITLE" = "Hide";
|
||||
"TXT_DELETE_ACCEPT" = "Accept";
|
||||
"NEW_CONVERSATION_MENU_OPEN_GROUP" = "Open Group";
|
||||
"NEW_CONVERSATION_MENU_DIRECT_MESSAGE" = "Direct Message";
|
||||
"NEW_CONVERSATION_MENU_CLOSED_GROUP" = "Closed Group";
|
||||
|
|
|
@ -4,6 +4,8 @@
|
|||
|
||||
import Foundation
|
||||
import PromiseKit
|
||||
import SessionMessagingKit
|
||||
import SignalUtilitiesKit
|
||||
|
||||
/// There are two primary components in our system notification integration:
|
||||
///
|
||||
|
@ -88,7 +90,7 @@ let kNotificationDelayForBackgroumdPoll: TimeInterval = 5
|
|||
let kAudioNotificationsThrottleCount = 2
|
||||
let kAudioNotificationsThrottleInterval: TimeInterval = 5
|
||||
|
||||
protocol NotificationPresenterAdaptee: class {
|
||||
protocol NotificationPresenterAdaptee: AnyObject {
|
||||
|
||||
func registerNotificationSettings() -> Promise<Void>
|
||||
|
||||
|
@ -157,10 +159,35 @@ public class NotificationPresenter: NSObject, NotificationsProtocol {
|
|||
}
|
||||
|
||||
public func notifyUser(for incomingMessage: TSIncomingMessage, in thread: TSThread, transaction: YapDatabaseReadTransaction) {
|
||||
|
||||
guard !thread.isMuted else { return }
|
||||
guard let threadId = thread.uniqueId else { return }
|
||||
|
||||
// If the thread is a message request and the user hasn't hidden message requests then we need
|
||||
// to check if this is the only message request thread (group threads can't be message requests
|
||||
// so just ignore those and if the user has hidden message requests then we want to show the
|
||||
// notification regardless of how many message requests there are)
|
||||
if !thread.isGroupThread() && thread.isMessageRequest() && !CurrentAppContext().appUserDefaults()[.hasHiddenMessageRequests] {
|
||||
let dbConnection: YapDatabaseConnection = OWSPrimaryStorage.shared().newDatabaseConnection()
|
||||
dbConnection.objectCacheLimit = 2
|
||||
dbConnection.beginLongLivedReadTransaction() // Freeze the connection for use on the main thread (this gives us a stable data source that doesn't change until we tell it to)
|
||||
let threads: YapDatabaseViewMappings = YapDatabaseViewMappings(groups: [ TSMessageRequestGroup ], view: TSThreadDatabaseViewExtensionName)
|
||||
dbConnection.read { transaction in
|
||||
threads.update(with: transaction) // Perform the initial update
|
||||
}
|
||||
|
||||
let numMessageRequests = threads.numberOfItems(inGroup: TSMessageRequestGroup)
|
||||
dbConnection.endLongLivedReadTransaction()
|
||||
|
||||
// Allow this to show a notification if there are no message requests (ie. this is the first one)
|
||||
guard numMessageRequests == 0 else { return }
|
||||
}
|
||||
else if thread.isMessageRequest() && CurrentAppContext().appUserDefaults()[.hasHiddenMessageRequests] {
|
||||
// If there are other interactions on this thread already then don't show the notification
|
||||
if thread.numberOfInteractions() > 1 { return }
|
||||
|
||||
CurrentAppContext().appUserDefaults()[.hasHiddenMessageRequests] = false
|
||||
}
|
||||
|
||||
let identifier: String = incomingMessage.notificationIdentifier ?? UUID().uuidString
|
||||
|
||||
let isBackgroudPoll = identifier == threadId
|
||||
|
@ -185,36 +212,44 @@ public class NotificationPresenter: NSObject, NotificationsProtocol {
|
|||
let senderName = Storage.shared.getContact(with: incomingMessage.authorId, using: transaction)?.displayName(for: context) ?? incomingMessage.authorId
|
||||
|
||||
let notificationTitle: String?
|
||||
let previewType = preferences.notificationPreviewType(with: transaction)
|
||||
switch previewType {
|
||||
case .noNameNoPreview:
|
||||
notificationTitle = "Session"
|
||||
case .nameNoPreview, .namePreview:
|
||||
switch thread {
|
||||
case is TSContactThread:
|
||||
notificationTitle = senderName
|
||||
case is TSGroupThread:
|
||||
var groupName = thread.name()
|
||||
if groupName.count < 1 {
|
||||
groupName = MessageStrings.newGroupDefaultTitle
|
||||
}
|
||||
notificationTitle = isBackgroudPoll ? groupName : String(format: NotificationStrings.incomingGroupMessageTitleFormat, senderName, groupName)
|
||||
default:
|
||||
owsFailDebug("unexpected thread: \(thread)")
|
||||
return
|
||||
}
|
||||
default:
|
||||
notificationTitle = "Session"
|
||||
}
|
||||
|
||||
var notificationBody: String?
|
||||
let previewType = preferences.notificationPreviewType(with: transaction)
|
||||
|
||||
switch previewType {
|
||||
case .noNameNoPreview, .nameNoPreview:
|
||||
notificationBody = NotificationStrings.incomingMessageBody
|
||||
case .namePreview:
|
||||
notificationBody = messageText
|
||||
default:
|
||||
notificationBody = NotificationStrings.incomingMessageBody
|
||||
case .noNameNoPreview:
|
||||
notificationTitle = "Session"
|
||||
|
||||
case .nameNoPreview, .namePreview:
|
||||
switch thread {
|
||||
case is TSContactThread:
|
||||
notificationTitle = (thread.isMessageRequest() ? "Session" : senderName)
|
||||
|
||||
case is TSGroupThread:
|
||||
var groupName = thread.name()
|
||||
if groupName.count < 1 {
|
||||
groupName = MessageStrings.newGroupDefaultTitle
|
||||
}
|
||||
notificationTitle = isBackgroudPoll ? groupName : String(format: NotificationStrings.incomingGroupMessageTitleFormat, senderName, groupName)
|
||||
|
||||
default:
|
||||
owsFailDebug("unexpected thread: \(thread)")
|
||||
return
|
||||
}
|
||||
|
||||
default:
|
||||
notificationTitle = "Session"
|
||||
}
|
||||
|
||||
switch previewType {
|
||||
case .noNameNoPreview, .nameNoPreview: notificationBody = NotificationStrings.incomingMessageBody
|
||||
case .namePreview: notificationBody = messageText
|
||||
default: notificationBody = NotificationStrings.incomingMessageBody
|
||||
}
|
||||
|
||||
// If it's a message request then overwrite the body to be something generic (only show a notification
|
||||
// when receiving a new message request if there aren't any others or the user had hidden them)
|
||||
if thread.isMessageRequest() {
|
||||
notificationBody = NSLocalizedString("MESSAGE_REQUESTS_NOTIFICATION", comment: "")
|
||||
}
|
||||
|
||||
assert((notificationBody ?? notificationTitle) != nil)
|
||||
|
@ -230,12 +265,15 @@ public class NotificationPresenter: NSObject, NotificationsProtocol {
|
|||
DispatchQueue.main.async {
|
||||
notificationBody = MentionUtilities.highlightMentions(in: notificationBody!, threadID: thread.uniqueId!)
|
||||
let sound = self.requestSound(thread: thread)
|
||||
self.adaptee.notify(category: category,
|
||||
title: notificationTitle,
|
||||
body: notificationBody ?? "",
|
||||
userInfo: userInfo,
|
||||
sound: sound,
|
||||
replacingIdentifier: identifier)
|
||||
|
||||
self.adaptee.notify(
|
||||
category: category,
|
||||
title: notificationTitle,
|
||||
body: notificationBody ?? "",
|
||||
userInfo: userInfo,
|
||||
sound: sound,
|
||||
replacingIdentifier: identifier
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -53,11 +53,11 @@ public enum PushRegistrationError: Error {
|
|||
Logger.info("")
|
||||
|
||||
return firstly { () -> Promise<Void> in
|
||||
return self.registerUserNotificationSettings()
|
||||
self.registerUserNotificationSettings()
|
||||
}.then { (_) -> Promise<(pushToken: String, voipToken: String)> in
|
||||
guard !Platform.isSimulator else {
|
||||
throw PushRegistrationError.pushNotSupported(description: "Push not supported on simulators")
|
||||
}
|
||||
#if targetEnvironment(simulator)
|
||||
throw PushRegistrationError.pushNotSupported(description: "Push not supported on simulators")
|
||||
#endif
|
||||
|
||||
return self.registerForVanillaPushToken().then { vanillaPushToken -> Promise<(pushToken: String, voipToken: String)> in
|
||||
self.registerForVoipPushToken().map { voipPushToken in
|
||||
|
|
|
@ -91,7 +91,7 @@ final class PNModeVC : BaseVC, OptionViewDelegate {
|
|||
let title = NSLocalizedString("vc_pn_mode_no_option_picked_modal_title", comment: "")
|
||||
let alert = UIAlertController(title: title, message: nil, preferredStyle: .alert)
|
||||
alert.addAction(UIAlertAction(title: NSLocalizedString("BUTTON_OK", comment: ""), style: .default, handler: nil))
|
||||
return present(alert, animated: true, completion: nil)
|
||||
return presentAlert(alert)
|
||||
}
|
||||
UserDefaults.standard[.isUsingFullAPNs] = (selectedOptionView == apnsOptionView)
|
||||
TSAccountManager.sharedInstance().didRegister()
|
||||
|
|
|
@ -61,7 +61,7 @@ final class JoinOpenGroupVC : BaseVC, UIPageViewControllerDataSource, UIPageView
|
|||
tabBar.pin(.leading, to: .leading, of: view)
|
||||
let tabBarInset: CGFloat
|
||||
if #available(iOS 13, *) {
|
||||
tabBarInset = navigationBar.height()
|
||||
tabBarInset = UIDevice.current.isIPad ? navigationBar.height() + 20 : navigationBar.height()
|
||||
} else {
|
||||
tabBarInset = 0
|
||||
}
|
||||
|
|
|
@ -210,6 +210,12 @@ private final class ViewMyQRCodeVC : UIViewController {
|
|||
@objc private func shareQRCode() {
|
||||
let qrCode = QRCode.generate(for: getUserHexEncodedPublicKey(), hasBackground: true)
|
||||
let shareVC = UIActivityViewController(activityItems: [ qrCode ], applicationActivities: nil)
|
||||
if UIDevice.current.isIPad {
|
||||
shareVC.excludedActivityTypes = []
|
||||
shareVC.popoverPresentationController?.permittedArrowDirections = []
|
||||
shareVC.popoverPresentationController?.sourceView = self.view
|
||||
shareVC.popoverPresentationController?.sourceRect = self.view.bounds
|
||||
}
|
||||
qrCodeVC.navigationController!.present(shareVC, animated: true, completion: nil)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -139,9 +139,6 @@ final class SettingsVC : BaseVC, AvatarViewHelperDelegate {
|
|||
setUpNavBarStyle()
|
||||
setNavBarTitle(NSLocalizedString("vc_settings_title", comment: ""))
|
||||
// Navigation bar buttons
|
||||
let backButton = UIBarButtonItem(title: "Back", style: .plain, target: nil, action: nil)
|
||||
backButton.tintColor = Colors.text
|
||||
navigationItem.backBarButtonItem = backButton
|
||||
updateNavigationBarButtons()
|
||||
// Profile picture view
|
||||
let profilePictureTapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(showEditProfilePictureUI))
|
||||
|
@ -254,8 +251,6 @@ final class SettingsVC : BaseVC, AvatarViewHelperDelegate {
|
|||
pathStatusView.pin(.leading, to: .trailing, of: pathButton.titleLabel!, withInset: Values.smallSpacing)
|
||||
pathStatusView.autoVCenterInSuperview()
|
||||
|
||||
pathButton.titleEdgeInsets = UIEdgeInsets(top: 0, leading: 0, bottom: 0, trailing: Values.smallSpacing)
|
||||
|
||||
return [
|
||||
getSeparator(),
|
||||
pathButton,
|
||||
|
@ -264,6 +259,8 @@ final class SettingsVC : BaseVC, AvatarViewHelperDelegate {
|
|||
getSeparator(),
|
||||
getSettingButton(withTitle: NSLocalizedString("vc_settings_notifications_button_title", comment: ""), color: Colors.text, action: #selector(showNotificationSettings)),
|
||||
getSeparator(),
|
||||
getSettingButton(withTitle: NSLocalizedString("MESSAGE_REQUESTS_TITLE", comment: ""), color: Colors.text, action: #selector(showMessageRequests)),
|
||||
getSeparator(),
|
||||
getSettingButton(withTitle: NSLocalizedString("vc_settings_recovery_phrase_button_title", comment: ""), color: Colors.text, action: #selector(showSeed)),
|
||||
getSeparator(),
|
||||
getSettingButton(withTitle: NSLocalizedString("vc_settings_clear_all_data_button_title", comment: ""), color: Colors.destructive, action: #selector(clearAllData)),
|
||||
|
@ -392,7 +389,7 @@ final class SettingsVC : BaseVC, AvatarViewHelperDelegate {
|
|||
let message = isMaxFileSizeExceeded ? "Please select a smaller photo and try again" : "Please check your internet connection and try again"
|
||||
let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)
|
||||
alert.addAction(UIAlertAction(title: NSLocalizedString("BUTTON_OK", comment: ""), style: .default, handler: nil))
|
||||
self?.present(alert, animated: true, completion: nil)
|
||||
self?.presentAlert(alert)
|
||||
}
|
||||
}
|
||||
}, requiresSync: true)
|
||||
|
@ -491,6 +488,12 @@ final class SettingsVC : BaseVC, AvatarViewHelperDelegate {
|
|||
|
||||
@objc private func sharePublicKey() {
|
||||
let shareVC = UIActivityViewController(activityItems: [ getUserHexEncodedPublicKey() ], applicationActivities: nil)
|
||||
if UIDevice.current.isIPad {
|
||||
shareVC.excludedActivityTypes = []
|
||||
shareVC.popoverPresentationController?.permittedArrowDirections = []
|
||||
shareVC.popoverPresentationController?.sourceView = self.view
|
||||
shareVC.popoverPresentationController?.sourceRect = self.view.bounds
|
||||
}
|
||||
navigationController!.present(shareVC, animated: true, completion: nil)
|
||||
}
|
||||
|
||||
|
@ -509,6 +512,11 @@ final class SettingsVC : BaseVC, AvatarViewHelperDelegate {
|
|||
navigationController!.pushViewController(notificationSettingsVC, animated: true)
|
||||
}
|
||||
|
||||
@objc private func showMessageRequests() {
|
||||
let viewController: MessageRequestsViewController = MessageRequestsViewController()
|
||||
self.navigationController?.pushViewController(viewController, animated: true)
|
||||
}
|
||||
|
||||
@objc private func showSeed() {
|
||||
let seedModal = SeedModal()
|
||||
seedModal.modalPresentationStyle = .overFullScreen
|
||||
|
@ -526,6 +534,12 @@ final class SettingsVC : BaseVC, AvatarViewHelperDelegate {
|
|||
@objc private func sendInvitation() {
|
||||
let invitation = "Hey, I've been using Session to chat with complete privacy and security. Come join me! Download it at https://getsession.org/. My Session ID is \(getUserHexEncodedPublicKey()) !"
|
||||
let shareVC = UIActivityViewController(activityItems: [ invitation ], applicationActivities: nil)
|
||||
if UIDevice.current.isIPad {
|
||||
shareVC.excludedActivityTypes = []
|
||||
shareVC.popoverPresentationController?.permittedArrowDirections = []
|
||||
shareVC.popoverPresentationController?.sourceView = self.view
|
||||
shareVC.popoverPresentationController?.sourceRect = self.view.bounds
|
||||
}
|
||||
navigationController!.present(shareVC, animated: true, completion: nil)
|
||||
}
|
||||
|
||||
|
|
|
@ -64,7 +64,16 @@ final class ShareLogsModal : Modal {
|
|||
if let latestLogFilePath = logFilePaths.first {
|
||||
let latestLogFileURL = URL(fileURLWithPath: latestLogFilePath)
|
||||
self.dismiss(animated: true, completion: {
|
||||
AttachmentSharing.showShareUI(for: latestLogFileURL)
|
||||
if let vc = CurrentAppContext().frontmostViewController() {
|
||||
let shareVC = UIActivityViewController(activityItems: [ latestLogFileURL ], applicationActivities: nil)
|
||||
if UIDevice.current.isIPad {
|
||||
shareVC.excludedActivityTypes = []
|
||||
shareVC.popoverPresentationController?.permittedArrowDirections = []
|
||||
shareVC.popoverPresentationController?.sourceView = vc.view
|
||||
shareVC.popoverPresentationController?.sourceRect = vc.view.bounds
|
||||
}
|
||||
vc.present(shareVC, animated: true, completion: nil)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -46,6 +46,7 @@ class BaseVC : UIViewController {
|
|||
|
||||
internal func setUpNavBarStyle() {
|
||||
guard let navigationBar = navigationController?.navigationBar else { return }
|
||||
|
||||
if #available(iOS 15.0, *) {
|
||||
let appearance = UINavigationBarAppearance()
|
||||
appearance.configureWithOpaqueBackground()
|
||||
|
@ -59,6 +60,11 @@ class BaseVC : UIViewController {
|
|||
navigationBar.isTranslucent = false
|
||||
navigationBar.barTintColor = Colors.navigationBarBackground
|
||||
}
|
||||
|
||||
// Back button (to appear on pushed screen)
|
||||
let backButton = UIBarButtonItem(title: "", style: .plain, target: nil, action: nil)
|
||||
backButton.tintColor = Colors.text
|
||||
navigationItem.backBarButtonItem = backButton
|
||||
}
|
||||
|
||||
internal func setNavBarTitle(_ title: String, customFontSize: CGFloat? = nil) {
|
||||
|
|
|
@ -118,7 +118,8 @@ final class ConversationCell : UITableViewCell {
|
|||
}()
|
||||
|
||||
// MARK: Settings
|
||||
private static let unreadCountViewSize: CGFloat = 20
|
||||
|
||||
public static let unreadCountViewSize: CGFloat = 20
|
||||
private static let statusIndicatorSize: CGFloat = 14
|
||||
|
||||
// MARK: Initialization
|
||||
|
@ -172,6 +173,7 @@ final class ConversationCell : UITableViewCell {
|
|||
labelContainerView.axis = .vertical
|
||||
labelContainerView.alignment = .leading
|
||||
labelContainerView.spacing = 6
|
||||
labelContainerView.isUserInteractionEnabled = false
|
||||
// Main stack view
|
||||
let stackView = UIStackView(arrangedSubviews: [ accentLineView, profilePictureView, labelContainerView ])
|
||||
stackView.axis = .horizontal
|
||||
|
@ -356,15 +358,20 @@ final class ConversationCell : UITableViewCell {
|
|||
if threadViewModel.isGroupThread {
|
||||
if threadViewModel.name.isEmpty {
|
||||
return "Unknown Group"
|
||||
} else {
|
||||
}
|
||||
else {
|
||||
return threadViewModel.name
|
||||
}
|
||||
} else {
|
||||
}
|
||||
else {
|
||||
if threadViewModel.threadRecord.isNoteToSelf() {
|
||||
return NSLocalizedString("NOTE_TO_SELF", comment: "")
|
||||
} else {
|
||||
let hexEncodedPublicKey = threadViewModel.contactSessionID!
|
||||
return Storage.shared.getContact(with: hexEncodedPublicKey)?.displayName(for: .regular) ?? hexEncodedPublicKey
|
||||
}
|
||||
else {
|
||||
let hexEncodedPublicKey: String = threadViewModel.contactSessionID!
|
||||
let displayName: String = (Storage.shared.getContact(with: hexEncodedPublicKey)?.displayName(for: .regular) ?? hexEncodedPublicKey)
|
||||
let middleTruncatedHexKey: String = "\(hexEncodedPublicKey.prefix(4))...\(hexEncodedPublicKey.suffix(4))"
|
||||
return (displayName == hexEncodedPublicKey ? middleTruncatedHexKey : displayName)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import UIKit
|
||||
|
||||
@objc(LKModal)
|
||||
class Modal : BaseVC {
|
||||
class Modal: BaseVC, UIGestureRecognizerDelegate {
|
||||
private(set) var verticalCenteringConstraint: NSLayoutConstraint!
|
||||
|
||||
// MARK: Components
|
||||
|
@ -38,9 +39,15 @@ class Modal : BaseVC {
|
|||
let alpha = isLightMode ? CGFloat(0.1) : Values.highOpacity
|
||||
view.backgroundColor = UIColor(hex: 0x000000).withAlphaComponent(alpha)
|
||||
cancelButton.addTarget(self, action: #selector(close), for: UIControl.Event.touchUpInside)
|
||||
|
||||
let swipeGestureRecognizer = UISwipeGestureRecognizer(target: self, action: #selector(close))
|
||||
swipeGestureRecognizer.direction = .down
|
||||
view.addGestureRecognizer(swipeGestureRecognizer)
|
||||
|
||||
let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(close))
|
||||
tapGestureRecognizer.delegate = self
|
||||
view.addGestureRecognizer(tapGestureRecognizer)
|
||||
|
||||
setUpViewHierarchy()
|
||||
}
|
||||
|
||||
|
@ -57,18 +64,17 @@ class Modal : BaseVC {
|
|||
preconditionFailure("populateContentView() is abstract and must be overridden.")
|
||||
}
|
||||
|
||||
// MARK: Interaction
|
||||
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
|
||||
let touch = touches.first!
|
||||
let location = touch.location(in: view)
|
||||
if contentView.frame.contains(location) {
|
||||
super.touchesBegan(touches, with: event)
|
||||
} else {
|
||||
close()
|
||||
}
|
||||
}
|
||||
// MARK: - Interaction
|
||||
|
||||
@objc func close() {
|
||||
dismiss(animated: true, completion: nil)
|
||||
}
|
||||
|
||||
// MARK: - UIGestureRecognizerDelegate
|
||||
|
||||
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool {
|
||||
let location: CGPoint = touch.location(in: contentView)
|
||||
|
||||
return !contentView.point(inside: location, with: nil)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,17 +6,29 @@ enum ContactUtilities {
|
|||
var result: [String] = []
|
||||
Storage.read { transaction in
|
||||
TSContactThread.enumerateCollectionObjects(with: transaction) { object, _ in
|
||||
guard let thread = object as? TSContactThread, thread.shouldBeVisible else { return }
|
||||
guard
|
||||
let thread: TSContactThread = object as? TSContactThread,
|
||||
thread.shouldBeVisible,
|
||||
Storage.shared.getContact(
|
||||
with: thread.contactSessionID(),
|
||||
using: transaction
|
||||
)?.didApproveMe == true
|
||||
else {
|
||||
return
|
||||
}
|
||||
|
||||
result.append(thread.contactSessionID())
|
||||
}
|
||||
}
|
||||
func getDisplayName(for publicKey: String) -> String {
|
||||
return Storage.shared.getContact(with: publicKey)?.displayName(for: .regular) ?? publicKey
|
||||
}
|
||||
|
||||
// Remove the current user
|
||||
if let index = result.firstIndex(of: getUserHexEncodedPublicKey()) {
|
||||
result.remove(at: index)
|
||||
}
|
||||
|
||||
// Sort alphabetically
|
||||
return result.sorted { getDisplayName(for: $0) < getDisplayName(for: $1) }
|
||||
}
|
||||
|
|
|
@ -1,23 +0,0 @@
|
|||
import Sodium
|
||||
|
||||
enum KeyPairUtilities {
|
||||
|
||||
static func generate(from seed: Data) -> (ed25519KeyPair: Sign.KeyPair, x25519KeyPair: ECKeyPair) {
|
||||
assert(seed.count == 16)
|
||||
let padding = Data(repeating: 0, count: 16)
|
||||
let ed25519KeyPair = Sodium().sign.keyPair(seed: (seed + padding).bytes)!
|
||||
let x25519PublicKey = Sodium().sign.toX25519(ed25519PublicKey: ed25519KeyPair.publicKey)!
|
||||
let x25519SecretKey = Sodium().sign.toX25519(ed25519SecretKey: ed25519KeyPair.secretKey)!
|
||||
let x25519KeyPair = ECKeyPair(publicKey: Data(x25519PublicKey), privateKey: Data(x25519SecretKey))!
|
||||
return (ed25519KeyPair: ed25519KeyPair, x25519KeyPair: x25519KeyPair)
|
||||
}
|
||||
|
||||
static func store(seed: Data, ed25519KeyPair: Sign.KeyPair, x25519KeyPair: ECKeyPair) {
|
||||
let dbConnection = OWSIdentityManager.shared().dbConnection
|
||||
let collection = OWSPrimaryStorageIdentityKeyStoreCollection
|
||||
dbConnection.setObject(seed.toHexString(), forKey: LKSeedKey, inCollection: collection)
|
||||
dbConnection.setObject(ed25519KeyPair.secretKey.toHexString(), forKey: LKED25519SecretKey, inCollection: collection)
|
||||
dbConnection.setObject(ed25519KeyPair.publicKey.toHexString(), forKey: LKED25519PublicKey, inCollection: collection)
|
||||
dbConnection.setObject(x25519KeyPair, forKey: OWSPrimaryStorageIdentityKeyStoreIdentityKey, inCollection: collection)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,255 @@
|
|||
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
|
||||
|
||||
import Foundation
|
||||
import Sodium
|
||||
import SessionMessagingKit
|
||||
|
||||
enum MockDataGenerator {
|
||||
// Note: This was taken from TensorFlow's Random (https://github.com/apple/swift/blob/bc8f9e61d333b8f7a625f74d48ef0b554726e349/stdlib/public/TensorFlow/Random.swift)
|
||||
// the complex approach is needed due to an issue with Swift's randomElement(using:)
|
||||
// generation (see https://stackoverflow.com/a/64897775 for more info)
|
||||
struct ARC4RandomNumberGenerator: RandomNumberGenerator {
|
||||
var state: [UInt8] = Array(0...255)
|
||||
var iPos: UInt8 = 0
|
||||
var jPos: UInt8 = 0
|
||||
|
||||
init<T: BinaryInteger>(seed: T) {
|
||||
self.init(
|
||||
seed: (0..<(UInt64.bitWidth / UInt64.bitWidth)).map { index in
|
||||
UInt8(truncatingIfNeeded: seed >> (UInt8.bitWidth * index))
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
init(seed: [UInt8]) {
|
||||
precondition(seed.count > 0, "Length of seed must be positive")
|
||||
precondition(seed.count <= 256, "Length of seed must be at most 256")
|
||||
|
||||
// Note: Have to use a for loop instead of a 'forEach' otherwise
|
||||
// it doesn't work properly (not sure why...)
|
||||
var j: UInt8 = 0
|
||||
for i: UInt8 in 0...255 {
|
||||
j &+= S(i) &+ seed[Int(i) % seed.count]
|
||||
swapAt(i, j)
|
||||
}
|
||||
}
|
||||
|
||||
/// Produce the next random UInt64 from the stream, and advance the internal state
|
||||
mutating func next() -> UInt64 {
|
||||
// Note: Have to use a for loop instead of a 'forEach' otherwise
|
||||
// it doesn't work properly (not sure why...)
|
||||
var result: UInt64 = 0
|
||||
for _ in 0..<UInt64.bitWidth / UInt8.bitWidth {
|
||||
result <<= UInt8.bitWidth
|
||||
result += UInt64(nextByte())
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
/// Helper to access the state
|
||||
private func S(_ index: UInt8) -> UInt8 {
|
||||
return state[Int(index)]
|
||||
}
|
||||
|
||||
/// Helper to swap elements of the state
|
||||
private mutating func swapAt(_ i: UInt8, _ j: UInt8) {
|
||||
state.swapAt(Int(i), Int(j))
|
||||
}
|
||||
|
||||
/// Generates the next byte in the keystream.
|
||||
private mutating func nextByte() -> UInt8 {
|
||||
iPos &+= 1
|
||||
jPos &+= S(iPos)
|
||||
swapAt(iPos, jPos)
|
||||
return S(S(iPos) &+ S(jPos))
|
||||
}
|
||||
}
|
||||
|
||||
static func generateMockData() {
|
||||
// Don't re-generate the mock data if it already exists
|
||||
var existingMockDataThread: TSContactThread?
|
||||
|
||||
Storage.read { transaction in
|
||||
existingMockDataThread = TSContactThread.getWithContactSessionID("MockDatabaseThread", transaction: transaction)
|
||||
}
|
||||
|
||||
guard existingMockDataThread == nil else { return }
|
||||
|
||||
/// The mock data generation is quite slow, there are 3 parts which take a decent amount of time (deleting the account afterwards will also take a long time):
|
||||
/// Generating the threads & content - ~3s per 100
|
||||
/// Writing to the database - ~10s per 1000
|
||||
/// Updating the UI - ~10s per 1000
|
||||
let dmThreadCount: Int = 100
|
||||
let closedGroupThreadCount: Int = 0
|
||||
let openGroupThreadCount: Int = 0
|
||||
let maxMessagesPerThread: Int = 50
|
||||
let dmRandomSeed: Int = 1111
|
||||
let cgRandomSeed: Int = 2222
|
||||
let ogRandomSeed: Int = 3333
|
||||
|
||||
// FIXME: Make sure this data doesn't go off device somehow?
|
||||
Storage.shared.write { anyTransaction in
|
||||
guard let transaction: YapDatabaseReadWriteTransaction = anyTransaction as? YapDatabaseReadWriteTransaction else { return }
|
||||
|
||||
// First create the thread used to indicate that the mock data has been generated
|
||||
_ = TSContactThread.getOrCreateThread(withContactSessionID: "MockDatabaseThread", transaction: transaction)
|
||||
|
||||
// Multiple spaces to make it look more like words
|
||||
let stringContent: [String] = "abcdefghijklmnopqrstuvwxyz ABCDEFGHIJKLMNOPQRSTUVWXYZ 0123456789 ".map { String($0) }
|
||||
let timestampNow: TimeInterval = Date().timeIntervalSince1970
|
||||
let userSessionId: String = getUserHexEncodedPublicKey()
|
||||
|
||||
// MARK: - -- DM Thread
|
||||
var dmThreadRandomGenerator: ARC4RandomNumberGenerator = ARC4RandomNumberGenerator(seed: dmRandomSeed)
|
||||
|
||||
(0..<dmThreadCount).forEach { threadIndex in
|
||||
let data = Data((0..<16).map { _ in UInt8.random(in: (UInt8.min...UInt8.max), using: &dmThreadRandomGenerator) })
|
||||
|
||||
let randomSessionId: String = KeyPairUtilities.generate(from: data).x25519KeyPair.hexEncodedPublicKey
|
||||
let isMessageRequest: Bool = Bool.random(using: &dmThreadRandomGenerator)
|
||||
let contactNameLength: Int = ((5..<20).randomElement(using: &dmThreadRandomGenerator) ?? 0)
|
||||
let numMessages: Int = ((0..<maxMessagesPerThread).randomElement(using: &dmThreadRandomGenerator) ?? 0)
|
||||
|
||||
// Generate the thread
|
||||
let thread: TSContactThread = TSContactThread.getOrCreateThread(withContactSessionID: randomSessionId, transaction: transaction)
|
||||
thread.shouldBeVisible = true
|
||||
|
||||
// Generate the contact
|
||||
let contact = Contact(sessionID: randomSessionId)
|
||||
contact.name = (0..<contactNameLength)
|
||||
.compactMap { _ in stringContent.randomElement(using: &dmThreadRandomGenerator) }
|
||||
.joined()
|
||||
contact.isApproved = (!isMessageRequest || Bool.random(using: &dmThreadRandomGenerator))
|
||||
contact.didApproveMe = (!isMessageRequest && Bool.random(using: &dmThreadRandomGenerator))
|
||||
Storage.shared.setContact(contact, using: transaction)
|
||||
|
||||
// Generate the message history (Note: Unapproved message requests will only include incoming messages)
|
||||
(0..<numMessages).forEach { index in
|
||||
let isIncoming: Bool = (
|
||||
Bool.random(using: &dmThreadRandomGenerator) &&
|
||||
(!isMessageRequest || contact.isApproved)
|
||||
)
|
||||
let messageLength: Int = ((3..<40).randomElement(using: &dmThreadRandomGenerator) ?? 0)
|
||||
|
||||
let message: VisibleMessage = VisibleMessage()
|
||||
message.sender = (isIncoming ? randomSessionId : userSessionId)
|
||||
message.sentTimestamp = UInt64(floor(timestampNow - Double(index * 5)))
|
||||
message.text = (0..<messageLength)
|
||||
.compactMap { _ in stringContent.randomElement(using: &dmThreadRandomGenerator) }
|
||||
.joined()
|
||||
|
||||
if isIncoming {
|
||||
let tsMessage: TSOutgoingMessage = TSOutgoingMessage.from(message, associatedWith: thread, using: transaction)
|
||||
tsMessage.save(with: transaction)
|
||||
}
|
||||
else {
|
||||
let tsMessage: TSIncomingMessage = TSIncomingMessage.from(message, quotedMessage: nil, linkPreview: nil, associatedWith: thread)
|
||||
tsMessage.save(with: transaction)
|
||||
}
|
||||
}
|
||||
|
||||
// Save the thread
|
||||
thread.save(with: transaction)
|
||||
}
|
||||
|
||||
// MARK: - -- Closed Group
|
||||
var cgThreadRandomGenerator: ARC4RandomNumberGenerator = ARC4RandomNumberGenerator(seed: cgRandomSeed)
|
||||
|
||||
(0..<closedGroupThreadCount).forEach { threadIndex in
|
||||
let data = Data((0..<16).map { _ in UInt8.random(in: (UInt8.min...UInt8.max), using: &cgThreadRandomGenerator) })
|
||||
let randomGroupPublicKey: String = KeyPairUtilities.generate(from: data).x25519KeyPair.hexEncodedPublicKey
|
||||
let groupNameLength: Int = ((5..<20).randomElement(using: &cgThreadRandomGenerator) ?? 0)
|
||||
let groupName: String = (0..<groupNameLength)
|
||||
.compactMap { _ in stringContent.randomElement(using: &cgThreadRandomGenerator) }
|
||||
.joined()
|
||||
let numGroupMembers: Int = ((0..<5).randomElement(using: &cgThreadRandomGenerator) ?? 0)
|
||||
let numMessages: Int = ((0..<maxMessagesPerThread).randomElement(using: &cgThreadRandomGenerator) ?? 0)
|
||||
|
||||
// Generate the Contacts in the group
|
||||
var members: [String] = [userSessionId]
|
||||
|
||||
(0..<numGroupMembers).forEach { _ in
|
||||
let contactData = Data((0..<16).map { _ in UInt8.random(in: (UInt8.min...UInt8.max), using: &cgThreadRandomGenerator) })
|
||||
let randomSessionId: String = KeyPairUtilities.generate(from: contactData).x25519KeyPair.hexEncodedPublicKey
|
||||
let contactNameLength: Int = ((5..<20).randomElement(using: &cgThreadRandomGenerator) ?? 0)
|
||||
let contact = Contact(sessionID: randomSessionId)
|
||||
contact.name = (0..<contactNameLength)
|
||||
.compactMap { _ in stringContent.randomElement(using: &cgThreadRandomGenerator) }
|
||||
.joined()
|
||||
Storage.shared.setContact(contact, using: transaction)
|
||||
|
||||
members.append(randomSessionId)
|
||||
}
|
||||
|
||||
let groupId: Data = LKGroupUtilities.getEncodedClosedGroupIDAsData(randomGroupPublicKey)
|
||||
let group: TSGroupModel = TSGroupModel(
|
||||
title: groupName,
|
||||
memberIds: members,
|
||||
image: nil,
|
||||
groupId: groupId,
|
||||
groupType: .closedGroup,
|
||||
adminIds: [members.randomElement(using: &cgThreadRandomGenerator) ?? userSessionId]
|
||||
)
|
||||
let thread = TSGroupThread.getOrCreateThread(with: group, transaction: transaction)
|
||||
thread.shouldBeVisible = true
|
||||
thread.save(with: transaction)
|
||||
|
||||
// Add the group to the user's set of public keys to poll for and store the key pair
|
||||
let encryptionKeyPair = Curve25519.generateKeyPair()
|
||||
Storage.shared.addClosedGroupPublicKey(randomGroupPublicKey, using: transaction)
|
||||
Storage.shared.addClosedGroupEncryptionKeyPair(encryptionKeyPair, for: randomGroupPublicKey, using: transaction)
|
||||
|
||||
// Generate the message history (Note: Unapproved message requests will only include incoming messages)
|
||||
(0..<numMessages).forEach { index in
|
||||
let messageLength: Int = ((3..<40).randomElement(using: &dmThreadRandomGenerator) ?? 0)
|
||||
let message: VisibleMessage = VisibleMessage()
|
||||
message.sender = (members.randomElement(using: &cgThreadRandomGenerator) ?? userSessionId)
|
||||
message.sentTimestamp = UInt64(floor(timestampNow - Double(index * 5)))
|
||||
message.text = (0..<messageLength)
|
||||
.compactMap { _ in stringContent.randomElement(using: &dmThreadRandomGenerator) }
|
||||
.joined()
|
||||
|
||||
if message.sender != userSessionId {
|
||||
let tsMessage: TSOutgoingMessage = TSOutgoingMessage.from(message, associatedWith: thread, using: transaction)
|
||||
tsMessage.save(with: transaction)
|
||||
}
|
||||
else {
|
||||
let tsMessage: TSIncomingMessage = TSIncomingMessage.from(message, quotedMessage: nil, linkPreview: nil, associatedWith: thread)
|
||||
tsMessage.save(with: transaction)
|
||||
}
|
||||
}
|
||||
|
||||
// Save the thread
|
||||
thread.save(with: transaction)
|
||||
}
|
||||
|
||||
// MARK: - --Open Group
|
||||
var ogThreadRandomGenerator: ARC4RandomNumberGenerator = ARC4RandomNumberGenerator(seed: ogRandomSeed)
|
||||
|
||||
(0..<openGroupThreadCount).forEach { threadIndex in
|
||||
let data = Data((0..<16).map { _ in UInt8.random(in: (UInt8.min...UInt8.max), using: &ogThreadRandomGenerator) })
|
||||
let randomGroupPublicKey: String = KeyPairUtilities.generate(from: data).x25519KeyPair.hexEncodedPublicKey
|
||||
let serverNameLength: Int = ((5..<20).randomElement(using: &ogThreadRandomGenerator) ?? 0)
|
||||
let roomNameLength: Int = ((5..<20).randomElement(using: &ogThreadRandomGenerator) ?? 0)
|
||||
let serverName: String = (0..<serverNameLength)
|
||||
.compactMap { _ in stringContent.randomElement(using: &ogThreadRandomGenerator) }
|
||||
.joined()
|
||||
let roomName: String = (0..<roomNameLength)
|
||||
.compactMap { _ in stringContent.randomElement(using: &ogThreadRandomGenerator) }
|
||||
.joined()
|
||||
|
||||
// Create the open group model and the thread
|
||||
let openGroup: OpenGroupV2 = OpenGroupV2(server: serverName, room: roomName, name: roomName, publicKey: randomGroupPublicKey, imageID: nil)
|
||||
let groupId: Data = LKGroupUtilities.getEncodedOpenGroupIDAsData(openGroup.id)
|
||||
let model = TSGroupModel(title: openGroup.name, memberIds: [ userSessionId ], image: nil, groupId: groupId, groupType: .openGroup, adminIds: [])
|
||||
|
||||
let thread = TSGroupThread.getOrCreateThread(with: model, transaction: transaction)
|
||||
thread.shouldBeVisible = true
|
||||
thread.save(with: transaction)
|
||||
|
||||
Storage.shared.setV2OpenGroup(openGroup, for: thread.uniqueId!, using: transaction)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,14 +0,0 @@
|
|||
// Created by Michael Kirk on 12/23/16.
|
||||
// Copyright © 2016 Open Whisper Systems. All rights reserved.
|
||||
|
||||
import Foundation
|
||||
|
||||
struct Platform {
|
||||
static let isSimulator: Bool = {
|
||||
var isSim = false
|
||||
#if arch(i386) || arch(x86_64)
|
||||
isSim = true
|
||||
#endif
|
||||
return isSim
|
||||
}()
|
||||
}
|
|
@ -1,16 +0,0 @@
|
|||
//
|
||||
// Copyright (c) 2017 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
@interface UIViewController (CameraPermissions)
|
||||
|
||||
- (void)ows_askForCameraPermissions:(void (^)(void))permissionsGrantedCallback;
|
||||
|
||||
- (void)ows_askForCameraPermissions:(void (^)(void))permissionsGrantedCallback
|
||||
failureCallback:(nullable void (^)(void))failureCallback;
|
||||
|
||||
@end
|
||||
NS_ASSUME_NONNULL_END
|
|
@ -12,6 +12,12 @@ public class Contact : NSObject, NSCoding { // NSObject/NSCoding conformance is
|
|||
@objc public var threadID: String?
|
||||
/// This flag is used to determine whether we should auto-download files sent by this contact.
|
||||
@objc public var isTrusted = false
|
||||
/// This flag is used to determine whether message requests from this contact are approved
|
||||
@objc public var isApproved = false
|
||||
/// This flag is used to determine whether message requests from this contact are blocked
|
||||
@objc public var isBlocked = false
|
||||
/// This flag is used to determine whether this contact has approved the current users message request
|
||||
@objc public var didApproveMe = false
|
||||
|
||||
// MARK: Name
|
||||
/// The name of the contact. Use this whenever you need the "real", underlying name of a user (e.g. when sending a message).
|
||||
|
@ -65,6 +71,10 @@ public class Contact : NSObject, NSCoding { // NSObject/NSCoding conformance is
|
|||
if let profilePictureFileName = coder.decodeObject(forKey: "profilePictureFileName") as! String? { self.profilePictureFileName = profilePictureFileName }
|
||||
if let profileEncryptionKey = coder.decodeObject(forKey: "profilePictureEncryptionKey") as! OWSAES256Key? { self.profileEncryptionKey = profileEncryptionKey }
|
||||
if let threadID = coder.decodeObject(forKey: "threadID") as! String? { self.threadID = threadID }
|
||||
|
||||
isApproved = coder.decodeBool(forKey: "isApproved")
|
||||
isBlocked = coder.decodeBool(forKey: "isBlocked")
|
||||
didApproveMe = coder.decodeBool(forKey: "didApproveMe")
|
||||
}
|
||||
|
||||
public func encode(with coder: NSCoder) {
|
||||
|
@ -76,6 +86,9 @@ public class Contact : NSObject, NSCoding { // NSObject/NSCoding conformance is
|
|||
coder.encode(profileEncryptionKey, forKey: "profilePictureEncryptionKey")
|
||||
coder.encode(threadID, forKey: "threadID")
|
||||
coder.encode(isTrusted, forKey: "isTrusted")
|
||||
coder.encode(isApproved, forKey: "isApproved")
|
||||
coder.encode(isBlocked, forKey: "isBlocked")
|
||||
coder.encode(didApproveMe, forKey: "didApproveMe")
|
||||
}
|
||||
|
||||
// MARK: Equality
|
||||
|
|
|
@ -55,10 +55,16 @@ extension Storage {
|
|||
@objc public func getAllContacts() -> Set<Contact> {
|
||||
var result: Set<Contact> = []
|
||||
Storage.read { transaction in
|
||||
transaction.enumerateRows(inCollection: Storage.contactCollection) { _, object, _, _ in
|
||||
guard let contact = object as? Contact else { return }
|
||||
result.insert(contact)
|
||||
}
|
||||
result = self.getAllContacts(with: transaction)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
@objc public func getAllContacts(with transaction: YapDatabaseReadTransaction) -> Set<Contact> {
|
||||
var result: Set<Contact> = []
|
||||
transaction.enumerateRows(inCollection: Storage.contactCollection) { _, object, _, _ in
|
||||
guard let contact = object as? Contact else { return }
|
||||
result.insert(contact)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
|
|
@ -8,11 +8,14 @@
|
|||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
extern NSString *const TSInboxGroup;
|
||||
extern NSString *const TSMessageRequestGroup;
|
||||
extern NSString *const TSArchiveGroup;
|
||||
extern NSString *const TSShareExtensionGroup;
|
||||
extern NSString *const TSUnreadIncomingMessagesGroup;
|
||||
extern NSString *const TSSecondaryDevicesGroup;
|
||||
|
||||
extern NSString *const TSThreadDatabaseViewExtensionName;
|
||||
extern NSString *const TSThreadShareExtensionDatabaseViewExtensionName;
|
||||
|
||||
extern NSString *const TSMessageDatabaseViewExtensionName;
|
||||
extern NSString *const TSMessageDatabaseViewExtensionName_Legacy;
|
||||
|
|
|
@ -9,14 +9,20 @@
|
|||
#import "TSIncomingMessage.h"
|
||||
#import "TSOutgoingMessage.h"
|
||||
#import "TSThread.h"
|
||||
#import "OWSBlockingManager.h"
|
||||
#import <YapDatabase/YapDatabaseAutoView.h>
|
||||
#import <YapDatabase/YapDatabaseCrossProcessNotification.h>
|
||||
#import <YapDatabase/YapDatabaseViewTypes.h>
|
||||
#import <SessionUtilitiesKit/AppContext.h>
|
||||
#import <SessionUtilitiesKit/SessionUtilitiesKit-Swift.h>
|
||||
#import <SessionMessagingKit/SessionMessagingKit-Swift.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
NSString *const TSInboxGroup = @"TSInboxGroup";
|
||||
NSString *const TSMessageRequestGroup = @"TSMessageRequestGroup";
|
||||
NSString *const TSArchiveGroup = @"TSArchiveGroup";
|
||||
NSString *const TSShareExtensionGroup = @"TSShareExtensionGroup";
|
||||
|
||||
NSString *const TSUnreadIncomingMessagesGroup = @"TSUnreadIncomingMessagesGroup";
|
||||
NSString *const TSSecondaryDevicesGroup = @"TSSecondaryDevicesGroup";
|
||||
|
@ -25,6 +31,8 @@ NSString *const TSSecondaryDevicesGroup = @"TSSecondaryDevicesGroup";
|
|||
// -> TSThreadDatabaseViewExtensionName2 to work around https://github.com/yapstudios/YapDatabase/issues/324
|
||||
NSString *const TSThreadDatabaseViewExtensionName = @"TSThreadDatabaseViewExtensionName2";
|
||||
|
||||
NSString *const TSThreadShareExtensionDatabaseViewExtensionName = @"TSThreadShareExtensionDatabaseViewExtensionName";
|
||||
|
||||
// We sort interactions by a monotonically increasing counter.
|
||||
//
|
||||
// Previously we sorted the interactions database by local timestamp, which was problematic if the local clock changed.
|
||||
|
@ -234,7 +242,15 @@ NSString *const TSLazyRestoreAttachmentsGroup = @"TSLazyRestoreAttachmentsGroup"
|
|||
}
|
||||
TSThread *thread = (TSThread *)object;
|
||||
|
||||
if (thread.shouldBeVisible) {
|
||||
if ([thread isMessageRequestUsingTransaction:transaction]) {
|
||||
// Don't show blocked threads at all
|
||||
if ([[OWSBlockingManager sharedManager] isThreadBlocked: thread]) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
return TSMessageRequestGroup;
|
||||
}
|
||||
else if (thread.shouldBeVisible) {
|
||||
// Do nothing; we never hide threads that have ever had a message.
|
||||
} else {
|
||||
YapDatabaseViewTransaction *viewTransaction = [transaction ext:TSMessageDatabaseViewExtensionName];
|
||||
|
@ -258,6 +274,53 @@ NSString *const TSLazyRestoreAttachmentsGroup = @"TSLazyRestoreAttachmentsGroup"
|
|||
[[YapDatabaseAutoView alloc] initWithGrouping:viewGrouping sorting:viewSorting versionTag:@"4" options:options];
|
||||
|
||||
[storage asyncRegisterExtension:databaseView withName:TSThreadDatabaseViewExtensionName];
|
||||
|
||||
YapDatabaseView *shareExtensionThreadView = [storage registeredExtension:TSThreadShareExtensionDatabaseViewExtensionName];
|
||||
if (shareExtensionThreadView) {
|
||||
return;
|
||||
}
|
||||
|
||||
YapDatabaseViewGrouping *shareExtensionViewGrouping = [YapDatabaseViewGrouping withObjectBlock:^NSString *(
|
||||
YapDatabaseReadTransaction *transaction, NSString *collection, NSString *key, id object) {
|
||||
if (![object isKindOfClass:[TSThread class]]) {
|
||||
return nil;
|
||||
}
|
||||
TSThread *thread = (TSThread *)object;
|
||||
|
||||
if (thread.isMessageRequest) {
|
||||
return nil;
|
||||
}
|
||||
else {
|
||||
YapDatabaseViewTransaction *viewTransaction = [transaction ext:TSMessageDatabaseViewExtensionName];
|
||||
NSUInteger threadMessageCount = [viewTransaction numberOfItemsInGroup:thread.uniqueId];
|
||||
if (threadMessageCount < 1) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
if (!thread.isGroupThread) {
|
||||
TSContactThread *contactThead = (TSContactThread *)thread;
|
||||
SNContact *contact = [LKStorage.shared getContactWithSessionID:[contactThead contactSessionID]];
|
||||
|
||||
if (contact == nil || !contact.didApproveMe) {
|
||||
return nil;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return TSShareExtensionGroup;
|
||||
}];
|
||||
|
||||
YapDatabaseViewSorting *shareExtensionViewSorting = [self threadSorting];
|
||||
|
||||
YapDatabaseViewOptions *shareExtensionOptions = [[YapDatabaseViewOptions alloc] init];
|
||||
shareExtensionOptions.isPersistent = YES;
|
||||
shareExtensionOptions.allowedCollections =
|
||||
[[YapWhitelistBlacklist alloc] initWithWhitelist:[NSSet setWithObject:[TSThread collection]]];
|
||||
|
||||
YapDatabaseView *shareExtensionDatabaseView =
|
||||
[[YapDatabaseAutoView alloc] initWithGrouping:shareExtensionViewGrouping sorting:shareExtensionViewSorting versionTag:@"1" options:shareExtensionOptions];
|
||||
|
||||
[storage asyncRegisterExtension:shareExtensionDatabaseView withName:TSThreadShareExtensionDatabaseViewExtensionName];
|
||||
}
|
||||
|
||||
+ (YapDatabaseViewSorting *)threadSorting {
|
||||
|
|
|
@ -0,0 +1,111 @@
|
|||
|
||||
extension ConfigurationMessage {
|
||||
|
||||
public static func getCurrent(with transaction: YapDatabaseReadWriteTransaction? = nil) -> ConfigurationMessage? {
|
||||
let storage = Storage.shared
|
||||
guard let user = storage.getUser() else { return nil }
|
||||
|
||||
let displayName = user.name
|
||||
let profilePictureURL = user.profilePictureURL
|
||||
let profileKey = user.profileEncryptionKey?.keyData
|
||||
var closedGroups: Set<ClosedGroup> = []
|
||||
var openGroups: Set<String> = []
|
||||
var contacts: Set<Contact> = []
|
||||
var contactCount = 0
|
||||
|
||||
let populateDataClosure: (YapDatabaseReadTransaction) -> () = { transaction in
|
||||
TSGroupThread.enumerateCollectionObjects(with: transaction) { object, _ in
|
||||
guard let thread = object as? TSGroupThread else { return }
|
||||
|
||||
switch thread.groupModel.groupType {
|
||||
case .closedGroup:
|
||||
guard thread.isCurrentUserMemberInGroup() else { return }
|
||||
|
||||
let groupID = thread.groupModel.groupId
|
||||
let groupPublicKey = LKGroupUtilities.getDecodedGroupID(groupID)
|
||||
|
||||
guard storage.isClosedGroup(groupPublicKey), let encryptionKeyPair = storage.getLatestClosedGroupEncryptionKeyPair(for: groupPublicKey) else {
|
||||
return
|
||||
}
|
||||
|
||||
let closedGroup = ClosedGroup(
|
||||
publicKey: groupPublicKey,
|
||||
name: thread.groupModel.groupName!,
|
||||
encryptionKeyPair: encryptionKeyPair,
|
||||
members: Set(thread.groupModel.groupMemberIds),
|
||||
admins: Set(thread.groupModel.groupAdminIds),
|
||||
expirationTimer: thread.disappearingMessagesDuration(with: transaction)
|
||||
)
|
||||
closedGroups.insert(closedGroup)
|
||||
|
||||
case .openGroup:
|
||||
if let v2OpenGroup = storage.getV2OpenGroup(for: thread.uniqueId!) {
|
||||
openGroups.insert("\(v2OpenGroup.server)/\(v2OpenGroup.room)?public_key=\(v2OpenGroup.publicKey)")
|
||||
}
|
||||
|
||||
default: break
|
||||
}
|
||||
}
|
||||
|
||||
let currentUserPublicKey: String = getUserHexEncodedPublicKey()
|
||||
var truncatedContacts = storage.getAllContacts(with: transaction)
|
||||
|
||||
if truncatedContacts.count > 200 {
|
||||
truncatedContacts = Set(Array(truncatedContacts)[0..<200])
|
||||
}
|
||||
|
||||
truncatedContacts.forEach { contact in
|
||||
let publicKey = contact.sessionID
|
||||
let threadID = TSContactThread.threadID(fromContactSessionID: publicKey)
|
||||
|
||||
// Want to sync contacts for visible threads and blocked contacts between devices
|
||||
guard
|
||||
publicKey != currentUserPublicKey && (
|
||||
TSContactThread.fetch(uniqueId: threadID, transaction: transaction)?.shouldBeVisible == true ||
|
||||
SSKEnvironment.shared.blockingManager.isRecipientIdBlocked(publicKey)
|
||||
)
|
||||
else {
|
||||
return
|
||||
}
|
||||
|
||||
// Can just default the 'hasX' values to true as they will be set to this
|
||||
// when converting to proto anyway
|
||||
let profilePictureURL = contact.profilePictureURL
|
||||
let profileKey = contact.profileEncryptionKey?.keyData
|
||||
let contact = ConfigurationMessage.Contact(
|
||||
publicKey: publicKey,
|
||||
displayName: (contact.name ?? publicKey),
|
||||
profilePictureURL: profilePictureURL,
|
||||
profileKey: profileKey,
|
||||
hasIsApproved: true,
|
||||
isApproved: contact.isApproved,
|
||||
hasIsBlocked: true,
|
||||
isBlocked: contact.isBlocked,
|
||||
hasDidApproveMe: true,
|
||||
didApproveMe: contact.didApproveMe
|
||||
)
|
||||
|
||||
contacts.insert(contact)
|
||||
contactCount += 1
|
||||
}
|
||||
}
|
||||
|
||||
// If we are provided with a transaction then read the data based on the state of the database
|
||||
// from within the transaction rather than the state in disk
|
||||
if let transaction: YapDatabaseReadWriteTransaction = transaction {
|
||||
populateDataClosure(transaction)
|
||||
}
|
||||
else {
|
||||
Storage.read { transaction in populateDataClosure(transaction) }
|
||||
}
|
||||
|
||||
return ConfigurationMessage(
|
||||
displayName: displayName,
|
||||
profilePictureURL: profilePictureURL,
|
||||
profileKey: profileKey,
|
||||
closedGroups: closedGroups,
|
||||
openGroups: openGroups,
|
||||
contacts: contacts
|
||||
)
|
||||
}
|
||||
}
|
|
@ -193,14 +193,38 @@ extension ConfigurationMessage {
|
|||
public var displayName: String?
|
||||
public var profilePictureURL: String?
|
||||
public var profileKey: Data?
|
||||
|
||||
public var hasIsApproved: Bool
|
||||
public var isApproved: Bool
|
||||
public var hasIsBlocked: Bool
|
||||
public var isBlocked: Bool
|
||||
public var hasDidApproveMe: Bool
|
||||
public var didApproveMe: Bool
|
||||
|
||||
public var isValid: Bool { publicKey != nil && displayName != nil }
|
||||
|
||||
public init(publicKey: String, displayName: String, profilePictureURL: String?, profileKey: Data?) {
|
||||
public init(
|
||||
publicKey: String,
|
||||
displayName: String,
|
||||
profilePictureURL: String?,
|
||||
profileKey: Data?,
|
||||
hasIsApproved: Bool,
|
||||
isApproved: Bool,
|
||||
hasIsBlocked: Bool,
|
||||
isBlocked: Bool,
|
||||
hasDidApproveMe: Bool,
|
||||
didApproveMe: Bool
|
||||
) {
|
||||
self.publicKey = publicKey
|
||||
self.displayName = displayName
|
||||
self.profilePictureURL = profilePictureURL
|
||||
self.profileKey = profileKey
|
||||
self.hasIsApproved = hasIsApproved
|
||||
self.isApproved = isApproved
|
||||
self.hasIsBlocked = hasIsBlocked
|
||||
self.isBlocked = isBlocked
|
||||
self.hasDidApproveMe = hasDidApproveMe
|
||||
self.didApproveMe = didApproveMe
|
||||
}
|
||||
|
||||
public required init?(coder: NSCoder) {
|
||||
|
@ -210,6 +234,12 @@ extension ConfigurationMessage {
|
|||
self.displayName = displayName
|
||||
self.profilePictureURL = coder.decodeObject(forKey: "profilePictureURL") as! String?
|
||||
self.profileKey = coder.decodeObject(forKey: "profileKey") as! Data?
|
||||
self.hasIsApproved = (coder.decodeObject(forKey: "hasIsApproved") as? Bool ?? false)
|
||||
self.isApproved = (coder.decodeObject(forKey: "isApproved") as? Bool ?? false)
|
||||
self.hasIsBlocked = (coder.decodeObject(forKey: "hasIsBlocked") as? Bool ?? false)
|
||||
self.isBlocked = (coder.decodeObject(forKey: "isBlocked") as? Bool ?? false)
|
||||
self.hasDidApproveMe = (coder.decodeObject(forKey: "hasDidApproveMe") as? Bool ?? false)
|
||||
self.didApproveMe = (coder.decodeObject(forKey: "didApproveMe") as? Bool ?? false)
|
||||
}
|
||||
|
||||
public func encode(with coder: NSCoder) {
|
||||
|
@ -217,14 +247,28 @@ extension ConfigurationMessage {
|
|||
coder.encode(displayName, forKey: "displayName")
|
||||
coder.encode(profilePictureURL, forKey: "profilePictureURL")
|
||||
coder.encode(profileKey, forKey: "profileKey")
|
||||
coder.encode(hasIsApproved, forKey: "hasIsApproved")
|
||||
coder.encode(isApproved, forKey: "isApproved")
|
||||
coder.encode(hasIsBlocked, forKey: "hasIsBlocked")
|
||||
coder.encode(isBlocked, forKey: "isBlocked")
|
||||
coder.encode(hasDidApproveMe, forKey: "hasDidApproveMe")
|
||||
coder.encode(didApproveMe, forKey: "didApproveMe")
|
||||
}
|
||||
|
||||
public static func fromProto(_ proto: SNProtoConfigurationMessageContact) -> Contact? {
|
||||
let publicKey = proto.publicKey.toHexString()
|
||||
let displayName = proto.name
|
||||
let profilePictureURL = proto.profilePicture
|
||||
let profileKey = proto.profileKey
|
||||
let result = Contact(publicKey: publicKey, displayName: displayName, profilePictureURL: profilePictureURL, profileKey: profileKey)
|
||||
let result: Contact = Contact(
|
||||
publicKey: proto.publicKey.toHexString(),
|
||||
displayName: proto.name,
|
||||
profilePictureURL: proto.profilePicture,
|
||||
profileKey: proto.profileKey,
|
||||
hasIsApproved: proto.hasIsApproved,
|
||||
isApproved: proto.isApproved,
|
||||
hasIsBlocked: proto.hasIsBlocked,
|
||||
isBlocked: proto.isBlocked,
|
||||
hasDidApproveMe: proto.hasDidApproveMe,
|
||||
didApproveMe: proto.didApproveMe
|
||||
)
|
||||
|
||||
guard result.isValid else { return nil }
|
||||
return result
|
||||
}
|
||||
|
@ -235,6 +279,11 @@ extension ConfigurationMessage {
|
|||
let result = SNProtoConfigurationMessageContact.builder(publicKey: Data(hex: publicKey), name: displayName)
|
||||
if let profilePictureURL = profilePictureURL { result.setProfilePicture(profilePictureURL) }
|
||||
if let profileKey = profileKey { result.setProfileKey(profileKey) }
|
||||
|
||||
if hasIsApproved { result.setIsApproved(isApproved) }
|
||||
if hasIsBlocked { result.setIsBlocked(isBlocked) }
|
||||
if hasDidApproveMe { result.setDidApproveMe(didApproveMe) }
|
||||
|
||||
do {
|
||||
return try result.build()
|
||||
} catch {
|
||||
|
|
|
@ -0,0 +1,63 @@
|
|||
import SessionUtilitiesKit
|
||||
|
||||
@objc(SNMessageRequestResponse)
|
||||
public final class MessageRequestResponse: ControlMessage {
|
||||
public var isApproved: Bool
|
||||
|
||||
// MARK: - Initialization
|
||||
|
||||
public init(isApproved: Bool) {
|
||||
self.isApproved = isApproved
|
||||
|
||||
super.init()
|
||||
}
|
||||
|
||||
// MARK: - Coding
|
||||
|
||||
public required init?(coder: NSCoder) {
|
||||
guard let isApproved: Bool = coder.decodeObject(forKey: "isApproved") as? Bool else { return nil }
|
||||
|
||||
self.isApproved = isApproved
|
||||
|
||||
super.init(coder: coder)
|
||||
}
|
||||
|
||||
public override func encode(with coder: NSCoder) {
|
||||
super.encode(with: coder)
|
||||
|
||||
coder.encode(isApproved, forKey: "isApproved")
|
||||
}
|
||||
|
||||
// MARK: - Proto Conversion
|
||||
|
||||
public override class func fromProto(_ proto: SNProtoContent) -> MessageRequestResponse? {
|
||||
guard let messageRequestResponseProto = proto.messageRequestResponse else { return nil }
|
||||
|
||||
let isApproved = messageRequestResponseProto.isApproved
|
||||
|
||||
return MessageRequestResponse(isApproved: isApproved)
|
||||
}
|
||||
|
||||
public override func toProto(using transaction: YapDatabaseReadWriteTransaction) -> SNProtoContent? {
|
||||
let messageRequestResponseProto = SNProtoMessageRequestResponse.builder(isApproved: isApproved)
|
||||
let contentProto = SNProtoContent.builder()
|
||||
|
||||
do {
|
||||
contentProto.setMessageRequestResponse(try messageRequestResponseProto.build())
|
||||
return try contentProto.build()
|
||||
} catch {
|
||||
SNLog("Couldn't construct unsend request proto from: \(self).")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Description
|
||||
|
||||
public override var description: String {
|
||||
"""
|
||||
MessageRequestResponse(
|
||||
isApproved: \(isApproved)
|
||||
)
|
||||
"""
|
||||
}
|
||||
}
|
|
@ -140,16 +140,16 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
return YES;
|
||||
}
|
||||
|
||||
- (void)markAsReadNowWithSendReadReceipt:(BOOL)sendReadReceipt
|
||||
- (void)markAsReadNowWithTrySendReadReceipt:(BOOL)trySendReadReceipt
|
||||
transaction:(YapDatabaseReadWriteTransaction *)transaction;
|
||||
{
|
||||
[self markAsReadAtTimestamp:[NSDate millisecondTimestamp]
|
||||
sendReadReceipt:sendReadReceipt
|
||||
trySendReadReceipt:trySendReadReceipt
|
||||
transaction:transaction];
|
||||
}
|
||||
|
||||
- (void)markAsReadAtTimestamp:(uint64_t)readTimestamp
|
||||
sendReadReceipt:(BOOL)sendReadReceipt
|
||||
trySendReadReceipt:(BOOL)trySendReadReceipt
|
||||
transaction:(YapDatabaseReadWriteTransaction *)transaction;
|
||||
{
|
||||
if (_read && readTimestamp >= self.expireStartedAt) {
|
||||
|
@ -174,7 +174,7 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
expirationStartedAt:readTimestamp
|
||||
transaction:transaction];
|
||||
|
||||
if (sendReadReceipt) {
|
||||
if (trySendReadReceipt) {
|
||||
[OWSReadReceiptManager.sharedManager messageWasReadLocally:self];
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,7 +16,8 @@ typedef NS_ENUM(NSInteger, TSInfoMessageType) {
|
|||
TSInfoMessageTypeDisappearingMessagesUpdate,
|
||||
TSInfoMessageTypeScreenshotNotification,
|
||||
TSInfoMessageTypeMediaSavedNotification,
|
||||
TSInfoMessageTypeCall
|
||||
TSInfoMessageTypeCall,
|
||||
TSInfoMessageTypeMessageRequestAccepted
|
||||
};
|
||||
|
||||
typedef NS_ENUM(NSInteger, TSInfoMessageCallState) {
|
||||
|
|
|
@ -141,6 +141,8 @@ NSUInteger TSInfoMessageSchemaVersion = 1;
|
|||
return _customMessage != nil ? _customMessage : NSLocalizedString(@"GROUP_UPDATED", @"");
|
||||
case TSInfoMessageTypeCall:
|
||||
return [self getCallMessagePreviewTextWithTransaction:transaction];
|
||||
case TSInfoMessageTypeMessageRequestAccepted:
|
||||
return NSLocalizedString(@"MESSAGE_REQUESTS_ACCEPTED", @"");
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
@ -166,7 +168,7 @@ NSUInteger TSInfoMessageSchemaVersion = 1;
|
|||
}
|
||||
|
||||
- (void)markAsReadAtTimestamp:(uint64_t)readTimestamp
|
||||
sendReadReceipt:(BOOL)sendReadReceipt
|
||||
trySendReadReceipt:(BOOL)trySendReadReceipt
|
||||
transaction:(YapDatabaseReadWriteTransaction *)transaction
|
||||
{
|
||||
if (_read) {
|
||||
|
@ -176,7 +178,7 @@ NSUInteger TSInfoMessageSchemaVersion = 1;
|
|||
_read = YES;
|
||||
[self saveWithTransaction:transaction];
|
||||
|
||||
// Ignore sendReadReceipt, it doesn't apply to info messages.
|
||||
// Ignore trySendReadReceipt, it doesn't apply to info messages.
|
||||
}
|
||||
|
||||
@end
|
||||
|
|
|
@ -117,7 +117,8 @@ public final class OpenGroupManagerV2 : NSObject {
|
|||
// https://143.198.213.225:443/main?public_key=658d29b91892a2389505596b135e76a53db6e11d613a51dbd3d0816adffb231c
|
||||
// 143.198.213.255:80/main?public_key=658d29b91892a2389505596b135e76a53db6e11d613a51dbd3d0816adffb231c
|
||||
let useTLS = (url.scheme == "https")
|
||||
let room = String(url.path.dropFirst()) // Drop the leading slash
|
||||
let updatedPath = (url.path.starts(with: "/r/") ? url.path.substring(from: 2) : url.path)
|
||||
let room = String(updatedPath.dropFirst()) // Drop the leading slash
|
||||
let queryParts = query.split(separator: "=")
|
||||
guard !room.isEmpty && !room.contains("/"), queryParts.count == 2, queryParts[0] == "public_key" else { return nil }
|
||||
let publicKey = String(queryParts[1])
|
||||
|
|
|
@ -450,6 +450,103 @@ extension SNProtoUnsendRequest.SNProtoUnsendRequestBuilder {
|
|||
|
||||
#endif
|
||||
|
||||
// MARK: - SNProtoMessageRequestResponse
|
||||
|
||||
@objc public class SNProtoMessageRequestResponse: NSObject {
|
||||
|
||||
// MARK: - SNProtoMessageRequestResponseBuilder
|
||||
|
||||
@objc public class func builder(isApproved: Bool) -> SNProtoMessageRequestResponseBuilder {
|
||||
return SNProtoMessageRequestResponseBuilder(isApproved: isApproved)
|
||||
}
|
||||
|
||||
// asBuilder() constructs a builder that reflects the proto's contents.
|
||||
@objc public func asBuilder() -> SNProtoMessageRequestResponseBuilder {
|
||||
let builder = SNProtoMessageRequestResponseBuilder(isApproved: isApproved)
|
||||
return builder
|
||||
}
|
||||
|
||||
@objc public class SNProtoMessageRequestResponseBuilder: NSObject {
|
||||
|
||||
private var proto = SessionProtos_MessageRequestResponse()
|
||||
|
||||
@objc fileprivate override init() {}
|
||||
|
||||
@objc fileprivate init(isApproved: Bool) {
|
||||
super.init()
|
||||
|
||||
setIsApproved(isApproved)
|
||||
}
|
||||
|
||||
@objc public func setIsApproved(_ valueParam: Bool) {
|
||||
proto.isApproved = valueParam
|
||||
}
|
||||
|
||||
@objc public func build() throws -> SNProtoMessageRequestResponse {
|
||||
return try SNProtoMessageRequestResponse.parseProto(proto)
|
||||
}
|
||||
|
||||
@objc public func buildSerializedData() throws -> Data {
|
||||
return try SNProtoMessageRequestResponse.parseProto(proto).serializedData()
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate let proto: SessionProtos_MessageRequestResponse
|
||||
|
||||
@objc public let isApproved: Bool
|
||||
|
||||
private init(proto: SessionProtos_MessageRequestResponse,
|
||||
isApproved: Bool) {
|
||||
self.proto = proto
|
||||
self.isApproved = isApproved
|
||||
}
|
||||
|
||||
@objc
|
||||
public func serializedData() throws -> Data {
|
||||
return try self.proto.serializedData()
|
||||
}
|
||||
|
||||
@objc public class func parseData(_ serializedData: Data) throws -> SNProtoMessageRequestResponse {
|
||||
let proto = try SessionProtos_MessageRequestResponse(serializedData: serializedData)
|
||||
return try parseProto(proto)
|
||||
}
|
||||
|
||||
fileprivate class func parseProto(_ proto: SessionProtos_MessageRequestResponse) throws -> SNProtoMessageRequestResponse {
|
||||
guard proto.hasIsApproved else {
|
||||
throw SNProtoError.invalidProtobuf(description: "\(logTag) missing required field: isApproved")
|
||||
}
|
||||
let isApproved = proto.isApproved
|
||||
|
||||
// MARK: - Begin Validation Logic for SNProtoMessageRequestResponse -
|
||||
|
||||
// MARK: - End Validation Logic for SNProtoMessageRequestResponse -
|
||||
|
||||
let result = SNProtoMessageRequestResponse(proto: proto,
|
||||
isApproved: isApproved)
|
||||
return result
|
||||
}
|
||||
|
||||
@objc public override var debugDescription: String {
|
||||
return "\(proto)"
|
||||
}
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
|
||||
extension SNProtoMessageRequestResponse {
|
||||
@objc public func serializedDataIgnoringErrors() -> Data? {
|
||||
return try! self.serializedData()
|
||||
}
|
||||
}
|
||||
|
||||
extension SNProtoMessageRequestResponse.SNProtoMessageRequestResponseBuilder {
|
||||
@objc public func buildIgnoringErrors() -> SNProtoMessageRequestResponse? {
|
||||
return try! self.build()
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
// MARK: - SNProtoContent
|
||||
|
||||
@objc public class SNProtoContent: NSObject {
|
||||
|
@ -484,6 +581,9 @@ extension SNProtoUnsendRequest.SNProtoUnsendRequestBuilder {
|
|||
if let _value = unsendRequest {
|
||||
builder.setUnsendRequest(_value)
|
||||
}
|
||||
if let _value = messageRequestResponse {
|
||||
builder.setMessageRequestResponse(_value)
|
||||
}
|
||||
return builder
|
||||
}
|
||||
|
||||
|
@ -521,6 +621,10 @@ extension SNProtoUnsendRequest.SNProtoUnsendRequestBuilder {
|
|||
proto.unsendRequest = valueParam.proto
|
||||
}
|
||||
|
||||
@objc public func setMessageRequestResponse(_ valueParam: SNProtoMessageRequestResponse) {
|
||||
proto.messageRequestResponse = valueParam.proto
|
||||
}
|
||||
|
||||
@objc public func build() throws -> SNProtoContent {
|
||||
return try SNProtoContent.parseProto(proto)
|
||||
}
|
||||
|
@ -546,6 +650,8 @@ extension SNProtoUnsendRequest.SNProtoUnsendRequestBuilder {
|
|||
|
||||
@objc public let unsendRequest: SNProtoUnsendRequest?
|
||||
|
||||
@objc public let messageRequestResponse: SNProtoMessageRequestResponse?
|
||||
|
||||
private init(proto: SessionProtos_Content,
|
||||
dataMessage: SNProtoDataMessage?,
|
||||
callMessage: SNProtoCallMessage?,
|
||||
|
@ -553,7 +659,8 @@ extension SNProtoUnsendRequest.SNProtoUnsendRequestBuilder {
|
|||
typingMessage: SNProtoTypingMessage?,
|
||||
configurationMessage: SNProtoConfigurationMessage?,
|
||||
dataExtractionNotification: SNProtoDataExtractionNotification?,
|
||||
unsendRequest: SNProtoUnsendRequest?) {
|
||||
unsendRequest: SNProtoUnsendRequest?,
|
||||
messageRequestResponse: SNProtoMessageRequestResponse?) {
|
||||
self.proto = proto
|
||||
self.dataMessage = dataMessage
|
||||
self.callMessage = callMessage
|
||||
|
@ -562,6 +669,7 @@ extension SNProtoUnsendRequest.SNProtoUnsendRequestBuilder {
|
|||
self.configurationMessage = configurationMessage
|
||||
self.dataExtractionNotification = dataExtractionNotification
|
||||
self.unsendRequest = unsendRequest
|
||||
self.messageRequestResponse = messageRequestResponse
|
||||
}
|
||||
|
||||
@objc
|
||||
|
@ -610,6 +718,11 @@ extension SNProtoUnsendRequest.SNProtoUnsendRequestBuilder {
|
|||
unsendRequest = try SNProtoUnsendRequest.parseProto(proto.unsendRequest)
|
||||
}
|
||||
|
||||
var messageRequestResponse: SNProtoMessageRequestResponse? = nil
|
||||
if proto.hasMessageRequestResponse {
|
||||
messageRequestResponse = try SNProtoMessageRequestResponse.parseProto(proto.messageRequestResponse)
|
||||
}
|
||||
|
||||
// MARK: - Begin Validation Logic for SNProtoContent -
|
||||
|
||||
// MARK: - End Validation Logic for SNProtoContent -
|
||||
|
@ -621,7 +734,8 @@ extension SNProtoUnsendRequest.SNProtoUnsendRequestBuilder {
|
|||
typingMessage: typingMessage,
|
||||
configurationMessage: configurationMessage,
|
||||
dataExtractionNotification: dataExtractionNotification,
|
||||
unsendRequest: unsendRequest)
|
||||
unsendRequest: unsendRequest,
|
||||
messageRequestResponse: messageRequestResponse)
|
||||
return result
|
||||
}
|
||||
|
||||
|
@ -2603,6 +2717,15 @@ extension SNProtoConfigurationMessageClosedGroup.SNProtoConfigurationMessageClos
|
|||
if let _value = profileKey {
|
||||
builder.setProfileKey(_value)
|
||||
}
|
||||
if hasIsApproved {
|
||||
builder.setIsApproved(isApproved)
|
||||
}
|
||||
if hasIsBlocked {
|
||||
builder.setIsBlocked(isBlocked)
|
||||
}
|
||||
if hasDidApproveMe {
|
||||
builder.setDidApproveMe(didApproveMe)
|
||||
}
|
||||
return builder
|
||||
}
|
||||
|
||||
|
@ -2635,6 +2758,18 @@ extension SNProtoConfigurationMessageClosedGroup.SNProtoConfigurationMessageClos
|
|||
proto.profileKey = valueParam
|
||||
}
|
||||
|
||||
@objc public func setIsApproved(_ valueParam: Bool) {
|
||||
proto.isApproved = valueParam
|
||||
}
|
||||
|
||||
@objc public func setIsBlocked(_ valueParam: Bool) {
|
||||
proto.isBlocked = valueParam
|
||||
}
|
||||
|
||||
@objc public func setDidApproveMe(_ valueParam: Bool) {
|
||||
proto.didApproveMe = valueParam
|
||||
}
|
||||
|
||||
@objc public func build() throws -> SNProtoConfigurationMessageContact {
|
||||
return try SNProtoConfigurationMessageContact.parseProto(proto)
|
||||
}
|
||||
|
@ -2670,6 +2805,27 @@ extension SNProtoConfigurationMessageClosedGroup.SNProtoConfigurationMessageClos
|
|||
return proto.hasProfileKey
|
||||
}
|
||||
|
||||
@objc public var isApproved: Bool {
|
||||
return proto.isApproved
|
||||
}
|
||||
@objc public var hasIsApproved: Bool {
|
||||
return proto.hasIsApproved
|
||||
}
|
||||
|
||||
@objc public var isBlocked: Bool {
|
||||
return proto.isBlocked
|
||||
}
|
||||
@objc public var hasIsBlocked: Bool {
|
||||
return proto.hasIsBlocked
|
||||
}
|
||||
|
||||
@objc public var didApproveMe: Bool {
|
||||
return proto.didApproveMe
|
||||
}
|
||||
@objc public var hasDidApproveMe: Bool {
|
||||
return proto.hasDidApproveMe
|
||||
}
|
||||
|
||||
private init(proto: SessionProtos_ConfigurationMessage.Contact,
|
||||
publicKey: Data,
|
||||
name: String) {
|
||||
|
|
|
@ -229,6 +229,28 @@ struct SessionProtos_UnsendRequest {
|
|||
fileprivate var _author: String? = nil
|
||||
}
|
||||
|
||||
struct SessionProtos_MessageRequestResponse {
|
||||
// SwiftProtobuf.Message conformance is added in an extension below. See the
|
||||
// `Message` and `Message+*Additions` files in the SwiftProtobuf library for
|
||||
// methods supported on all messages.
|
||||
|
||||
/// @required
|
||||
var isApproved: Bool {
|
||||
get {return _isApproved ?? false}
|
||||
set {_isApproved = newValue}
|
||||
}
|
||||
/// Returns true if `isApproved` has been explicitly set.
|
||||
var hasIsApproved: Bool {return self._isApproved != nil}
|
||||
/// Clears the value of `isApproved`. Subsequent reads from it will return its default value.
|
||||
mutating func clearIsApproved() {self._isApproved = nil}
|
||||
|
||||
var unknownFields = SwiftProtobuf.UnknownStorage()
|
||||
|
||||
init() {}
|
||||
|
||||
fileprivate var _isApproved: Bool? = nil
|
||||
}
|
||||
|
||||
struct SessionProtos_Content {
|
||||
// SwiftProtobuf.Message conformance is added in an extension below. See the
|
||||
// `Message` and `Message+*Additions` files in the SwiftProtobuf library for
|
||||
|
@ -297,6 +319,15 @@ struct SessionProtos_Content {
|
|||
/// Clears the value of `unsendRequest`. Subsequent reads from it will return its default value.
|
||||
mutating func clearUnsendRequest() {_uniqueStorage()._unsendRequest = nil}
|
||||
|
||||
var messageRequestResponse: SessionProtos_MessageRequestResponse {
|
||||
get {return _storage._messageRequestResponse ?? SessionProtos_MessageRequestResponse()}
|
||||
set {_uniqueStorage()._messageRequestResponse = newValue}
|
||||
}
|
||||
/// Returns true if `messageRequestResponse` has been explicitly set.
|
||||
var hasMessageRequestResponse: Bool {return _storage._messageRequestResponse != nil}
|
||||
/// Clears the value of `messageRequestResponse`. Subsequent reads from it will return its default value.
|
||||
mutating func clearMessageRequestResponse() {_uniqueStorage()._messageRequestResponse = nil}
|
||||
|
||||
var unknownFields = SwiftProtobuf.UnknownStorage()
|
||||
|
||||
init() {}
|
||||
|
@ -1165,6 +1196,36 @@ struct SessionProtos_ConfigurationMessage {
|
|||
/// Clears the value of `profileKey`. Subsequent reads from it will return its default value.
|
||||
mutating func clearProfileKey() {self._profileKey = nil}
|
||||
|
||||
/// added for msg requests
|
||||
var isApproved: Bool {
|
||||
get {return _isApproved ?? false}
|
||||
set {_isApproved = newValue}
|
||||
}
|
||||
/// Returns true if `isApproved` has been explicitly set.
|
||||
var hasIsApproved: Bool {return self._isApproved != nil}
|
||||
/// Clears the value of `isApproved`. Subsequent reads from it will return its default value.
|
||||
mutating func clearIsApproved() {self._isApproved = nil}
|
||||
|
||||
/// added for msg requests
|
||||
var isBlocked: Bool {
|
||||
get {return _isBlocked ?? false}
|
||||
set {_isBlocked = newValue}
|
||||
}
|
||||
/// Returns true if `isBlocked` has been explicitly set.
|
||||
var hasIsBlocked: Bool {return self._isBlocked != nil}
|
||||
/// Clears the value of `isBlocked`. Subsequent reads from it will return its default value.
|
||||
mutating func clearIsBlocked() {self._isBlocked = nil}
|
||||
|
||||
/// added for msg requests
|
||||
var didApproveMe: Bool {
|
||||
get {return _didApproveMe ?? false}
|
||||
set {_didApproveMe = newValue}
|
||||
}
|
||||
/// Returns true if `didApproveMe` has been explicitly set.
|
||||
var hasDidApproveMe: Bool {return self._didApproveMe != nil}
|
||||
/// Clears the value of `didApproveMe`. Subsequent reads from it will return its default value.
|
||||
mutating func clearDidApproveMe() {self._didApproveMe = nil}
|
||||
|
||||
var unknownFields = SwiftProtobuf.UnknownStorage()
|
||||
|
||||
init() {}
|
||||
|
@ -1173,6 +1234,9 @@ struct SessionProtos_ConfigurationMessage {
|
|||
fileprivate var _name: String? = nil
|
||||
fileprivate var _profilePicture: String? = nil
|
||||
fileprivate var _profileKey: Data? = nil
|
||||
fileprivate var _isApproved: Bool? = nil
|
||||
fileprivate var _isBlocked: Bool? = nil
|
||||
fileprivate var _didApproveMe: Bool? = nil
|
||||
}
|
||||
|
||||
init() {}
|
||||
|
@ -1680,6 +1744,43 @@ extension SessionProtos_UnsendRequest: SwiftProtobuf.Message, SwiftProtobuf._Mes
|
|||
}
|
||||
}
|
||||
|
||||
extension SessionProtos_MessageRequestResponse: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding {
|
||||
static let protoMessageName: String = _protobuf_package + ".MessageRequestResponse"
|
||||
static let _protobuf_nameMap: SwiftProtobuf._NameMap = [
|
||||
1: .same(proto: "isApproved"),
|
||||
]
|
||||
|
||||
public var isInitialized: Bool {
|
||||
if self._isApproved == nil {return false}
|
||||
return true
|
||||
}
|
||||
|
||||
mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
|
||||
while let fieldNumber = try decoder.nextFieldNumber() {
|
||||
// The use of inline closures is to circumvent an issue where the compiler
|
||||
// allocates stack space for every case branch when no optimizations are
|
||||
// enabled. https://github.com/apple/swift-protobuf/issues/1034
|
||||
switch fieldNumber {
|
||||
case 1: try { try decoder.decodeSingularBoolField(value: &self._isApproved) }()
|
||||
default: break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func traverse<V: SwiftProtobuf.Visitor>(visitor: inout V) throws {
|
||||
if let v = self._isApproved {
|
||||
try visitor.visitSingularBoolField(value: v, fieldNumber: 1)
|
||||
}
|
||||
try unknownFields.traverse(visitor: &visitor)
|
||||
}
|
||||
|
||||
static func ==(lhs: SessionProtos_MessageRequestResponse, rhs: SessionProtos_MessageRequestResponse) -> Bool {
|
||||
if lhs._isApproved != rhs._isApproved {return false}
|
||||
if lhs.unknownFields != rhs.unknownFields {return false}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
extension SessionProtos_Content: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding {
|
||||
static let protoMessageName: String = _protobuf_package + ".Content"
|
||||
static let _protobuf_nameMap: SwiftProtobuf._NameMap = [
|
||||
|
@ -1690,6 +1791,7 @@ extension SessionProtos_Content: SwiftProtobuf.Message, SwiftProtobuf._MessageIm
|
|||
7: .same(proto: "configurationMessage"),
|
||||
8: .same(proto: "dataExtractionNotification"),
|
||||
9: .same(proto: "unsendRequest"),
|
||||
10: .same(proto: "messageRequestResponse"),
|
||||
]
|
||||
|
||||
fileprivate class _StorageClass {
|
||||
|
@ -1700,6 +1802,7 @@ extension SessionProtos_Content: SwiftProtobuf.Message, SwiftProtobuf._MessageIm
|
|||
var _configurationMessage: SessionProtos_ConfigurationMessage? = nil
|
||||
var _dataExtractionNotification: SessionProtos_DataExtractionNotification? = nil
|
||||
var _unsendRequest: SessionProtos_UnsendRequest? = nil
|
||||
var _messageRequestResponse: SessionProtos_MessageRequestResponse? = nil
|
||||
|
||||
static let defaultInstance = _StorageClass()
|
||||
|
||||
|
@ -1713,6 +1816,7 @@ extension SessionProtos_Content: SwiftProtobuf.Message, SwiftProtobuf._MessageIm
|
|||
_configurationMessage = source._configurationMessage
|
||||
_dataExtractionNotification = source._dataExtractionNotification
|
||||
_unsendRequest = source._unsendRequest
|
||||
_messageRequestResponse = source._messageRequestResponse
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1732,6 +1836,7 @@ extension SessionProtos_Content: SwiftProtobuf.Message, SwiftProtobuf._MessageIm
|
|||
if let v = _storage._configurationMessage, !v.isInitialized {return false}
|
||||
if let v = _storage._dataExtractionNotification, !v.isInitialized {return false}
|
||||
if let v = _storage._unsendRequest, !v.isInitialized {return false}
|
||||
if let v = _storage._messageRequestResponse, !v.isInitialized {return false}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
@ -1751,6 +1856,7 @@ extension SessionProtos_Content: SwiftProtobuf.Message, SwiftProtobuf._MessageIm
|
|||
case 7: try { try decoder.decodeSingularMessageField(value: &_storage._configurationMessage) }()
|
||||
case 8: try { try decoder.decodeSingularMessageField(value: &_storage._dataExtractionNotification) }()
|
||||
case 9: try { try decoder.decodeSingularMessageField(value: &_storage._unsendRequest) }()
|
||||
case 10: try { try decoder.decodeSingularMessageField(value: &_storage._messageRequestResponse) }()
|
||||
default: break
|
||||
}
|
||||
}
|
||||
|
@ -1780,6 +1886,9 @@ extension SessionProtos_Content: SwiftProtobuf.Message, SwiftProtobuf._MessageIm
|
|||
if let v = _storage._unsendRequest {
|
||||
try visitor.visitSingularMessageField(value: v, fieldNumber: 9)
|
||||
}
|
||||
if let v = _storage._messageRequestResponse {
|
||||
try visitor.visitSingularMessageField(value: v, fieldNumber: 10)
|
||||
}
|
||||
}
|
||||
try unknownFields.traverse(visitor: &visitor)
|
||||
}
|
||||
|
@ -1796,6 +1905,7 @@ extension SessionProtos_Content: SwiftProtobuf.Message, SwiftProtobuf._MessageIm
|
|||
if _storage._configurationMessage != rhs_storage._configurationMessage {return false}
|
||||
if _storage._dataExtractionNotification != rhs_storage._dataExtractionNotification {return false}
|
||||
if _storage._unsendRequest != rhs_storage._unsendRequest {return false}
|
||||
if _storage._messageRequestResponse != rhs_storage._messageRequestResponse {return false}
|
||||
return true
|
||||
}
|
||||
if !storagesAreEqual {return false}
|
||||
|
@ -2679,6 +2789,9 @@ extension SessionProtos_ConfigurationMessage.Contact: SwiftProtobuf.Message, Swi
|
|||
2: .same(proto: "name"),
|
||||
3: .same(proto: "profilePicture"),
|
||||
4: .same(proto: "profileKey"),
|
||||
5: .same(proto: "isApproved"),
|
||||
6: .same(proto: "isBlocked"),
|
||||
7: .same(proto: "didApproveMe"),
|
||||
]
|
||||
|
||||
public var isInitialized: Bool {
|
||||
|
@ -2697,6 +2810,9 @@ extension SessionProtos_ConfigurationMessage.Contact: SwiftProtobuf.Message, Swi
|
|||
case 2: try { try decoder.decodeSingularStringField(value: &self._name) }()
|
||||
case 3: try { try decoder.decodeSingularStringField(value: &self._profilePicture) }()
|
||||
case 4: try { try decoder.decodeSingularBytesField(value: &self._profileKey) }()
|
||||
case 5: try { try decoder.decodeSingularBoolField(value: &self._isApproved) }()
|
||||
case 6: try { try decoder.decodeSingularBoolField(value: &self._isBlocked) }()
|
||||
case 7: try { try decoder.decodeSingularBoolField(value: &self._didApproveMe) }()
|
||||
default: break
|
||||
}
|
||||
}
|
||||
|
@ -2715,6 +2831,15 @@ extension SessionProtos_ConfigurationMessage.Contact: SwiftProtobuf.Message, Swi
|
|||
if let v = self._profileKey {
|
||||
try visitor.visitSingularBytesField(value: v, fieldNumber: 4)
|
||||
}
|
||||
if let v = self._isApproved {
|
||||
try visitor.visitSingularBoolField(value: v, fieldNumber: 5)
|
||||
}
|
||||
if let v = self._isBlocked {
|
||||
try visitor.visitSingularBoolField(value: v, fieldNumber: 6)
|
||||
}
|
||||
if let v = self._didApproveMe {
|
||||
try visitor.visitSingularBoolField(value: v, fieldNumber: 7)
|
||||
}
|
||||
try unknownFields.traverse(visitor: &visitor)
|
||||
}
|
||||
|
||||
|
@ -2723,6 +2848,9 @@ extension SessionProtos_ConfigurationMessage.Contact: SwiftProtobuf.Message, Swi
|
|||
if lhs._name != rhs._name {return false}
|
||||
if lhs._profilePicture != rhs._profilePicture {return false}
|
||||
if lhs._profileKey != rhs._profileKey {return false}
|
||||
if lhs._isApproved != rhs._isApproved {return false}
|
||||
if lhs._isBlocked != rhs._isBlocked {return false}
|
||||
if lhs._didApproveMe != rhs._didApproveMe {return false}
|
||||
if lhs.unknownFields != rhs.unknownFields {return false}
|
||||
return true
|
||||
}
|
||||
|
|
|
@ -41,6 +41,11 @@ message UnsendRequest {
|
|||
required string author = 2;
|
||||
}
|
||||
|
||||
message MessageRequestResponse {
|
||||
// @required
|
||||
required bool isApproved = 1; // Whether the request was approved
|
||||
}
|
||||
|
||||
message Content {
|
||||
optional DataMessage dataMessage = 1;
|
||||
optional CallMessage callMessage = 3;
|
||||
|
@ -49,6 +54,7 @@ message Content {
|
|||
optional ConfigurationMessage configurationMessage = 7;
|
||||
optional DataExtractionNotification dataExtractionNotification = 8;
|
||||
optional UnsendRequest unsendRequest = 9;
|
||||
optional MessageRequestResponse messageRequestResponse = 10;
|
||||
}
|
||||
|
||||
message CallMessage {
|
||||
|
@ -202,6 +208,9 @@ message ConfigurationMessage {
|
|||
required string name = 2;
|
||||
optional string profilePicture = 3;
|
||||
optional bytes profileKey = 4;
|
||||
optional bool isApproved = 5; // added for msg requests
|
||||
optional bool isBlocked = 6; // added for msg requests
|
||||
optional bool didApproveMe = 7; // added for msg requests
|
||||
}
|
||||
|
||||
repeated ClosedGroup closedGroups = 1;
|
||||
|
|
|
@ -12,6 +12,7 @@
|
|||
#import "TSGroupThread.h"
|
||||
#import "YapDatabaseConnection+OWS.h"
|
||||
#import <SessionMessagingKit/SessionMessagingKit-Swift.h>
|
||||
#import <SessionUtilitiesKit/SessionUtilitiesKit-Swift.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
|
@ -250,7 +251,27 @@ NSString *const kOWSBlockingManager_SyncedBlockedGroupIdsKey = @"kOWSBlockingMan
|
|||
forKey:kOWSBlockingManager_BlockedGroupMapKey
|
||||
inCollection:kOWSBlockingManager_BlockListCollection];
|
||||
|
||||
|
||||
// Update the contact blocked state (so sync'ing won't be busted)
|
||||
NSMutableArray<SNContact *> *contactsToUpdate = [[NSMutableArray alloc] init];
|
||||
|
||||
[[[LKStorage shared] getAllContacts] enumerateObjectsUsingBlock:^(SNContact * _Nonnull obj, BOOL * _Nonnull stop) {
|
||||
// If the blocked flag doesn't match then add it to the array to be saved
|
||||
BOOL contactInBlockedList = [blockedPhoneNumbers containsObject:obj.sessionID];
|
||||
|
||||
if (obj.isBlocked != contactInBlockedList) {
|
||||
obj.isBlocked = contactInBlockedList;
|
||||
[contactsToUpdate addObject:obj];
|
||||
}
|
||||
}];
|
||||
|
||||
if ([contactsToUpdate count] > 0) {
|
||||
[LKStorage writeWithBlock:^(YapDatabaseReadWriteTransaction * _Nonnull transaction) {
|
||||
[contactsToUpdate enumerateObjectsUsingBlock:^(SNContact * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
|
||||
[[LKStorage shared] setContact:obj usingTransaction:transaction];
|
||||
}];
|
||||
}];
|
||||
}
|
||||
|
||||
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
||||
if (sendSyncMessage) {
|
||||
|
||||
|
|
|
@ -18,9 +18,11 @@ extension MessageReceiver {
|
|||
case let message as ConfigurationMessage: handleConfigurationMessage(message, using: transaction)
|
||||
case let message as UnsendRequest: handleUnsendRequest(message, using: transaction)
|
||||
case let message as CallMessage: handleCallMessage(message, using: transaction)
|
||||
case let message as MessageRequestResponse: handleMessageRequestResponse(message, using: transaction)
|
||||
case let message as VisibleMessage: try handleVisibleMessage(message, associatedWithProto: proto, openGroupID: openGroupID, isBackgroundPoll: isBackgroundPoll, using: transaction)
|
||||
default: fatalError()
|
||||
}
|
||||
|
||||
var isMainAppAndActive = false
|
||||
if let sharedUserDefaults = UserDefaults(suiteName: "group.com.loki-project.loki-messenger") {
|
||||
isMainAppAndActive = sharedUserDefaults.bool(forKey: "isMainAppActive")
|
||||
|
@ -190,15 +192,23 @@ extension MessageReceiver {
|
|||
SNLog("Configuration message received.")
|
||||
let storage = SNMessagingKitConfiguration.shared.storage
|
||||
let transaction = transaction as! YapDatabaseReadWriteTransaction
|
||||
let messageSentTimestamp: TimeInterval = TimeInterval((message.sentTimestamp ?? 0) / 1000) // `sentTimestamp` is in ms
|
||||
let lastConfigTimestamp: TimeInterval = (UserDefaults.standard[.lastConfigurationSync]?.timeIntervalSince1970 ?? Date(timeIntervalSince1970: 0).timeIntervalSince1970)
|
||||
|
||||
// Profile
|
||||
var userProfileKey: OWSAES256Key? = nil
|
||||
if let profileKey = message.profileKey { userProfileKey = OWSAES256Key(data: profileKey) }
|
||||
updateProfileIfNeeded(publicKey: userPublicKey, name: message.displayName, profilePictureURL: message.profilePictureURL,
|
||||
profileKey: userProfileKey, sentTimestamp: message.sentTimestamp!, transaction: transaction)
|
||||
// Initial configuration sync
|
||||
if !UserDefaults.standard[.hasSyncedInitialConfiguration] {
|
||||
UserDefaults.standard[.hasSyncedInitialConfiguration] = true
|
||||
NotificationCenter.default.post(name: .initialConfigurationMessageReceived, object: nil)
|
||||
|
||||
if !UserDefaults.standard[.hasSyncedInitialConfiguration] || messageSentTimestamp > lastConfigTimestamp {
|
||||
if !UserDefaults.standard[.hasSyncedInitialConfiguration] {
|
||||
UserDefaults.standard[.hasSyncedInitialConfiguration] = true
|
||||
NotificationCenter.default.post(name: .initialConfigurationMessageReceived, object: nil)
|
||||
}
|
||||
|
||||
UserDefaults.standard[.lastConfigurationSync] = Date(timeIntervalSince1970: messageSentTimestamp)
|
||||
|
||||
// Contacts
|
||||
for contactInfo in message.contacts {
|
||||
let sessionID = contactInfo.publicKey!
|
||||
|
@ -206,11 +216,54 @@ extension MessageReceiver {
|
|||
if let profileKey = contactInfo.profileKey { contact.profileEncryptionKey = OWSAES256Key(data: profileKey) }
|
||||
contact.profilePictureURL = contactInfo.profilePictureURL
|
||||
contact.name = contactInfo.displayName
|
||||
|
||||
// Note: We only update these values if the proto actually has values for them (this is to
|
||||
// prevent an edge case where an old client could override the values with default values
|
||||
// since they aren't included)
|
||||
if contactInfo.hasIsApproved { contact.isApproved = contactInfo.isApproved }
|
||||
if contactInfo.hasIsBlocked { contact.isBlocked = contactInfo.isBlocked }
|
||||
if contactInfo.hasDidApproveMe { contact.didApproveMe = contactInfo.didApproveMe }
|
||||
|
||||
Storage.shared.setContact(contact, using: transaction)
|
||||
let thread = TSContactThread.getOrCreateThread(withContactSessionID: sessionID, transaction: transaction)
|
||||
thread.shouldBeVisible = true
|
||||
thread.save(with: transaction)
|
||||
|
||||
// If the contact is blocked
|
||||
if contactInfo.hasIsBlocked && contactInfo.isBlocked {
|
||||
// If this message changed them to the blocked state and there is an existing thread
|
||||
// associated with them that is a message request thread then delete it (assume
|
||||
// that the current user had deleted that message request)
|
||||
if
|
||||
contactInfo.isBlocked != OWSBlockingManager.shared().isRecipientIdBlocked(sessionID),
|
||||
let thread: TSContactThread = TSContactThread.fetch(for: sessionID, using: transaction),
|
||||
thread.isMessageRequest(using: transaction)
|
||||
{
|
||||
thread.removeAllThreadInteractions(with: transaction)
|
||||
thread.remove(with: transaction)
|
||||
}
|
||||
}
|
||||
else {
|
||||
// Otherwise create and save the thread
|
||||
let thread = TSContactThread.getOrCreateThread(withContactSessionID: sessionID, transaction: transaction)
|
||||
thread.shouldBeVisible = true
|
||||
thread.save(with: transaction)
|
||||
}
|
||||
}
|
||||
|
||||
// FIXME: 'OWSBlockingManager' manages it's own dbConnection and transactions so we have to dispatch this to prevent deadlocks
|
||||
DispatchQueue.global().async {
|
||||
for contactInfo in message.contacts {
|
||||
let sessionID = contactInfo.publicKey!
|
||||
|
||||
if contactInfo.hasIsBlocked && contactInfo.isBlocked != OWSBlockingManager.shared().isRecipientIdBlocked(sessionID) {
|
||||
if contactInfo.isBlocked {
|
||||
OWSBlockingManager.shared().addBlockedPhoneNumber(sessionID)
|
||||
}
|
||||
else {
|
||||
OWSBlockingManager.shared().removeBlockedPhoneNumber(sessionID)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Closed groups
|
||||
let allClosedGroupPublicKeys = storage.getUserClosedGroupPublicKeys()
|
||||
for closedGroup in message.closedGroups {
|
||||
|
@ -379,8 +432,24 @@ extension MessageReceiver {
|
|||
if let tsOutgoingMessage = TSMessage.fetch(uniqueId: tsMessageID, transaction: transaction) as? TSOutgoingMessage,
|
||||
let thread = TSThread.fetch(uniqueId: threadID, transaction: transaction) {
|
||||
// Mark previous messages as read if there is a sync message
|
||||
OWSReadReceiptManager.shared().markAsReadLocally(beforeSortId: tsOutgoingMessage.sortId, thread: thread)
|
||||
OWSReadReceiptManager.shared().markAsReadLocally(beforeSortId: tsOutgoingMessage.sortId, thread: thread, trySendReadReceipt: true)
|
||||
}
|
||||
|
||||
// Update the contact's approval status of the current user if needed (if we are getting messages from
|
||||
// them outside of a group then we can assume they have approved the current user)
|
||||
//
|
||||
// Note: This is to resolve a rare edge-case where a conversation was started with a user on an old
|
||||
// version of the app and their message request approval state was set via a migration rather than
|
||||
// by using the approval process
|
||||
if !isGroup, let senderSessionId: String = message.sender {
|
||||
updateContactApprovalStatusIfNeeded(
|
||||
senderSessionId: senderSessionId,
|
||||
threadId: message.threadID,
|
||||
forceConfigSync: false,
|
||||
using: transaction
|
||||
)
|
||||
}
|
||||
|
||||
// Notify the user if needed
|
||||
guard (isMainAppAndActive || isBackgroundPoll), let tsIncomingMessage = TSMessage.fetch(uniqueId: tsMessageID, transaction: transaction) as? TSIncomingMessage,
|
||||
let thread = TSThread.fetch(uniqueId: threadID, transaction: transaction) else { return tsMessageID }
|
||||
|
@ -469,10 +538,26 @@ extension MessageReceiver {
|
|||
|
||||
private static func handleNewClosedGroup(groupPublicKey: String, name: String, encryptionKeyPair: ECKeyPair, members: [String], admins: [String], expirationTimer: UInt32, messageSentTimestamp: UInt64, using transaction: Any) {
|
||||
let transaction = transaction as! YapDatabaseReadWriteTransaction
|
||||
|
||||
// With new closed groups we only want to create them if the admin creating the closed group is an
|
||||
// approved contact (to prevent spam via closed groups getting around message requests if users are
|
||||
// on old or modified clients)
|
||||
var hasApprovedAdmin: Bool = false
|
||||
|
||||
for adminId in admins {
|
||||
if let contact: Contact = Storage.shared.getContact(with: adminId), contact.isApproved {
|
||||
hasApprovedAdmin = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
guard hasApprovedAdmin else { return }
|
||||
|
||||
// Create the group
|
||||
let groupID = LKGroupUtilities.getEncodedClosedGroupIDAsData(groupPublicKey)
|
||||
let group = TSGroupModel(title: name, memberIds: members, image: nil, groupId: groupID, groupType: .closedGroup, adminIds: admins)
|
||||
let thread: TSGroupThread
|
||||
|
||||
if let t = TSGroupThread.fetch(uniqueId: TSGroupThread.threadId(fromGroupId: groupID), transaction: transaction) {
|
||||
thread = t
|
||||
thread.setGroupModel(group, with: transaction)
|
||||
|
@ -481,18 +566,24 @@ extension MessageReceiver {
|
|||
if !storage.isClosedGroup(groupPublicKey) {
|
||||
storage.setZombieMembers(for: groupPublicKey, to: [], using: transaction)
|
||||
}
|
||||
} else {
|
||||
}
|
||||
else {
|
||||
thread = TSGroupThread.getOrCreateThread(with: group, transaction: transaction)
|
||||
thread.save(with: transaction)
|
||||
// Notify the user
|
||||
let infoMessage = TSInfoMessage(timestamp: messageSentTimestamp, in: thread, messageType: .groupCreated)
|
||||
infoMessage.save(with: transaction)
|
||||
}
|
||||
|
||||
let isExpirationTimerEnabled = (expirationTimer > 0)
|
||||
let expirationTimerDuration = (isExpirationTimerEnabled ? expirationTimer : 24 * 60 * 60)
|
||||
let configuration = OWSDisappearingMessagesConfiguration(threadId: thread.uniqueId!, enabled: isExpirationTimerEnabled,
|
||||
durationSeconds: expirationTimerDuration)
|
||||
let configuration = OWSDisappearingMessagesConfiguration(
|
||||
threadId: thread.uniqueId!,
|
||||
enabled: isExpirationTimerEnabled,
|
||||
durationSeconds: expirationTimerDuration
|
||||
)
|
||||
configuration.save(with: transaction)
|
||||
|
||||
// Add the group to the user's set of public keys to poll for
|
||||
Storage.shared.addClosedGroupPublicKey(groupPublicKey, using: transaction)
|
||||
// Store the key pair
|
||||
|
@ -736,4 +827,80 @@ extension MessageReceiver {
|
|||
// Perform the update
|
||||
update(groupID, thread, group)
|
||||
}
|
||||
|
||||
// MARK: - Message Requests
|
||||
|
||||
private static func updateContactApprovalStatusIfNeeded(
|
||||
senderSessionId: String,
|
||||
threadId: String?,
|
||||
forceConfigSync: Bool,
|
||||
using transaction: Any
|
||||
) {
|
||||
guard let transaction: YapDatabaseReadWriteTransaction = transaction as? YapDatabaseReadWriteTransaction else { return }
|
||||
|
||||
let userPublicKey: String = getUserHexEncodedPublicKey()
|
||||
|
||||
// If the sender of the message was the current user
|
||||
if senderSessionId == userPublicKey {
|
||||
// Retrieve the contact for the thread the message was sent to (excluding 'NoteToSelf' threads) and if
|
||||
// the contact isn't flagged as approved then do so
|
||||
guard let threadId: String = threadId else { return }
|
||||
guard let thread: TSContactThread = TSContactThread.fetch(uniqueId: threadId, transaction: transaction), !thread.isNoteToSelf() else { return }
|
||||
guard let contact: Contact = Storage.shared.getContact(with: thread.contactSessionID(), using: transaction) else { return }
|
||||
guard !contact.isApproved else { return }
|
||||
|
||||
contact.isApproved = true
|
||||
Storage.shared.setContact(contact, using: transaction)
|
||||
}
|
||||
else {
|
||||
// The message was sent to the current user so flag their 'didApproveMe' as true (can't send a message to
|
||||
// someone without approving them)
|
||||
guard let contact: Contact = Storage.shared.getContact(with: senderSessionId, using: transaction) else { return }
|
||||
guard !contact.didApproveMe else { return }
|
||||
|
||||
contact.didApproveMe = true
|
||||
Storage.shared.setContact(contact, using: transaction)
|
||||
}
|
||||
|
||||
// Force a config sync to ensure all devices know the contact approval state if desired (Note: This logic
|
||||
// should match the behaviour in AppDelegate.forceSyncConfigurationNowIfNeeded())
|
||||
guard forceConfigSync else { return }
|
||||
|
||||
// Note: We MUST run this async as we need to ensure the database `transaction` has finished before we generate
|
||||
// a new configuration message (otherwise the `contact` will be loaded direct from the database and the
|
||||
// `didApproveMe` value won't have been updated)
|
||||
DispatchQueue.global(qos: .background).async {
|
||||
guard Storage.shared.getUser()?.name != nil, let configurationMessage = ConfigurationMessage.getCurrent() else {
|
||||
return
|
||||
}
|
||||
|
||||
let destination: Message.Destination = Message.Destination.contact(publicKey: userPublicKey)
|
||||
MessageSender.send(configurationMessage, to: destination, using: transaction).retainUntilComplete()
|
||||
}
|
||||
}
|
||||
|
||||
public static func handleMessageRequestResponse(_ message: MessageRequestResponse, using transaction: Any) {
|
||||
let userPublicKey = getUserHexEncodedPublicKey()
|
||||
|
||||
// Ignore messages which were sent from the current user
|
||||
guard message.sender != userPublicKey else { return }
|
||||
guard let senderId: String = message.sender else { return }
|
||||
|
||||
// Get the existing thead and notify the user
|
||||
if let transaction: YapDatabaseReadWriteTransaction = transaction as? YapDatabaseReadWriteTransaction, let thread: TSContactThread = TSContactThread.fetch(for: senderId, using: transaction) {
|
||||
let infoMessage = TSInfoMessage(
|
||||
timestamp: (message.sentTimestamp ?? NSDate.ows_millisecondTimeStamp()),
|
||||
in: thread,
|
||||
messageType: .messageRequestAccepted
|
||||
)
|
||||
infoMessage.save(with: transaction)
|
||||
}
|
||||
|
||||
updateContactApprovalStatusIfNeeded(
|
||||
senderSessionId: senderId,
|
||||
threadId: nil,
|
||||
forceConfigSync: true,
|
||||
using: transaction
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -129,6 +129,7 @@ public enum MessageReceiver {
|
|||
if let expirationTimerUpdate = ExpirationTimerUpdate.fromProto(proto) { return expirationTimerUpdate }
|
||||
if let configurationMessage = ConfigurationMessage.fromProto(proto) { return configurationMessage }
|
||||
if let unsendRequest = UnsendRequest.fromProto(proto) { return unsendRequest }
|
||||
if let messageRequestResponse = MessageRequestResponse.fromProto(proto) { return messageRequestResponse }
|
||||
if let visibleMessage = VisibleMessage.fromProto(proto) { return visibleMessage }
|
||||
if let callMessage = CallMessage.fromProto(proto) { return callMessage }
|
||||
return nil
|
||||
|
|
|
@ -45,7 +45,7 @@ extern NSString *const kIncomingMessageMarkedAsReadNotification;
|
|||
// This method can be called from any thread.
|
||||
- (void)messageWasReadLocally:(TSIncomingMessage *)message;
|
||||
|
||||
- (void)markAsReadLocallyBeforeSortId:(uint64_t)sortId thread:(TSThread *)thread;
|
||||
- (void)markAsReadLocallyBeforeSortId:(uint64_t)sortId thread:(TSThread *)thread trySendReadReceipt:(BOOL)trySendReadReceipt;
|
||||
|
||||
#pragma mark - Settings
|
||||
|
||||
|
|
|
@ -180,13 +180,13 @@ NSString *const OWSReadReceiptManagerAreReadReceiptsEnabled = @"areReadReceiptsE
|
|||
|
||||
#pragma mark - Mark as Read Locally
|
||||
|
||||
- (void)markAsReadLocallyBeforeSortId:(uint64_t)sortId thread:(TSThread *)thread
|
||||
- (void)markAsReadLocallyBeforeSortId:(uint64_t)sortId thread:(TSThread *)thread trySendReadReceipt:(BOOL)trySendReadReceipt
|
||||
{
|
||||
[LKStorage writeWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
|
||||
[self markAsReadBeforeSortId:sortId
|
||||
thread:thread
|
||||
readTimestamp:[NSDate millisecondTimestamp]
|
||||
wasLocal:YES
|
||||
trySendReadReceipt:trySendReadReceipt
|
||||
transaction:transaction];
|
||||
}];
|
||||
}
|
||||
|
@ -254,7 +254,7 @@ NSString *const OWSReadReceiptManagerAreReadReceiptsEnabled = @"areReadReceiptsE
|
|||
- (void)markAsReadBeforeSortId:(uint64_t)sortId
|
||||
thread:(TSThread *)thread
|
||||
readTimestamp:(uint64_t)readTimestamp
|
||||
wasLocal:(BOOL)wasLocal
|
||||
trySendReadReceipt:(BOOL)trySendReadReceipt
|
||||
transaction:(YapDatabaseReadWriteTransaction *)transaction
|
||||
{
|
||||
NSMutableArray<id<OWSReadTracking>> *newlyReadList = [NSMutableArray new];
|
||||
|
@ -285,7 +285,7 @@ NSString *const OWSReadReceiptManagerAreReadReceiptsEnabled = @"areReadReceiptsE
|
|||
}
|
||||
|
||||
for (id<OWSReadTracking> readItem in newlyReadList) {
|
||||
[readItem markAsReadAtTimestamp:readTimestamp sendReadReceipt:wasLocal transaction:transaction];
|
||||
[readItem markAsReadAtTimestamp:readTimestamp trySendReadReceipt:trySendReadReceipt transaction:transaction];
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -29,7 +29,7 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
* Used both for *responding* to a remote read receipt and in response to the local user's activity.
|
||||
*/
|
||||
- (void)markAsReadAtTimestamp:(uint64_t)readTimestamp
|
||||
sendReadReceipt:(BOOL)sendReadReceipt
|
||||
trySendReadReceipt:(BOOL)trySendReadReceipt
|
||||
transaction:(YapDatabaseReadWriteTransaction *)transaction;
|
||||
|
||||
@end
|
||||
|
|
|
@ -86,7 +86,7 @@ public class TypingIndicatorsImpl : NSObject, TypingIndicators {
|
|||
|
||||
@objc
|
||||
public func didStartTypingOutgoingInput(inThread thread: TSThread) {
|
||||
guard let outgoingIndicators = ensureOutgoingIndicators(forThread: thread) else {
|
||||
guard let outgoingIndicators = ensureOutgoingIndicators(forThread: thread), !thread.isMessageRequest() else {
|
||||
return
|
||||
}
|
||||
outgoingIndicators.didStartTypingOutgoingInput()
|
||||
|
@ -94,7 +94,7 @@ public class TypingIndicatorsImpl : NSObject, TypingIndicators {
|
|||
|
||||
@objc
|
||||
public func didStopTypingOutgoingInput(inThread thread: TSThread) {
|
||||
guard let outgoingIndicators = ensureOutgoingIndicators(forThread: thread) else {
|
||||
guard let outgoingIndicators = ensureOutgoingIndicators(forThread: thread), !thread.isMessageRequest() else {
|
||||
return
|
||||
}
|
||||
outgoingIndicators.didStopTypingOutgoingInput()
|
||||
|
|
|
@ -18,6 +18,7 @@ public protocol SessionMessagingKitStorageProtocol {
|
|||
func getUserED25519KeyPair() -> Box.KeyPair?
|
||||
func getUser() -> Contact?
|
||||
func getAllContacts() -> Set<Contact>
|
||||
func getAllContacts(with transaction: YapDatabaseReadTransaction) -> Set<Contact>
|
||||
|
||||
// MARK: - Closed Groups
|
||||
|
||||
|
|
|
@ -57,6 +57,32 @@ NSString *const TSContactThreadPrefix = @"c";
|
|||
return @[ self.contactSessionID ];
|
||||
}
|
||||
|
||||
- (BOOL)isMessageRequest {
|
||||
NSString *sessionID = self.contactSessionID;
|
||||
SNContact *contact = [LKStorage.shared getContactWithSessionID:sessionID];
|
||||
|
||||
return (
|
||||
self.shouldBeVisible &&
|
||||
!self.isNoteToSelf && (
|
||||
contact == nil ||
|
||||
!contact.isApproved
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
- (BOOL)isMessageRequestUsingTransaction:(YapDatabaseReadTransaction *)transaction {
|
||||
NSString *sessionID = self.contactSessionID;
|
||||
SNContact *contact = [LKStorage.shared getContactWithSessionID:sessionID using:transaction];
|
||||
|
||||
return (
|
||||
self.shouldBeVisible &&
|
||||
!self.isNoteToSelf && (
|
||||
contact == nil ||
|
||||
!contact.isApproved
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
- (BOOL)isGroupThread
|
||||
{
|
||||
return NO;
|
||||
|
|
|
@ -54,6 +54,14 @@ BOOL IsNoteToSelfEnabled(void);
|
|||
|
||||
- (BOOL)isNoteToSelf;
|
||||
|
||||
/**
|
||||
* Whether the thread is a message request.
|
||||
*
|
||||
* @return YES if the combination of thread and contact approval means this thread should appear in the message requests section, NO otherwise.
|
||||
*/
|
||||
- (BOOL)isMessageRequest;
|
||||
- (BOOL)isMessageRequestUsingTransaction:(YapDatabaseReadTransaction *)transaction;
|
||||
|
||||
#pragma mark Interactions
|
||||
|
||||
- (void)enumerateInteractionsWithTransaction:(YapDatabaseReadTransaction *)transaction usingBlock:(void (^)(TSInteraction *interaction, BOOL *stop))block;
|
||||
|
|
|
@ -132,6 +132,16 @@ BOOL IsNoteToSelfEnabled(void)
|
|||
return [self.contactSessionID isEqual:[SNGeneralUtilities getUserPublicKey]];
|
||||
}
|
||||
|
||||
// Override in ContactThread
|
||||
- (BOOL)isMessageRequest {
|
||||
return NO;
|
||||
}
|
||||
|
||||
// Override in ContactThread
|
||||
- (BOOL)isMessageRequestUsingTransaction:(YapDatabaseReadTransaction *)transaction {
|
||||
return NO;
|
||||
}
|
||||
|
||||
#pragma mark To be subclassed.
|
||||
|
||||
- (BOOL)isGroupThread {
|
||||
|
@ -311,7 +321,7 @@ BOOL IsNoteToSelfEnabled(void)
|
|||
- (void)markAllAsReadWithTransaction:(YapDatabaseReadWriteTransaction *)transaction
|
||||
{
|
||||
for (id<OWSReadTracking> message in [self unseenMessagesWithTransaction:transaction]) {
|
||||
[message markAsReadAtTimestamp:[NSDate ows_millisecondTimeStamp] sendReadReceipt:YES transaction:transaction];
|
||||
[message markAsReadAtTimestamp:[NSDate ows_millisecondTimeStamp] trySendReadReceipt:YES transaction:transaction];
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -41,72 +41,110 @@ public final class NotificationServiceExtension : UNNotificationServiceExtension
|
|||
let senderPublicKey = message.sender!
|
||||
var senderDisplayName = Storage.shared.getContact(with: senderPublicKey)?.displayName(for: .regular) ?? senderPublicKey
|
||||
let snippet: String
|
||||
var userInfo: [String:Any] = [ NotificationServiceExtension.isFromRemoteKey : true ]
|
||||
var userInfo: [String: Any] = [ NotificationServiceExtension.isFromRemoteKey: true ]
|
||||
var isMessageRequest: Bool = false
|
||||
|
||||
switch message {
|
||||
case let visibleMessage as VisibleMessage:
|
||||
let tsIncomingMessageID = try MessageReceiver.handleVisibleMessage(visibleMessage, associatedWithProto: proto, openGroupID: nil, isBackgroundPoll: false, using: transaction)
|
||||
guard let tsMessage = TSMessage.fetch(uniqueId: tsIncomingMessageID, transaction: transaction) else {
|
||||
return self.completeSilenty()
|
||||
}
|
||||
let thread = tsMessage.thread(with: transaction)
|
||||
let threadID = thread.uniqueId!
|
||||
userInfo[NotificationServiceExtension.threadIdKey] = threadID
|
||||
snippet = tsMessage.previewText(with: transaction).filterForDisplay?.replacingMentions(for: threadID, using: transaction)
|
||||
?? "You've got a new message"
|
||||
if let tsIncomingMessage = tsMessage as? TSIncomingMessage {
|
||||
if thread.isMuted {
|
||||
// Ignore PNs if the thread is muted
|
||||
case let visibleMessage as VisibleMessage:
|
||||
let tsIncomingMessageID = try MessageReceiver.handleVisibleMessage(visibleMessage, associatedWithProto: proto, openGroupID: nil, isBackgroundPoll: false, using: transaction)
|
||||
|
||||
guard let tsMessage = TSMessage.fetch(uniqueId: tsIncomingMessageID, transaction: transaction) else {
|
||||
return self.completeSilenty()
|
||||
}
|
||||
if let thread = TSThread.fetch(uniqueId: threadID, transaction: transaction), let group = thread as? TSGroupThread,
|
||||
group.groupModel.groupType == .closedGroup { // Should always be true because we don't get PNs for open groups
|
||||
senderDisplayName = String(format: NotificationStrings.incomingGroupMessageTitleFormat, senderDisplayName, group.groupModel.groupName ?? MessageStrings.newGroupDefaultTitle)
|
||||
if group.isOnlyNotifyingForMentions && !tsIncomingMessage.isUserMentioned {
|
||||
// Ignore PNs if the group is set to only notify for mentions
|
||||
return self.completeSilenty()
|
||||
|
||||
let thread = tsMessage.thread(with: transaction)
|
||||
let threadID = thread.uniqueId!
|
||||
userInfo[NotificationServiceExtension.threadIdKey] = threadID
|
||||
snippet = tsMessage.previewText(with: transaction).filterForDisplay?.replacingMentions(for: threadID, using: transaction)
|
||||
?? "You've got a new message"
|
||||
|
||||
if let tsIncomingMessage = tsMessage as? TSIncomingMessage {
|
||||
// Ignore PNs if the thread is muted
|
||||
if thread.isMuted { return self.completeSilenty() }
|
||||
if let thread = TSThread.fetch(uniqueId: threadID, transaction: transaction), let group = thread as? TSGroupThread,
|
||||
group.groupModel.groupType == .closedGroup { // Should always be true because we don't get PNs for open groups
|
||||
senderDisplayName = String(format: NotificationStrings.incomingGroupMessageTitleFormat, senderDisplayName, group.groupModel.groupName ?? MessageStrings.newGroupDefaultTitle)
|
||||
if group.isOnlyNotifyingForMentions && !tsIncomingMessage.isUserMentioned {
|
||||
// Ignore PNs if the group is set to only notify for mentions
|
||||
return self.completeSilenty()
|
||||
}
|
||||
}
|
||||
|
||||
// If the thread is a message request and the user hasn't hidden message requests then we need
|
||||
// to check if this is the only message request thread (group threads can't be message requests
|
||||
// so just ignore those and if the user has hidden message requests then we want to show the
|
||||
// notification regardless of how many message requests there are)
|
||||
if !thread.isGroupThread() && thread.isMessageRequest() && !CurrentAppContext().appUserDefaults()[.hasHiddenMessageRequests] {
|
||||
let dbConnection: YapDatabaseConnection = OWSPrimaryStorage.shared().newDatabaseConnection()
|
||||
dbConnection.objectCacheLimit = 2
|
||||
dbConnection.beginLongLivedReadTransaction() // Freeze the connection for use on the main thread (this gives us a stable data source that doesn't change until we tell it to)
|
||||
let threads: YapDatabaseViewMappings = YapDatabaseViewMappings(groups: [ TSMessageRequestGroup ], view: TSThreadDatabaseViewExtensionName)
|
||||
dbConnection.read { transaction in
|
||||
threads.update(with: transaction) // Perform the initial update
|
||||
}
|
||||
|
||||
let numMessageRequests = threads.numberOfItems(inGroup: TSMessageRequestGroup)
|
||||
dbConnection.endLongLivedReadTransaction()
|
||||
|
||||
// Allow this to show a notification if there are no message requests (ie. this is the first one)
|
||||
guard numMessageRequests == 0 else { return self.completeSilenty() }
|
||||
}
|
||||
else if thread.isMessageRequest() && CurrentAppContext().appUserDefaults()[.hasHiddenMessageRequests] {
|
||||
// If there are other interactions on this thread already then don't show the notification
|
||||
if thread.numberOfInteractions() > 1 { return }
|
||||
|
||||
CurrentAppContext().appUserDefaults()[.hasHiddenMessageRequests] = false
|
||||
}
|
||||
|
||||
isMessageRequest = thread.isMessageRequest()
|
||||
|
||||
// Store the notification ID for unsend requests to later cancel this notification
|
||||
tsIncomingMessage.setNotificationIdentifier(request.identifier, transaction: transaction)
|
||||
}
|
||||
// Store the notification ID for unsend requests to later cancel this notification
|
||||
tsIncomingMessage.setNotificationIdentifier(request.identifier, transaction: transaction)
|
||||
} else {
|
||||
let semaphore = DispatchSemaphore(value: 0)
|
||||
let center = UNUserNotificationCenter.current()
|
||||
center.getDeliveredNotifications { notifications in
|
||||
let matchingNotifications = notifications.filter({ $0.request.content.userInfo[NotificationServiceExtension.threadIdKey] as? String == threadID})
|
||||
center.removeDeliveredNotifications(withIdentifiers: matchingNotifications.map({ $0.request.identifier }))
|
||||
// Hack: removeDeliveredNotifications seems to be async,need to wait for some time before the delivered notifications can be removed.
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { semaphore.signal() }
|
||||
}
|
||||
semaphore.wait()
|
||||
}
|
||||
notificationContent.sound = OWSSounds.notificationSound(for: thread).notificationSound(isQuiet: false)
|
||||
case let unsendRequest as UnsendRequest:
|
||||
MessageReceiver.handleUnsendRequest(unsendRequest, using: transaction)
|
||||
return self.completeSilenty()
|
||||
case let closedGroupControlMessage as ClosedGroupControlMessage:
|
||||
// TODO: We could consider actually handling the update here. Not sure if there's enough time though, seeing as though
|
||||
// in some cases we need to send messages (e.g. our sender key) to a number of other users.
|
||||
switch closedGroupControlMessage.kind {
|
||||
case .new(_, let name, _, _, _, _): snippet = "\(senderDisplayName) added you to \(name)"
|
||||
default: return self.completeSilenty()
|
||||
}
|
||||
case let callMessage as CallMessage:
|
||||
MessageReceiver.handleCallMessage(callMessage, using: transaction)
|
||||
guard case .preOffer = callMessage.kind else { return self.completeSilenty() }
|
||||
if !SSKPreferences.areCallsEnabled {
|
||||
if let sender = callMessage.sender, let thread = TSContactThread.fetch(for: sender, using: transaction), thread.hasOutgoingInteraction(with: transaction) {
|
||||
let infoMessage = TSInfoMessage.from(callMessage, associatedWith: thread)
|
||||
infoMessage.updateCallInfoMessage(.missed, using: transaction)
|
||||
else {
|
||||
let semaphore = DispatchSemaphore(value: 0)
|
||||
let center = UNUserNotificationCenter.current()
|
||||
center.getDeliveredNotifications { notifications in
|
||||
let matchingNotifications = notifications.filter({ $0.request.content.userInfo[NotificationServiceExtension.threadIdKey] as? String == threadID})
|
||||
center.removeDeliveredNotifications(withIdentifiers: matchingNotifications.map({ $0.request.identifier }))
|
||||
// Hack: removeDeliveredNotifications seems to be async,need to wait for some time before the delivered notifications can be removed.
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { semaphore.signal() }
|
||||
}
|
||||
semaphore.wait()
|
||||
}
|
||||
notificationContent.sound = OWSSounds.notificationSound(for: thread).notificationSound(isQuiet: false)
|
||||
|
||||
case let unsendRequest as UnsendRequest:
|
||||
MessageReceiver.handleUnsendRequest(unsendRequest, using: transaction)
|
||||
return self.completeSilenty()
|
||||
}
|
||||
notificationContent.userInfo = userInfo
|
||||
notificationContent.badge = 1
|
||||
notificationContent.title = "Session"
|
||||
notificationContent.body = "\(senderDisplayName) is calling..."
|
||||
return self.handleSuccessForIncomingCall(for: notificationContent, callMessage: callMessage)
|
||||
default: return self.completeSilenty()
|
||||
|
||||
case let closedGroupControlMessage as ClosedGroupControlMessage:
|
||||
// TODO: We could consider actually handling the update here. Not sure if there's enough time though, seeing as though
|
||||
// in some cases we need to send messages (e.g. our sender key) to a number of other users.
|
||||
switch closedGroupControlMessage.kind {
|
||||
case .new(_, let name, _, _, _, _): snippet = "\(senderDisplayName) added you to \(name)"
|
||||
default: return self.completeSilenty()
|
||||
}
|
||||
|
||||
case let callMessage as CallMessage:
|
||||
MessageReceiver.handleCallMessage(callMessage, using: transaction)
|
||||
guard case .preOffer = callMessage.kind else { return self.completeSilenty() }
|
||||
if !SSKPreferences.areCallsEnabled {
|
||||
if let sender = callMessage.sender, let thread = TSContactThread.fetch(for: sender, using: transaction), thread.hasOutgoingInteraction(with: transaction) {
|
||||
let infoMessage = TSInfoMessage.from(callMessage, associatedWith: thread)
|
||||
infoMessage.updateCallInfoMessage(.missed, using: transaction)
|
||||
}
|
||||
return self.completeSilenty()
|
||||
}
|
||||
notificationContent.userInfo = userInfo
|
||||
notificationContent.badge = 1
|
||||
notificationContent.title = "Session"
|
||||
notificationContent.body = "\(senderDisplayName) is calling..."
|
||||
return self.handleSuccessForIncomingCall(for: notificationContent, callMessage: callMessage)
|
||||
|
||||
default: return self.completeSilenty()
|
||||
}
|
||||
|
||||
if (senderPublicKey == userPublicKey) {
|
||||
// Ignore PNs for messages sent by the current user
|
||||
// after handling the message. Otherwise the closed
|
||||
|
@ -115,21 +153,35 @@ public final class NotificationServiceExtension : UNNotificationServiceExtension
|
|||
}
|
||||
notificationContent.userInfo = userInfo
|
||||
notificationContent.badge = 1
|
||||
|
||||
let notificationsPreference = Environment.shared.preferences!.notificationPreviewType()
|
||||
|
||||
switch notificationsPreference {
|
||||
case .namePreview:
|
||||
notificationContent.title = senderDisplayName
|
||||
notificationContent.body = snippet
|
||||
case .nameNoPreview:
|
||||
notificationContent.title = senderDisplayName
|
||||
notificationContent.body = NotificationStrings.incomingMessageBody
|
||||
case .noNameNoPreview:
|
||||
notificationContent.title = "Session"
|
||||
notificationContent.body = NotificationStrings.incomingMessageBody
|
||||
default: break
|
||||
case .namePreview:
|
||||
notificationContent.title = senderDisplayName
|
||||
notificationContent.body = snippet
|
||||
|
||||
case .nameNoPreview:
|
||||
notificationContent.title = senderDisplayName
|
||||
notificationContent.body = NotificationStrings.incomingMessageBody
|
||||
|
||||
case .noNameNoPreview:
|
||||
notificationContent.title = "Session"
|
||||
notificationContent.body = NotificationStrings.incomingMessageBody
|
||||
|
||||
default: break
|
||||
}
|
||||
|
||||
// If it's a message request then overwrite the body to be something generic (only show a notification
|
||||
// when receiving a new message request if there aren't any others or the user had hidden them)
|
||||
if isMessageRequest {
|
||||
notificationContent.title = "Session"
|
||||
notificationContent.body = NSLocalizedString("MESSAGE_REQUESTS_NOTIFICATION", comment: "")
|
||||
}
|
||||
|
||||
self.handleSuccess(for: notificationContent)
|
||||
} catch {
|
||||
}
|
||||
catch {
|
||||
if let error = error as? MessageReceiver.Error, error.isRetryable {
|
||||
self.handleFailure(for: notificationContent)
|
||||
}
|
||||
|
|
|
@ -225,7 +225,7 @@ final class ShareVC : UINavigationController, ShareViewDelegate, AppModeManagerD
|
|||
alert.addAction(UIAlertAction(title: "BUTTON_OK".localized(), style: .default, handler: { _ in
|
||||
self.extensionContext!.cancelRequest(withError: error)
|
||||
}))
|
||||
present(alert, animated: true, completion: nil)
|
||||
presentAlert(alert)
|
||||
}
|
||||
|
||||
// MARK: Attachment Prep
|
||||
|
|
|
@ -11,7 +11,7 @@ final class ThreadPickerVC: UIViewController, UITableViewDataSource, UITableView
|
|||
var shareVC: ShareVC?
|
||||
|
||||
private var threadCount: UInt {
|
||||
threads.numberOfItems(inGroup: TSInboxGroup)
|
||||
threads.numberOfItems(inGroup: TSShareExtensionGroup)
|
||||
}
|
||||
|
||||
private lazy var dbConnection: YapDatabaseConnection = {
|
||||
|
@ -65,8 +65,8 @@ final class ThreadPickerVC: UIViewController, UITableViewDataSource, UITableView
|
|||
|
||||
// Threads
|
||||
dbConnection.beginLongLivedReadTransaction() // Freeze the connection for use on the main thread (this gives us a stable data source that doesn't change until we tell it to)
|
||||
threads = YapDatabaseViewMappings(groups: [ TSInboxGroup ], view: TSThreadDatabaseViewExtensionName) // The extension should be registered at this point
|
||||
threads.setIsReversed(true, forGroup: TSInboxGroup)
|
||||
threads = YapDatabaseViewMappings(groups: [ TSShareExtensionGroup ], view: TSThreadShareExtensionDatabaseViewExtensionName) // The extension should be registered at this point
|
||||
threads.setIsReversed(true, forGroup: TSShareExtensionGroup)
|
||||
dbConnection.read { transaction in
|
||||
self.threads.update(with: transaction) // Perform the initial update
|
||||
}
|
||||
|
@ -222,7 +222,7 @@ final class ThreadPickerVC: UIViewController, UITableViewDataSource, UITableView
|
|||
private func thread(at index: Int) -> TSThread? {
|
||||
var thread: TSThread? = nil
|
||||
dbConnection.read { transaction in
|
||||
let ext = transaction.ext(TSThreadDatabaseViewExtensionName) as! YapDatabaseViewTransaction
|
||||
let ext = transaction.ext(TSThreadShareExtensionDatabaseViewExtensionName) as! YapDatabaseViewTransaction
|
||||
thread = ext.object(atRow: UInt(index), inSection: 0, with: self.threads) as! TSThread?
|
||||
}
|
||||
return thread
|
||||
|
|
|
@ -6,7 +6,7 @@ public final class Button : UIButton {
|
|||
private var heightConstraint: NSLayoutConstraint!
|
||||
|
||||
public enum Style {
|
||||
case unimportant, regular, prominentOutline, prominentFilled, regularBorderless
|
||||
case unimportant, regular, prominentOutline, prominentFilled, regularBorderless, destructiveOutline
|
||||
}
|
||||
|
||||
public enum Size {
|
||||
|
@ -41,6 +41,7 @@ public final class Button : UIButton {
|
|||
case .prominentOutline: fillColor = UIColor.clear
|
||||
case .prominentFilled: fillColor = isLightMode ? Colors.text : Colors.accent
|
||||
case .regularBorderless: fillColor = UIColor.clear
|
||||
case .destructiveOutline: fillColor = UIColor.clear
|
||||
}
|
||||
let borderColor: UIColor
|
||||
switch style {
|
||||
|
@ -49,6 +50,7 @@ public final class Button : UIButton {
|
|||
case .prominentOutline: borderColor = isLightMode ? Colors.text : Colors.accent
|
||||
case .prominentFilled: borderColor = isLightMode ? Colors.text : Colors.accent
|
||||
case .regularBorderless: borderColor = UIColor.clear
|
||||
case .destructiveOutline: borderColor = Colors.destructive
|
||||
}
|
||||
let textColor: UIColor
|
||||
switch style {
|
||||
|
@ -57,6 +59,7 @@ public final class Button : UIButton {
|
|||
case .prominentOutline: textColor = isLightMode ? Colors.text : Colors.accent
|
||||
case .prominentFilled: textColor = isLightMode ? UIColor.white : Colors.text
|
||||
case .regularBorderless: textColor = Colors.text
|
||||
case .destructiveOutline: textColor = Colors.destructive
|
||||
}
|
||||
let height: CGFloat
|
||||
switch size {
|
||||
|
|
|
@ -4,18 +4,21 @@ public final class SearchBar : UISearchBar {
|
|||
|
||||
public override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
setUpStyle()
|
||||
setUpSessionStyle()
|
||||
}
|
||||
|
||||
public required init?(coder: NSCoder) {
|
||||
super.init(coder: coder)
|
||||
setUpStyle()
|
||||
setUpSessionStyle()
|
||||
}
|
||||
}
|
||||
|
||||
public extension UISearchBar {
|
||||
|
||||
private func setUpStyle() {
|
||||
func setUpSessionStyle() {
|
||||
searchBarStyle = .minimal // Hide the border around the search bar
|
||||
barStyle = .black // Use Apple's black design as a base
|
||||
tintColor = Colors.accent // The cursor color
|
||||
tintColor = Colors.text // The cursor color
|
||||
let searchImage = #imageLiteral(resourceName: "searchbar_search").withTint(Colors.searchBarPlaceholder)!
|
||||
setImage(searchImage, for: .search, state: .normal)
|
||||
let clearImage = #imageLiteral(resourceName: "searchbar_clear").withTint(Colors.searchBarPlaceholder)!
|
||||
|
|
|
@ -45,4 +45,8 @@ public final class Colors : NSObject {
|
|||
@objc public static var callMessageBackground: UIColor { UIColor(named: "session_call_message_background")! }
|
||||
@objc public static var pinIcon: UIColor { UIColor(named: "session_pin_icon")! }
|
||||
@objc public static var sessionHeading: UIColor { UIColor(named: "session_heading")! }
|
||||
@objc public static var sessionMessageRequestsBubble: UIColor { UIColor(named: "session_message_requests_bubble")! }
|
||||
@objc public static var sessionMessageRequestsIcon: UIColor { UIColor(named: "session_message_requests_icon")! }
|
||||
@objc public static var sessionMessageRequestsTitle: UIColor { UIColor(named: "session_message_requests_title")! }
|
||||
@objc public static var sessionMessageRequestsInfoText: UIColor { UIColor(named: "session_message_requests_info_text")! }
|
||||
}
|
||||
|
|
|
@ -11,6 +11,24 @@
|
|||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "dark"
|
||||
}
|
||||
],
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0xF9",
|
||||
"green" : "0xF9",
|
||||
"red" : "0xF9"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
|
|
|
@ -0,0 +1,38 @@
|
|||
{
|
||||
"colors" : [
|
||||
{
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0x58",
|
||||
"green" : "0x58",
|
||||
"red" : "0x58"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "dark"
|
||||
}
|
||||
],
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0x43",
|
||||
"green" : "0x43",
|
||||
"red" : "0x43"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
{
|
||||
"colors" : [
|
||||
{
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0xFF",
|
||||
"green" : "0xFF",
|
||||
"red" : "0xFF"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "dark"
|
||||
}
|
||||
],
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0xAD",
|
||||
"green" : "0xAD",
|
||||
"red" : "0xAD"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
{
|
||||
"colors" : [
|
||||
{
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0x9F",
|
||||
"green" : "0x9F",
|
||||
"red" : "0x9F"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue