Merge branch 'dev' of https://github.com/oxen-io/session-ios into ipad-support-1

This commit is contained in:
Ryan Zhao 2022-02-25 13:43:49 +11:00
commit c0615fe11d
82 changed files with 3854 additions and 552 deletions

View File

@ -65,7 +65,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 */; };
@ -675,7 +674,6 @@
C3A7225E2558C38D0043A11F /* Promise+Retaining.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3A7225D2558C38D0043A11F /* Promise+Retaining.swift */; };
C3A76A8D25DB83F90074CB90 /* PermissionMissingModal.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3A76A8C25DB83F90074CB90 /* PermissionMissingModal.swift */; };
C3AABDDF2553ECF00042FF4C /* Array+Description.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3C2A5D12553860800C340D1 /* Array+Description.swift */; };
C3AAFFE825AE975D0089E6DD /* ConfigurationMessage+Convenience.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3AAFFDE25AE96FF0089E6DD /* ConfigurationMessage+Convenience.swift */; };
C3AAFFF225AE99710089E6DD /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3AAFFF125AE99710089E6DD /* AppDelegate.swift */; };
C3ADC66126426688005F1414 /* ShareVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3ADC66026426688005F1414 /* ShareVC.swift */; };
C3BBE0762554CDA60050F1E3 /* Configuration.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3BBE0752554CDA60050F1E3 /* Configuration.swift */; };
@ -774,12 +772,20 @@
F5765D284BC6ECAC0C1D33F0 /* Pods_GlobalDependencies_FrameworkAndExtensionDependencies_SessionNotificationServiceExtension.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E4A93ECA93B3DE800CC7D7F6 /* Pods_GlobalDependencies_FrameworkAndExtensionDependencies_SessionNotificationServiceExtension.framework */; };
FC3BD9881A30A790005B96BB /* Social.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = FC3BD9871A30A790005B96BB /* Social.framework */; };
FCB11D8C1A129A76002F93FB /* CoreMedia.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = FCB11D8B1A129A76002F93FB /* CoreMedia.framework */; };
FD5D200F27AA2B6000FEA984 /* MessageRequestResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD5D200E27AA2B6000FEA984 /* MessageRequestResponse.swift */; };
FD5D201127AA331F00FEA984 /* ConfigurationMessage+Convenience.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD5D201027AA331F00FEA984 /* ConfigurationMessage+Convenience.swift */; };
FD659AC027A7649600F12C02 /* MessageRequestsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD659ABF27A7649600F12C02 /* MessageRequestsViewController.swift */; };
FD705A8C278CDB5600F16121 /* SAEScreenLockViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD705A8B278CDB5600F16121 /* SAEScreenLockViewController.swift */; };
FD705A8E278CE29800F16121 /* String+Localization.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD705A8D278CE29800F16121 /* String+Localization.swift */; };
FD705A90278CEBBC00F16121 /* ShareAppExtensionContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD705A8F278CEBBC00F16121 /* ShareAppExtensionContext.swift */; };
FD705A92278D051200F16121 /* ReusableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD705A91278D051200F16121 /* ReusableView.swift */; };
FD705A94278D052B00F16121 /* UITableView+ReusableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD705A93278D052B00F16121 /* UITableView+ReusableView.swift */; };
FD705A98278E9F4D00F16121 /* UIColor+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD705A97278E9F4D00F16121 /* UIColor+Extensions.swift */; };
FD859F0027C4691300510D0C /* MockDataGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD859EFF27C4691300510D0C /* MockDataGenerator.swift */; };
FD88BAD927A7439C00BBC442 /* MessageRequestsCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD88BAD827A7439C00BBC442 /* MessageRequestsCell.swift */; };
FD88BADB27A750F200BBC442 /* MessageRequestsMigration.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD88BADA27A750F200BBC442 /* MessageRequestsMigration.swift */; };
FDC4389E27BA2B8A00C60D73 /* SessionUtilitiesKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C3C2A679255388CC00C340D1 /* SessionUtilitiesKit.framework */; platformFilter = ios; };
FDC4389F27BA2B8A00C60D73 /* SessionUtilitiesKit.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = C3C2A679255388CC00C340D1 /* SessionUtilitiesKit.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
@ -895,6 +901,13 @@
remoteGlobalIDString = C33FD9AA255A548A00E217F9;
remoteInfo = SignalUtilitiesKit;
};
FDC438A027BA2B8A00C60D73 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = D221A080169C9E5E00537ABF /* Project object */;
proxyType = 1;
remoteGlobalIDString = C3C2A678255388CC00C340D1;
remoteInfo = SessionUtilitiesKit;
};
/* End PBXContainerItemProxy section */
/* Begin PBXCopyFilesBuildPhase section */
@ -925,6 +938,17 @@
name = "Embed Frameworks";
runOnlyForDeploymentPostprocessing = 0;
};
FDC438A227BA2B8A00C60D73 /* Embed Frameworks */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = "";
dstSubfolderSpec = 10;
files = (
FDC4389F27BA2B8A00C60D73 /* SessionUtilitiesKit.framework in Embed Frameworks */,
);
name = "Embed Frameworks";
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
@ -1026,7 +1050,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; };
@ -1699,7 +1722,6 @@
C3A76A8C25DB83F90074CB90 /* PermissionMissingModal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PermissionMissingModal.swift; sourceTree = "<group>"; };
C3A8AF752665B03900A467FE /* hi */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = hi; path = hi.lproj/Localizable.strings; sourceTree = "<group>"; };
C3A8AF762665F97A00A467FE /* fi */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fi; path = fi.lproj/Localizable.strings; sourceTree = "<group>"; };
C3AAFFDE25AE96FF0089E6DD /* ConfigurationMessage+Convenience.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ConfigurationMessage+Convenience.swift"; sourceTree = "<group>"; };
C3AAFFF125AE99710089E6DD /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
C3ADC66026426688005F1414 /* ShareVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareVC.swift; sourceTree = "<group>"; };
C3AECBEA24EF5244005743DE /* fa */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fa; path = fa.lproj/Localizable.strings; sourceTree = "<group>"; };
@ -1802,12 +1824,18 @@
F9BBF530D71905BA9007675F /* Pods-SessionShareExtension.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SessionShareExtension.debug.xcconfig"; path = "Pods/Target Support Files/Pods-SessionShareExtension/Pods-SessionShareExtension.debug.xcconfig"; sourceTree = "<group>"; };
FC3BD9871A30A790005B96BB /* Social.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Social.framework; path = System/Library/Frameworks/Social.framework; sourceTree = SDKROOT; };
FCB11D8B1A129A76002F93FB /* CoreMedia.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreMedia.framework; path = System/Library/Frameworks/CoreMedia.framework; sourceTree = SDKROOT; };
FD5D200E27AA2B6000FEA984 /* MessageRequestResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageRequestResponse.swift; sourceTree = "<group>"; };
FD5D201027AA331F00FEA984 /* ConfigurationMessage+Convenience.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ConfigurationMessage+Convenience.swift"; sourceTree = "<group>"; };
FD659ABF27A7649600F12C02 /* MessageRequestsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageRequestsViewController.swift; sourceTree = "<group>"; };
FD705A8B278CDB5600F16121 /* SAEScreenLockViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SAEScreenLockViewController.swift; sourceTree = "<group>"; };
FD705A8D278CE29800F16121 /* String+Localization.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+Localization.swift"; sourceTree = "<group>"; };
FD705A8F278CEBBC00F16121 /* ShareAppExtensionContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareAppExtensionContext.swift; sourceTree = "<group>"; };
FD705A91278D051200F16121 /* ReusableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReusableView.swift; sourceTree = "<group>"; };
FD705A93278D052B00F16121 /* UITableView+ReusableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UITableView+ReusableView.swift"; sourceTree = "<group>"; };
FD705A97278E9F4D00F16121 /* UIColor+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIColor+Extensions.swift"; sourceTree = "<group>"; };
FD859EFF27C4691300510D0C /* MockDataGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockDataGenerator.swift; sourceTree = "<group>"; };
FD88BAD827A7439C00BBC442 /* MessageRequestsCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageRequestsCell.swift; sourceTree = "<group>"; };
FD88BADA27A750F200BBC442 /* MessageRequestsMigration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageRequestsMigration.swift; sourceTree = "<group>"; };
FD9039443F7CB729CF71350E /* Pods-GlobalDependencies-FrameworkAndExtensionDependencies-SessionNotificationServiceExtension.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-GlobalDependencies-FrameworkAndExtensionDependencies-SessionNotificationServiceExtension.debug.xcconfig"; path = "Pods/Target Support Files/Pods-GlobalDependencies-FrameworkAndExtensionDependencies-SessionNotificationServiceExtension/Pods-GlobalDependencies-FrameworkAndExtensionDependencies-SessionNotificationServiceExtension.debug.xcconfig"; sourceTree = "<group>"; };
FEDBAE1B98C49BBE8C87F575 /* Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionUtilitiesKit.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionUtilitiesKit.debug.xcconfig"; path = "Pods/Target Support Files/Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionUtilitiesKit/Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionUtilitiesKit.debug.xcconfig"; sourceTree = "<group>"; };
FF9BA33D021B115B1F5B4E46 /* Pods-SessionMessagingKit.app store release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SessionMessagingKit.app store release.xcconfig"; path = "Pods/Target Support Files/Pods-SessionMessagingKit/Pods-SessionMessagingKit.app store release.xcconfig"; sourceTree = "<group>"; };
@ -1880,6 +1908,7 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
FDC4389E27BA2B8A00C60D73 /* SessionUtilitiesKit.framework in Frameworks */,
C3C2A70B25539E1E00C340D1 /* SessionSnodeKit.framework in Frameworks */,
9B0A583E9B89FEF0916B793A /* Pods_GlobalDependencies_FrameworkAndExtensionDependencies_ExtendedDependencies_SessionMessagingKit.framework in Frameworks */,
);
@ -2030,7 +2059,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 */,
@ -2045,6 +2073,7 @@
B83F2B87240CB75A000A54AB /* UIImage+Scaling.swift */,
C31A6C59247F214E001123EF /* UIView+Glow.swift */,
C3548F0724456AB6009433A8 /* UIView+Wrapping.swift */,
FD859EFF27C4691300510D0C /* MockDataGenerator.swift */,
);
path = Utilities;
sourceTree = "<group>";
@ -2450,7 +2479,9 @@
B8F5F60225EDE16F003BF8D4 /* DataExtractionNotification.swift */,
C300A5E62554B07300555489 /* ExpirationTimerUpdate.swift */,
C3DA9C0625AE7396008F7C7E /* ConfigurationMessage.swift */,
FD5D201027AA331F00FEA984 /* ConfigurationMessage+Convenience.swift */,
7B4C75CA26B37E0F0000AC89 /* UnsendRequest.swift */,
FD5D200E27AA2B6000FEA984 /* MessageRequestResponse.swift */,
);
path = "Control Messages";
sourceTree = "<group>";
@ -2818,9 +2849,11 @@
C360968E25AD16E8008B62B2 /* Home */ = {
isa = PBXGroup;
children = (
7BA7F4B9279F9F3700B3A466 /* GlobalSearch */,
FD659ABE27A7648200F12C02 /* Message Requests */,
FD88BAD727A7438E00BBC442 /* Views */,
B8BB82A4238F627000BA5194 /* HomeVC.swift */,
B83F2B85240C7B8F000A54AB /* NewConversationButtonSet.swift */,
7BA7F4B9279F9F3700B3A466 /* GlobalSearch */,
);
path = Home;
sourceTree = "<group>";
@ -3032,6 +3065,7 @@
isa = PBXGroup;
children = (
B8B32044258C117C0020074B /* ContactsMigration.swift */,
FD88BADA27A750F200BBC442 /* MessageRequestsMigration.swift */,
C38EF271255B6D79007E1867 /* OWSDatabaseMigration.h */,
C38EF270255B6D79007E1867 /* OWSDatabaseMigration.m */,
C38EF26F255B6D79007E1867 /* OWSDatabaseMigrationRunner.h */,
@ -3136,7 +3170,6 @@
C38EF2E3255B6DB9007E1867 /* OWSUnreadIndicator.m */,
C33FDAE8255A580500E217F9 /* OWSMessageUtils.h */,
C33FDBD7255A581900E217F9 /* OWSMessageUtils.m */,
C3AAFFDE25AE96FF0089E6DD /* ConfigurationMessage+Convenience.swift */,
);
path = Messaging;
sourceTree = "<group>";
@ -3599,6 +3632,22 @@
path = Session;
sourceTree = "<group>";
};
FD659ABE27A7648200F12C02 /* Message Requests */ = {
isa = PBXGroup;
children = (
FD659ABF27A7649600F12C02 /* MessageRequestsViewController.swift */,
);
path = "Message Requests";
sourceTree = "<group>";
};
FD88BAD727A7438E00BBC442 /* Views */ = {
isa = PBXGroup;
children = (
FD88BAD827A7439C00BBC442 /* MessageRequestsCell.swift */,
);
path = Views;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXHeadersBuildPhase section */
@ -3893,10 +3942,12 @@
C3C2A6EC25539DE700C340D1 /* Sources */,
C3C2A6ED25539DE700C340D1 /* Frameworks */,
C3C2A6EE25539DE700C340D1 /* Resources */,
FDC438A227BA2B8A00C60D73 /* Embed Frameworks */,
);
buildRules = (
);
dependencies = (
FDC438A127BA2B8A00C60D73 /* PBXTargetDependency */,
);
name = SessionMessagingKit;
productName = SessionMessagingKit;
@ -4548,13 +4599,13 @@
C33FDC98255A582000E217F9 /* SwiftSingletons.swift in Sources */,
C33FDC27255A581F00E217F9 /* YapDatabase+Promise.swift in Sources */,
C33FDCD3255A582000E217F9 /* GroupUtilities.swift in Sources */,
FD88BADB27A750F200BBC442 /* MessageRequestsMigration.swift in Sources */,
C38EF326255B6DBF007E1867 /* ConversationStyle.swift in Sources */,
C38EF3B8255B6DE7007E1867 /* ImageEditorTextViewController.swift in Sources */,
C33FDD91255A582000E217F9 /* OWSMessageUtils.m in Sources */,
B8C2B332256376F000551B4D /* ThreadUtil.m in Sources */,
C38EF40B255B6DF7007E1867 /* TappableStackView.swift in Sources */,
C38EF31D255B6DBF007E1867 /* UIImage+OWS.swift in Sources */,
C3AAFFE825AE975D0089E6DD /* ConfigurationMessage+Convenience.swift in Sources */,
C38EF359255B6DCC007E1867 /* SheetViewController.swift in Sources */,
B8F5F52925EC4F8A003BF8D4 /* BlockListUIUtils.m in Sources */,
C38EF386255B6DD2007E1867 /* AttachmentApprovalInputAccessoryView.swift in Sources */,
@ -4668,6 +4719,7 @@
C300A5F22554B09800555489 /* MessageSender.swift in Sources */,
C3C2A74D2553A39700C340D1 /* VisibleMessage.swift in Sources */,
C32C5AAD256DBE8F003C73A2 /* TSInfoMessage.m in Sources */,
FD5D201127AA331F00FEA984 /* ConfigurationMessage+Convenience.swift in Sources */,
C32C5A13256DB7A5003C73A2 /* PushNotificationAPI.swift in Sources */,
C32A026325A801AA000ED5D4 /* NSData+messagePadding.m in Sources */,
C352A3932557883D00338F3E /* JobDelegate.swift in Sources */,
@ -4726,6 +4778,7 @@
C32C5C4F256DCC36003C73A2 /* Storage+OpenGroups.swift in Sources */,
C3DA9C0725AE7396008F7C7E /* ConfigurationMessage.swift in Sources */,
B8856CEE256F1054001CE70E /* OWSAudioPlayer.m in Sources */,
FD5D200F27AA2B6000FEA984 /* MessageRequestResponse.swift in Sources */,
C32C5EDC256DF501003C73A2 /* YapDatabaseConnection+OWS.m in Sources */,
C3BBE0762554CDA60050F1E3 /* Configuration.swift in Sources */,
C35D76DB26606304009AA5FB /* ThreadUpdateBatcher.swift in Sources */,
@ -4855,7 +4908,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 */,
@ -4863,11 +4915,13 @@
3496957121A301A100DCFE74 /* OWSBackupImportJob.m in Sources */,
34BECE301F7ABCF800D7438D /* GifPickerLayout.swift in Sources */,
C331FFFE2558FF3B00070591 /* ConversationCell.swift in Sources */,
FD659AC027A7649600F12C02 /* MessageRequestsViewController.swift in Sources */,
B8F5F72325F1B4CA003BF8D4 /* DownloadAttachmentModal.swift in Sources */,
C3DFFAC623E96F0D0058DAF8 /* Sheet.swift in Sources */,
C31FFE57254A5FFE00F19441 /* KeyPairUtilities.swift in Sources */,
B8D84EA325DF745A005A043E /* LinkPreviewState.swift in Sources */,
45C0DC1E1E69011F00E04C47 /* UIStoryboard+OWS.swift in Sources */,
FD88BAD927A7439C00BBC442 /* MessageRequestsCell.swift in Sources */,
45A6DAD61EBBF85500893231 /* ReminderView.swift in Sources */,
B82B408E239DC00D00A248E7 /* DisplayNameVC.swift in Sources */,
B8214A2B25D63EB9009C0F2A /* MessagesTableView.swift in Sources */,
@ -4919,6 +4973,7 @@
34BECE2E1F7ABCE000D7438D /* GifPickerViewController.swift in Sources */,
B84664F5235022F30083A1CD /* MentionUtilities.swift in Sources */,
34D1F0C01F8EC1760066283D /* MessageRecipientStatusUtils.swift in Sources */,
FD859F0027C4691300510D0C /* MockDataGenerator.swift in Sources */,
C328250F25CA06020062D0A7 /* VoiceMessageView.swift in Sources */,
B82B4090239DD75000A248E7 /* RestoreVC.swift in Sources */,
3488F9362191CC4000E524CC /* MediaView.swift in Sources */,
@ -5052,6 +5107,12 @@
target = C33FD9AA255A548A00E217F9 /* SignalUtilitiesKit */;
targetProxy = C3D90A7025773A44002C9DF5 /* PBXContainerItemProxy */;
};
FDC438A127BA2B8A00C60D73 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
platformFilter = ios;
target = C3C2A678255388CC00C340D1 /* SessionUtilitiesKit */;
targetProxy = FDC438A027BA2B8A00C60D73 /* PBXContainerItemProxy */;
};
/* End PBXTargetDependency section */
/* Begin PBXVariantGroup section */

View File

@ -36,6 +36,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)
})
}
@ -217,9 +224,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()
@ -228,28 +238,55 @@ 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)
}
self?.handleMessageSent()
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(
with: { transaction in
self?.approveMessageRequestIfNeeded(
for: self?.thread,
with: (transaction as! YapDatabaseReadWriteTransaction),
isNewThread: !oldThreadShouldBeVisible,
timestamp: (sentTimestamp - 1) // Set 1ms earlier as this is used for sorting
)
},
completion: { [weak self] in
Storage.shared.write { transaction in
MessageSender.send(message, with: [], in: thread, using: transaction as! YapDatabaseReadWriteTransaction)
}
self?.handleMessageSent()
}
)
})
}
@ -287,6 +324,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)
@ -1018,3 +1070,153 @@ 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) {
guard let contactThread: TSContactThread = thread as? TSContactThread else { return }
// 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))
if !contact.isApproved {
// 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)
// 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)
if !isNewThread {
let messageRequestResponse: MessageRequestResponse = MessageRequestResponse(
isApproved: true
)
messageRequestResponse.sentTimestamp = timestamp
MessageSender.send(messageRequestResponse, in: contactThread, 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
)
}
}
// 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 { [weak self] transaction in
self?.approveMessageRequestIfNeeded(
for: self?.thread,
with: transaction,
isNewThread: false,
timestamp: NSDate.millisecondTimestamp()
)
}
}
@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)
}
}

View File

@ -1,3 +1,5 @@
import SessionUIKit
import SessionMessagingKit
// TODO:
// Slight paging glitch when scrolling up and loading more content
@ -7,10 +9,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 +98,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 +109,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 +144,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 +160,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 +274,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 +300,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 +353,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 +373,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()
@ -296,11 +463,14 @@ final class ConversationVC : BaseVC, ConversationViewModelDelegate, OWSConversat
}
// MARK: Updating
func updateNavBarButtons() {
navigationItem.hidesBackButton = isShowingSearchUI
if isShowingSearchUI {
navigationItem.leftBarButtonItem = nil
navigationItem.rightBarButtonItems = []
} else {
navigationItem.leftBarButtonItem = UIViewController.createOWSBackButton(withTarget: self, selector: #selector(handleBackPressed))
let rightBarButtonItem: UIBarButtonItem
if thread is TSContactThread {
let size = Values.verySmallProfilePictureSize
@ -330,24 +500,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() {
@ -392,6 +634,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() {
@ -457,6 +713,7 @@ final class ConversationVC : BaseVC, ConversationViewModelDelegate, OWSConversat
}
func markAllAsRead() {
guard !thread.isMessageRequest() else { return }
guard let lastSortID = viewItems.last?.interaction.sortId else { return }
OWSReadReceiptManager.shared().markAsReadLocally(beforeSortId: lastSortID, thread: thread)
SSKEnvironment.shared.disappearingMessagesJob.cleanupMessagesWhichFailedToStartExpiringFromNow()

View File

@ -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()

View File

@ -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

View File

@ -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)

View File

@ -358,6 +358,13 @@ CGFloat kIconViewLength = 24;
switchView.on = strongSelf.disappearingMessagesConfiguration.isEnabled;
[switchView addTarget:strongSelf action:@selector(disappearingMessagesSwitchValueDidChange:)
forControlEvents:UIControlEventValueChanged];
// Disable Disappearing Messages if the conversation hasn't been approved
if (!self.thread.isGroupThread) {
TSContactThread *thread = (TSContactThread *)self.thread;
SNContact *contact = [LKStorage.shared getContactWithSessionID:thread.contactSessionID];
[switchView setEnabled:(contact.isApproved && contact.didApproveMe)];
}
UIStackView *topRow =
[[UIStackView alloc] initWithArrangedSubviews:@[ iconView, rowLabel, switchView ]];
@ -430,6 +437,13 @@ CGFloat kIconViewLength = 24;
[slider autoPinEdge:ALEdgeLeading toEdge:ALEdgeLeading ofView:rowLabel];
[slider autoPinTrailingToSuperviewMargin];
[slider autoPinBottomToSuperviewMargin];
// Disable Disappearing Messages slider if the conversation hasn't been approved (just in case)
if (!self.thread.isGroupThread) {
TSContactThread *thread = (TSContactThread *)self.thread;
SNContact *contact = [LKStorage.shared getContactWithSessionID:thread.contactSessionID];
[slider setEnabled:(contact.isApproved && contact.didApproveMe)];
}
cell.userInteractionEnabled = !strongSelf.hasLeftGroup;

View File

@ -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()

View File

@ -7,6 +7,10 @@ final class HomeVC : BaseVC, UITableViewDataSource, UITableViewDelegate, NewConv
private var threadViewModelCache: [String:ThreadViewModel] = [:] // Thread ID to ThreadViewModel
private var tableViewTopConstraint: NSLayoutConstraint!
private var messageRequestCount: UInt {
threads.numberOfItems(inGroup: TSMessageRequestGroup)
}
private var threadCount: UInt {
threads.numberOfItems(inGroup: TSInboxGroup)
}
@ -34,6 +38,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 +137,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 +172,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 messageRequestCount > 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(messageRequestCount))
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 +232,94 @@ 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
}
}
return reload()
}
}
var sectionChanges = NSArray()
var rowChanges = NSArray()
ext.getSectionChanges(&sectionChanges, 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 tableView.numberOfRows(inSection: 0) == 1 && Int(messageRequestCount) <= 0 {
tableView.deleteRows(at: [IndexPath(row: 0, section: 0)], with: .automatic)
}
else if tableView.numberOfRows(inSection: 0) == 0 && Int(messageRequestCount) > 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 +332,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()
@ -308,19 +412,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 +520,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.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) ]
}
}
private func delete(_ thread: TSThread) {
let openGroupV2 = Storage.shared.getV2OpenGroup(for: thread.uniqueId!)
Storage.write { transaction in
@ -465,8 +597,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
}

View File

@ -0,0 +1,447 @@
// 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: UIButton = {
let result: UIButton = UIButton()
result.translatesAutoresizingMaskIntoConstraints = false
result.clipsToBounds = true
result.titleLabel?.font = UIFont.boldSystemFont(ofSize: 18)
result.setTitle(NSLocalizedString("MESSAGE_REQUESTS_CLEAR_ALL", 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.isHidden = true
result.layer.cornerRadius = (NewConversationButtonSet.collapsedButtonSize / 2)
result.layer.borderColor = Colors.destructive.cgColor
result.layer.borderWidth = 1.5
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.bottomAnchor,
constant: -Values.newConversationButtonBottomOffset // Negative due to how the constraint is set up
),
clearAllButton.widthAnchor.constraint(equalToConstant: 155),
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(&sectionChanges, 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(&sectionChanges, 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
}
}
}

View File

@ -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.cellBackground
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)
}
}

View File

@ -14,12 +14,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

View File

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "message_requests.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View 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

View File

@ -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. */
@ -597,6 +603,17 @@
"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_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";

View File

@ -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. */
@ -607,6 +613,17 @@
"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_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";

View File

@ -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. */
@ -597,6 +603,17 @@
"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_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";

View File

@ -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. */
@ -597,6 +603,17 @@
"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_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";

View File

@ -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. */
@ -597,6 +603,17 @@
"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_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";

View File

@ -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. */
@ -597,6 +603,17 @@
"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_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";

View File

@ -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. */
@ -597,6 +603,17 @@
"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_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";

View File

@ -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. */
@ -597,6 +603,17 @@
"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_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";

View File

@ -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. */
@ -597,6 +603,17 @@
"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_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";

View File

@ -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. */
@ -597,6 +603,17 @@
"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_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";

View File

@ -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. */
@ -597,6 +603,17 @@
"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_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";

View File

@ -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. */
@ -597,6 +603,17 @@
"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_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";

View File

@ -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. */
@ -597,6 +603,17 @@
"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_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";

View File

@ -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. */
@ -597,6 +603,17 @@
"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_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";

View File

@ -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. */
@ -597,6 +603,17 @@
"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_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";

View File

@ -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. */
@ -598,6 +604,17 @@
"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_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";

View File

@ -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. */
@ -597,6 +603,17 @@
"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_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";

View File

@ -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. */
@ -597,6 +603,17 @@
"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_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";

View File

@ -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. */
@ -597,6 +603,17 @@
"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_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";

View File

@ -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. */
@ -597,6 +603,17 @@
"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_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";

View File

@ -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. */
@ -597,6 +603,17 @@
"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_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";

View File

@ -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. */
@ -597,6 +603,17 @@
"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_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";

View File

@ -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
)
}
}

View File

@ -53,9 +53,9 @@ public enum PushRegistrationError: Error {
return firstly {
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().map { vanillaPushToken -> (pushToken: String, voipToken: String) in
return (pushToken: vanillaPushToken, voipToken: "")

View File

@ -264,6 +264,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)),
@ -515,6 +517,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

View File

@ -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

View File

@ -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)
}
}

View File

@ -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) }
}

View File

@ -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)
}
}
}
}

View File

@ -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
}()
}

View File

@ -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

View File

@ -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
}

View File

@ -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;

View File

@ -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 {

View File

@ -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
)
}
}

View File

@ -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 {

View File

@ -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)
)
"""
}
}

View File

@ -15,7 +15,8 @@ typedef NS_ENUM(NSInteger, TSInfoMessageType) {
TSInfoMessageTypeGroupCurrentUserLeft,
TSInfoMessageTypeDisappearingMessagesUpdate,
TSInfoMessageTypeScreenshotNotification,
TSInfoMessageTypeMediaSavedNotification
TSInfoMessageTypeMediaSavedNotification,
TSInfoMessageTypeMessageRequestAccepted
};
@property (atomic, readonly) TSInfoMessageType messageType;

View File

@ -112,6 +112,8 @@ NSUInteger TSInfoMessageSchemaVersion = 1;
return NSLocalizedString(@"GROUP_YOU_LEFT", @"");
case TSInfoMessageTypeGroupUpdated:
return _customMessage != nil ? _customMessage : NSLocalizedString(@"GROUP_UPDATED", @"");
case TSInfoMessageTypeMessageRequestAccepted:
return NSLocalizedString(@"MESSAGE_REQUESTS_ACCEPTED", @"");
default:
break;
}

View File

@ -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 {
@ -481,6 +578,9 @@ extension SNProtoUnsendRequest.SNProtoUnsendRequestBuilder {
if let _value = unsendRequest {
builder.setUnsendRequest(_value)
}
if let _value = messageRequestResponse {
builder.setMessageRequestResponse(_value)
}
return builder
}
@ -514,6 +614,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)
}
@ -537,13 +641,16 @@ extension SNProtoUnsendRequest.SNProtoUnsendRequestBuilder {
@objc public let unsendRequest: SNProtoUnsendRequest?
@objc public let messageRequestResponse: SNProtoMessageRequestResponse?
private init(proto: SessionProtos_Content,
dataMessage: SNProtoDataMessage?,
receiptMessage: SNProtoReceiptMessage?,
typingMessage: SNProtoTypingMessage?,
configurationMessage: SNProtoConfigurationMessage?,
dataExtractionNotification: SNProtoDataExtractionNotification?,
unsendRequest: SNProtoUnsendRequest?) {
unsendRequest: SNProtoUnsendRequest?,
messageRequestResponse: SNProtoMessageRequestResponse?) {
self.proto = proto
self.dataMessage = dataMessage
self.receiptMessage = receiptMessage
@ -551,6 +658,7 @@ extension SNProtoUnsendRequest.SNProtoUnsendRequestBuilder {
self.configurationMessage = configurationMessage
self.dataExtractionNotification = dataExtractionNotification
self.unsendRequest = unsendRequest
self.messageRequestResponse = messageRequestResponse
}
@objc
@ -594,6 +702,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 -
@ -604,7 +717,8 @@ extension SNProtoUnsendRequest.SNProtoUnsendRequestBuilder {
typingMessage: typingMessage,
configurationMessage: configurationMessage,
dataExtractionNotification: dataExtractionNotification,
unsendRequest: unsendRequest)
unsendRequest: unsendRequest,
messageRequestResponse: messageRequestResponse)
return result
}
@ -2396,6 +2510,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
}
@ -2428,6 +2551,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)
}
@ -2463,6 +2598,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) {

View File

@ -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
@ -288,6 +310,15 @@ struct SessionProtos_Content {
/// Clears the value of `unsendRequest`. Subsequent reads from it will return its default value.
mutating func clearUnsendRequest() {self._unsendRequest = nil}
var messageRequestResponse: SessionProtos_MessageRequestResponse {
get {return _messageRequestResponse ?? SessionProtos_MessageRequestResponse()}
set {_messageRequestResponse = newValue}
}
/// Returns true if `messageRequestResponse` has been explicitly set.
var hasMessageRequestResponse: Bool {return self._messageRequestResponse != nil}
/// Clears the value of `messageRequestResponse`. Subsequent reads from it will return its default value.
mutating func clearMessageRequestResponse() {self._messageRequestResponse = nil}
var unknownFields = SwiftProtobuf.UnknownStorage()
init() {}
@ -298,6 +329,7 @@ struct SessionProtos_Content {
fileprivate var _configurationMessage: SessionProtos_ConfigurationMessage? = nil
fileprivate var _dataExtractionNotification: SessionProtos_DataExtractionNotification? = nil
fileprivate var _unsendRequest: SessionProtos_UnsendRequest? = nil
fileprivate var _messageRequestResponse: SessionProtos_MessageRequestResponse? = nil
}
struct SessionProtos_KeyPair {
@ -1076,6 +1108,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() {}
@ -1084,6 +1146,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() {}
@ -1456,24 +1521,28 @@ extension SessionProtos_Envelope: SwiftProtobuf.Message, SwiftProtobuf._MessageI
}
func traverse<V: SwiftProtobuf.Visitor>(visitor: inout V) throws {
if let v = self._type {
// The use of inline closures is to circumvent an issue where the compiler
// allocates stack space for every if/case branch local when no optimizations
// are enabled. https://github.com/apple/swift-protobuf/issues/1034 and
// https://github.com/apple/swift-protobuf/issues/1182
try { if let v = self._type {
try visitor.visitSingularEnumField(value: v, fieldNumber: 1)
}
if let v = self._source {
} }()
try { if let v = self._source {
try visitor.visitSingularStringField(value: v, fieldNumber: 2)
}
if let v = self._timestamp {
} }()
try { if let v = self._timestamp {
try visitor.visitSingularUInt64Field(value: v, fieldNumber: 5)
}
if let v = self._sourceDevice {
} }()
try { if let v = self._sourceDevice {
try visitor.visitSingularUInt32Field(value: v, fieldNumber: 7)
}
if let v = self._content {
} }()
try { if let v = self._content {
try visitor.visitSingularBytesField(value: v, fieldNumber: 8)
}
if let v = self._serverTimestamp {
} }()
try { if let v = self._serverTimestamp {
try visitor.visitSingularUInt64Field(value: v, fieldNumber: 10)
}
} }()
try unknownFields.traverse(visitor: &visitor)
}
@ -1523,12 +1592,16 @@ extension SessionProtos_TypingMessage: SwiftProtobuf.Message, SwiftProtobuf._Mes
}
func traverse<V: SwiftProtobuf.Visitor>(visitor: inout V) throws {
if let v = self._timestamp {
// The use of inline closures is to circumvent an issue where the compiler
// allocates stack space for every if/case branch local when no optimizations
// are enabled. https://github.com/apple/swift-protobuf/issues/1034 and
// https://github.com/apple/swift-protobuf/issues/1182
try { if let v = self._timestamp {
try visitor.visitSingularUInt64Field(value: v, fieldNumber: 1)
}
if let v = self._action {
} }()
try { if let v = self._action {
try visitor.visitSingularEnumField(value: v, fieldNumber: 2)
}
} }()
try unknownFields.traverse(visitor: &visitor)
}
@ -1574,12 +1647,16 @@ extension SessionProtos_UnsendRequest: SwiftProtobuf.Message, SwiftProtobuf._Mes
}
func traverse<V: SwiftProtobuf.Visitor>(visitor: inout V) throws {
if let v = self._timestamp {
// The use of inline closures is to circumvent an issue where the compiler
// allocates stack space for every if/case branch local when no optimizations
// are enabled. https://github.com/apple/swift-protobuf/issues/1034 and
// https://github.com/apple/swift-protobuf/issues/1182
try { if let v = self._timestamp {
try visitor.visitSingularUInt64Field(value: v, fieldNumber: 1)
}
if let v = self._author {
} }()
try { if let v = self._author {
try visitor.visitSingularStringField(value: v, fieldNumber: 2)
}
} }()
try unknownFields.traverse(visitor: &visitor)
}
@ -1591,6 +1668,47 @@ 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 {
// The use of inline closures is to circumvent an issue where the compiler
// allocates stack space for every if/case branch local when no optimizations
// are enabled. https://github.com/apple/swift-protobuf/issues/1034 and
// https://github.com/apple/swift-protobuf/issues/1182
try { 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 = [
@ -1600,6 +1718,7 @@ extension SessionProtos_Content: SwiftProtobuf.Message, SwiftProtobuf._MessageIm
7: .same(proto: "configurationMessage"),
8: .same(proto: "dataExtractionNotification"),
9: .same(proto: "unsendRequest"),
10: .same(proto: "messageRequestResponse"),
]
public var isInitialized: Bool {
@ -1609,6 +1728,7 @@ extension SessionProtos_Content: SwiftProtobuf.Message, SwiftProtobuf._MessageIm
if let v = self._configurationMessage, !v.isInitialized {return false}
if let v = self._dataExtractionNotification, !v.isInitialized {return false}
if let v = self._unsendRequest, !v.isInitialized {return false}
if let v = self._messageRequestResponse, !v.isInitialized {return false}
return true
}
@ -1624,30 +1744,38 @@ extension SessionProtos_Content: SwiftProtobuf.Message, SwiftProtobuf._MessageIm
case 7: try { try decoder.decodeSingularMessageField(value: &self._configurationMessage) }()
case 8: try { try decoder.decodeSingularMessageField(value: &self._dataExtractionNotification) }()
case 9: try { try decoder.decodeSingularMessageField(value: &self._unsendRequest) }()
case 10: try { try decoder.decodeSingularMessageField(value: &self._messageRequestResponse) }()
default: break
}
}
}
func traverse<V: SwiftProtobuf.Visitor>(visitor: inout V) throws {
if let v = self._dataMessage {
// The use of inline closures is to circumvent an issue where the compiler
// allocates stack space for every if/case branch local when no optimizations
// are enabled. https://github.com/apple/swift-protobuf/issues/1034 and
// https://github.com/apple/swift-protobuf/issues/1182
try { if let v = self._dataMessage {
try visitor.visitSingularMessageField(value: v, fieldNumber: 1)
}
if let v = self._receiptMessage {
} }()
try { if let v = self._receiptMessage {
try visitor.visitSingularMessageField(value: v, fieldNumber: 5)
}
if let v = self._typingMessage {
} }()
try { if let v = self._typingMessage {
try visitor.visitSingularMessageField(value: v, fieldNumber: 6)
}
if let v = self._configurationMessage {
} }()
try { if let v = self._configurationMessage {
try visitor.visitSingularMessageField(value: v, fieldNumber: 7)
}
if let v = self._dataExtractionNotification {
} }()
try { if let v = self._dataExtractionNotification {
try visitor.visitSingularMessageField(value: v, fieldNumber: 8)
}
if let v = self._unsendRequest {
} }()
try { if let v = self._unsendRequest {
try visitor.visitSingularMessageField(value: v, fieldNumber: 9)
}
} }()
try { if let v = self._messageRequestResponse {
try visitor.visitSingularMessageField(value: v, fieldNumber: 10)
} }()
try unknownFields.traverse(visitor: &visitor)
}
@ -1658,6 +1786,7 @@ extension SessionProtos_Content: SwiftProtobuf.Message, SwiftProtobuf._MessageIm
if lhs._configurationMessage != rhs._configurationMessage {return false}
if lhs._dataExtractionNotification != rhs._dataExtractionNotification {return false}
if lhs._unsendRequest != rhs._unsendRequest {return false}
if lhs._messageRequestResponse != rhs._messageRequestResponse {return false}
if lhs.unknownFields != rhs.unknownFields {return false}
return true
}
@ -1690,12 +1819,16 @@ extension SessionProtos_KeyPair: SwiftProtobuf.Message, SwiftProtobuf._MessageIm
}
func traverse<V: SwiftProtobuf.Visitor>(visitor: inout V) throws {
if let v = self._publicKey {
// The use of inline closures is to circumvent an issue where the compiler
// allocates stack space for every if/case branch local when no optimizations
// are enabled. https://github.com/apple/swift-protobuf/issues/1034 and
// https://github.com/apple/swift-protobuf/issues/1182
try { if let v = self._publicKey {
try visitor.visitSingularBytesField(value: v, fieldNumber: 1)
}
if let v = self._privateKey {
} }()
try { if let v = self._privateKey {
try visitor.visitSingularBytesField(value: v, fieldNumber: 2)
}
} }()
try unknownFields.traverse(visitor: &visitor)
}
@ -1733,12 +1866,16 @@ extension SessionProtos_DataExtractionNotification: SwiftProtobuf.Message, Swift
}
func traverse<V: SwiftProtobuf.Visitor>(visitor: inout V) throws {
if let v = self._type {
// The use of inline closures is to circumvent an issue where the compiler
// allocates stack space for every if/case branch local when no optimizations
// are enabled. https://github.com/apple/swift-protobuf/issues/1034 and
// https://github.com/apple/swift-protobuf/issues/1182
try { if let v = self._type {
try visitor.visitSingularEnumField(value: v, fieldNumber: 1)
}
if let v = self._timestamp {
} }()
try { if let v = self._timestamp {
try visitor.visitSingularUInt64Field(value: v, fieldNumber: 2)
}
} }()
try unknownFields.traverse(visitor: &visitor)
}
@ -1859,45 +1996,49 @@ extension SessionProtos_DataMessage: SwiftProtobuf.Message, SwiftProtobuf._Messa
func traverse<V: SwiftProtobuf.Visitor>(visitor: inout V) throws {
try withExtendedLifetime(_storage) { (_storage: _StorageClass) in
if let v = _storage._body {
// The use of inline closures is to circumvent an issue where the compiler
// allocates stack space for every if/case branch local when no optimizations
// are enabled. https://github.com/apple/swift-protobuf/issues/1034 and
// https://github.com/apple/swift-protobuf/issues/1182
try { if let v = _storage._body {
try visitor.visitSingularStringField(value: v, fieldNumber: 1)
}
} }()
if !_storage._attachments.isEmpty {
try visitor.visitRepeatedMessageField(value: _storage._attachments, fieldNumber: 2)
}
if let v = _storage._group {
try { if let v = _storage._group {
try visitor.visitSingularMessageField(value: v, fieldNumber: 3)
}
if let v = _storage._flags {
} }()
try { if let v = _storage._flags {
try visitor.visitSingularUInt32Field(value: v, fieldNumber: 4)
}
if let v = _storage._expireTimer {
} }()
try { if let v = _storage._expireTimer {
try visitor.visitSingularUInt32Field(value: v, fieldNumber: 5)
}
if let v = _storage._profileKey {
} }()
try { if let v = _storage._profileKey {
try visitor.visitSingularBytesField(value: v, fieldNumber: 6)
}
if let v = _storage._timestamp {
} }()
try { if let v = _storage._timestamp {
try visitor.visitSingularUInt64Field(value: v, fieldNumber: 7)
}
if let v = _storage._quote {
} }()
try { if let v = _storage._quote {
try visitor.visitSingularMessageField(value: v, fieldNumber: 8)
}
} }()
if !_storage._preview.isEmpty {
try visitor.visitRepeatedMessageField(value: _storage._preview, fieldNumber: 10)
}
if let v = _storage._profile {
try { if let v = _storage._profile {
try visitor.visitSingularMessageField(value: v, fieldNumber: 101)
}
if let v = _storage._openGroupInvitation {
} }()
try { if let v = _storage._openGroupInvitation {
try visitor.visitSingularMessageField(value: v, fieldNumber: 102)
}
if let v = _storage._closedGroupControlMessage {
} }()
try { if let v = _storage._closedGroupControlMessage {
try visitor.visitSingularMessageField(value: v, fieldNumber: 104)
}
if let v = _storage._syncTarget {
} }()
try { if let v = _storage._syncTarget {
try visitor.visitSingularStringField(value: v, fieldNumber: 105)
}
} }()
}
try unknownFields.traverse(visitor: &visitor)
}
@ -1967,15 +2108,19 @@ extension SessionProtos_DataMessage.Quote: SwiftProtobuf.Message, SwiftProtobuf.
}
func traverse<V: SwiftProtobuf.Visitor>(visitor: inout V) throws {
if let v = self._id {
// The use of inline closures is to circumvent an issue where the compiler
// allocates stack space for every if/case branch local when no optimizations
// are enabled. https://github.com/apple/swift-protobuf/issues/1034 and
// https://github.com/apple/swift-protobuf/issues/1182
try { if let v = self._id {
try visitor.visitSingularUInt64Field(value: v, fieldNumber: 1)
}
if let v = self._author {
} }()
try { if let v = self._author {
try visitor.visitSingularStringField(value: v, fieldNumber: 2)
}
if let v = self._text {
} }()
try { if let v = self._text {
try visitor.visitSingularStringField(value: v, fieldNumber: 3)
}
} }()
if !self.attachments.isEmpty {
try visitor.visitRepeatedMessageField(value: self.attachments, fieldNumber: 4)
}
@ -2022,18 +2167,22 @@ extension SessionProtos_DataMessage.Quote.QuotedAttachment: SwiftProtobuf.Messag
}
func traverse<V: SwiftProtobuf.Visitor>(visitor: inout V) throws {
if let v = self._contentType {
// The use of inline closures is to circumvent an issue where the compiler
// allocates stack space for every if/case branch local when no optimizations
// are enabled. https://github.com/apple/swift-protobuf/issues/1034 and
// https://github.com/apple/swift-protobuf/issues/1182
try { if let v = self._contentType {
try visitor.visitSingularStringField(value: v, fieldNumber: 1)
}
if let v = self._fileName {
} }()
try { if let v = self._fileName {
try visitor.visitSingularStringField(value: v, fieldNumber: 2)
}
if let v = self._thumbnail {
} }()
try { if let v = self._thumbnail {
try visitor.visitSingularMessageField(value: v, fieldNumber: 3)
}
if let v = self._flags {
} }()
try { if let v = self._flags {
try visitor.visitSingularUInt32Field(value: v, fieldNumber: 4)
}
} }()
try unknownFields.traverse(visitor: &visitor)
}
@ -2082,15 +2231,19 @@ extension SessionProtos_DataMessage.Preview: SwiftProtobuf.Message, SwiftProtobu
}
func traverse<V: SwiftProtobuf.Visitor>(visitor: inout V) throws {
if let v = self._url {
// The use of inline closures is to circumvent an issue where the compiler
// allocates stack space for every if/case branch local when no optimizations
// are enabled. https://github.com/apple/swift-protobuf/issues/1034 and
// https://github.com/apple/swift-protobuf/issues/1182
try { if let v = self._url {
try visitor.visitSingularStringField(value: v, fieldNumber: 1)
}
if let v = self._title {
} }()
try { if let v = self._title {
try visitor.visitSingularStringField(value: v, fieldNumber: 2)
}
if let v = self._image {
} }()
try { if let v = self._image {
try visitor.visitSingularMessageField(value: v, fieldNumber: 3)
}
} }()
try unknownFields.traverse(visitor: &visitor)
}
@ -2124,12 +2277,16 @@ extension SessionProtos_DataMessage.LokiProfile: SwiftProtobuf.Message, SwiftPro
}
func traverse<V: SwiftProtobuf.Visitor>(visitor: inout V) throws {
if let v = self._displayName {
// The use of inline closures is to circumvent an issue where the compiler
// allocates stack space for every if/case branch local when no optimizations
// are enabled. https://github.com/apple/swift-protobuf/issues/1034 and
// https://github.com/apple/swift-protobuf/issues/1182
try { if let v = self._displayName {
try visitor.visitSingularStringField(value: v, fieldNumber: 1)
}
if let v = self._profilePicture {
} }()
try { if let v = self._profilePicture {
try visitor.visitSingularStringField(value: v, fieldNumber: 2)
}
} }()
try unknownFields.traverse(visitor: &visitor)
}
@ -2168,12 +2325,16 @@ extension SessionProtos_DataMessage.OpenGroupInvitation: SwiftProtobuf.Message,
}
func traverse<V: SwiftProtobuf.Visitor>(visitor: inout V) throws {
if let v = self._url {
// The use of inline closures is to circumvent an issue where the compiler
// allocates stack space for every if/case branch local when no optimizations
// are enabled. https://github.com/apple/swift-protobuf/issues/1034 and
// https://github.com/apple/swift-protobuf/issues/1182
try { if let v = self._url {
try visitor.visitSingularStringField(value: v, fieldNumber: 1)
}
if let v = self._name {
} }()
try { if let v = self._name {
try visitor.visitSingularStringField(value: v, fieldNumber: 3)
}
} }()
try unknownFields.traverse(visitor: &visitor)
}
@ -2225,18 +2386,22 @@ extension SessionProtos_DataMessage.ClosedGroupControlMessage: SwiftProtobuf.Mes
}
func traverse<V: SwiftProtobuf.Visitor>(visitor: inout V) throws {
if let v = self._type {
// The use of inline closures is to circumvent an issue where the compiler
// allocates stack space for every if/case branch local when no optimizations
// are enabled. https://github.com/apple/swift-protobuf/issues/1034 and
// https://github.com/apple/swift-protobuf/issues/1182
try { if let v = self._type {
try visitor.visitSingularEnumField(value: v, fieldNumber: 1)
}
if let v = self._publicKey {
} }()
try { if let v = self._publicKey {
try visitor.visitSingularBytesField(value: v, fieldNumber: 2)
}
if let v = self._name {
} }()
try { if let v = self._name {
try visitor.visitSingularStringField(value: v, fieldNumber: 3)
}
if let v = self._encryptionKeyPair {
} }()
try { if let v = self._encryptionKeyPair {
try visitor.visitSingularMessageField(value: v, fieldNumber: 4)
}
} }()
if !self.members.isEmpty {
try visitor.visitRepeatedBytesField(value: self.members, fieldNumber: 5)
}
@ -2246,9 +2411,9 @@ extension SessionProtos_DataMessage.ClosedGroupControlMessage: SwiftProtobuf.Mes
if !self.wrappers.isEmpty {
try visitor.visitRepeatedMessageField(value: self.wrappers, fieldNumber: 7)
}
if let v = self._expirationTimer {
try { if let v = self._expirationTimer {
try visitor.visitSingularUInt32Field(value: v, fieldNumber: 8)
}
} }()
try unknownFields.traverse(visitor: &visitor)
}
@ -2305,12 +2470,16 @@ extension SessionProtos_DataMessage.ClosedGroupControlMessage.KeyPairWrapper: Sw
}
func traverse<V: SwiftProtobuf.Visitor>(visitor: inout V) throws {
if let v = self._publicKey {
// The use of inline closures is to circumvent an issue where the compiler
// allocates stack space for every if/case branch local when no optimizations
// are enabled. https://github.com/apple/swift-protobuf/issues/1034 and
// https://github.com/apple/swift-protobuf/issues/1182
try { if let v = self._publicKey {
try visitor.visitSingularBytesField(value: v, fieldNumber: 1)
}
if let v = self._encryptedKeyPair {
} }()
try { if let v = self._encryptedKeyPair {
try visitor.visitSingularBytesField(value: v, fieldNumber: 2)
}
} }()
try unknownFields.traverse(visitor: &visitor)
}
@ -2357,21 +2526,25 @@ extension SessionProtos_ConfigurationMessage: SwiftProtobuf.Message, SwiftProtob
}
func traverse<V: SwiftProtobuf.Visitor>(visitor: inout V) throws {
// The use of inline closures is to circumvent an issue where the compiler
// allocates stack space for every if/case branch local when no optimizations
// are enabled. https://github.com/apple/swift-protobuf/issues/1034 and
// https://github.com/apple/swift-protobuf/issues/1182
if !self.closedGroups.isEmpty {
try visitor.visitRepeatedMessageField(value: self.closedGroups, fieldNumber: 1)
}
if !self.openGroups.isEmpty {
try visitor.visitRepeatedStringField(value: self.openGroups, fieldNumber: 2)
}
if let v = self._displayName {
try { if let v = self._displayName {
try visitor.visitSingularStringField(value: v, fieldNumber: 3)
}
if let v = self._profilePicture {
} }()
try { if let v = self._profilePicture {
try visitor.visitSingularStringField(value: v, fieldNumber: 4)
}
if let v = self._profileKey {
} }()
try { if let v = self._profileKey {
try visitor.visitSingularBytesField(value: v, fieldNumber: 5)
}
} }()
if !self.contacts.isEmpty {
try visitor.visitRepeatedMessageField(value: self.contacts, fieldNumber: 6)
}
@ -2424,24 +2597,28 @@ extension SessionProtos_ConfigurationMessage.ClosedGroup: SwiftProtobuf.Message,
}
func traverse<V: SwiftProtobuf.Visitor>(visitor: inout V) throws {
if let v = self._publicKey {
// The use of inline closures is to circumvent an issue where the compiler
// allocates stack space for every if/case branch local when no optimizations
// are enabled. https://github.com/apple/swift-protobuf/issues/1034 and
// https://github.com/apple/swift-protobuf/issues/1182
try { if let v = self._publicKey {
try visitor.visitSingularBytesField(value: v, fieldNumber: 1)
}
if let v = self._name {
} }()
try { if let v = self._name {
try visitor.visitSingularStringField(value: v, fieldNumber: 2)
}
if let v = self._encryptionKeyPair {
} }()
try { if let v = self._encryptionKeyPair {
try visitor.visitSingularMessageField(value: v, fieldNumber: 3)
}
} }()
if !self.members.isEmpty {
try visitor.visitRepeatedBytesField(value: self.members, fieldNumber: 4)
}
if !self.admins.isEmpty {
try visitor.visitRepeatedBytesField(value: self.admins, fieldNumber: 5)
}
if let v = self._expirationTimer {
try { if let v = self._expirationTimer {
try visitor.visitSingularUInt32Field(value: v, fieldNumber: 6)
}
} }()
try unknownFields.traverse(visitor: &visitor)
}
@ -2464,6 +2641,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 {
@ -2482,24 +2662,40 @@ 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
}
}
}
func traverse<V: SwiftProtobuf.Visitor>(visitor: inout V) throws {
if let v = self._publicKey {
// The use of inline closures is to circumvent an issue where the compiler
// allocates stack space for every if/case branch local when no optimizations
// are enabled. https://github.com/apple/swift-protobuf/issues/1034 and
// https://github.com/apple/swift-protobuf/issues/1182
try { if let v = self._publicKey {
try visitor.visitSingularBytesField(value: v, fieldNumber: 1)
}
if let v = self._name {
} }()
try { if let v = self._name {
try visitor.visitSingularStringField(value: v, fieldNumber: 2)
}
if let v = self._profilePicture {
} }()
try { if let v = self._profilePicture {
try visitor.visitSingularStringField(value: v, fieldNumber: 3)
}
if let v = self._profileKey {
} }()
try { if let v = self._profileKey {
try visitor.visitSingularBytesField(value: v, fieldNumber: 4)
}
} }()
try { if let v = self._isApproved {
try visitor.visitSingularBoolField(value: v, fieldNumber: 5)
} }()
try { if let v = self._isBlocked {
try visitor.visitSingularBoolField(value: v, fieldNumber: 6)
} }()
try { if let v = self._didApproveMe {
try visitor.visitSingularBoolField(value: v, fieldNumber: 7)
} }()
try unknownFields.traverse(visitor: &visitor)
}
@ -2508,6 +2704,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
}
@ -2539,9 +2738,13 @@ extension SessionProtos_ReceiptMessage: SwiftProtobuf.Message, SwiftProtobuf._Me
}
func traverse<V: SwiftProtobuf.Visitor>(visitor: inout V) throws {
if let v = self._type {
// The use of inline closures is to circumvent an issue where the compiler
// allocates stack space for every if/case branch local when no optimizations
// are enabled. https://github.com/apple/swift-protobuf/issues/1034 and
// https://github.com/apple/swift-protobuf/issues/1182
try { if let v = self._type {
try visitor.visitSingularEnumField(value: v, fieldNumber: 1)
}
} }()
if !self.timestamp.isEmpty {
try visitor.visitRepeatedUInt64Field(value: self.timestamp, fieldNumber: 2)
}
@ -2609,42 +2812,46 @@ extension SessionProtos_AttachmentPointer: SwiftProtobuf.Message, SwiftProtobuf.
}
func traverse<V: SwiftProtobuf.Visitor>(visitor: inout V) throws {
if let v = self._id {
// The use of inline closures is to circumvent an issue where the compiler
// allocates stack space for every if/case branch local when no optimizations
// are enabled. https://github.com/apple/swift-protobuf/issues/1034 and
// https://github.com/apple/swift-protobuf/issues/1182
try { if let v = self._id {
try visitor.visitSingularFixed64Field(value: v, fieldNumber: 1)
}
if let v = self._contentType {
} }()
try { if let v = self._contentType {
try visitor.visitSingularStringField(value: v, fieldNumber: 2)
}
if let v = self._key {
} }()
try { if let v = self._key {
try visitor.visitSingularBytesField(value: v, fieldNumber: 3)
}
if let v = self._size {
} }()
try { if let v = self._size {
try visitor.visitSingularUInt32Field(value: v, fieldNumber: 4)
}
if let v = self._thumbnail {
} }()
try { if let v = self._thumbnail {
try visitor.visitSingularBytesField(value: v, fieldNumber: 5)
}
if let v = self._digest {
} }()
try { if let v = self._digest {
try visitor.visitSingularBytesField(value: v, fieldNumber: 6)
}
if let v = self._fileName {
} }()
try { if let v = self._fileName {
try visitor.visitSingularStringField(value: v, fieldNumber: 7)
}
if let v = self._flags {
} }()
try { if let v = self._flags {
try visitor.visitSingularUInt32Field(value: v, fieldNumber: 8)
}
if let v = self._width {
} }()
try { if let v = self._width {
try visitor.visitSingularUInt32Field(value: v, fieldNumber: 9)
}
if let v = self._height {
} }()
try { if let v = self._height {
try visitor.visitSingularUInt32Field(value: v, fieldNumber: 10)
}
if let v = self._caption {
} }()
try { if let v = self._caption {
try visitor.visitSingularStringField(value: v, fieldNumber: 11)
}
if let v = self._url {
} }()
try { if let v = self._url {
try visitor.visitSingularStringField(value: v, fieldNumber: 101)
}
} }()
try unknownFields.traverse(visitor: &visitor)
}
@ -2741,21 +2948,25 @@ extension SessionProtos_GroupContext: SwiftProtobuf.Message, SwiftProtobuf._Mess
func traverse<V: SwiftProtobuf.Visitor>(visitor: inout V) throws {
try withExtendedLifetime(_storage) { (_storage: _StorageClass) in
if let v = _storage._id {
// The use of inline closures is to circumvent an issue where the compiler
// allocates stack space for every if/case branch local when no optimizations
// are enabled. https://github.com/apple/swift-protobuf/issues/1034 and
// https://github.com/apple/swift-protobuf/issues/1182
try { if let v = _storage._id {
try visitor.visitSingularBytesField(value: v, fieldNumber: 1)
}
if let v = _storage._type {
} }()
try { if let v = _storage._type {
try visitor.visitSingularEnumField(value: v, fieldNumber: 2)
}
if let v = _storage._name {
} }()
try { if let v = _storage._name {
try visitor.visitSingularStringField(value: v, fieldNumber: 3)
}
} }()
if !_storage._members.isEmpty {
try visitor.visitRepeatedStringField(value: _storage._members, fieldNumber: 4)
}
if let v = _storage._avatar {
try { if let v = _storage._avatar {
try visitor.visitSingularMessageField(value: v, fieldNumber: 5)
}
} }()
if !_storage._admins.isEmpty {
try visitor.visitRepeatedStringField(value: _storage._admins, fieldNumber: 6)
}

View File

@ -249,18 +249,22 @@ extension WebSocketProtos_WebSocketRequestMessage: SwiftProtobuf.Message, SwiftP
}
func traverse<V: SwiftProtobuf.Visitor>(visitor: inout V) throws {
if let v = self._verb {
// The use of inline closures is to circumvent an issue where the compiler
// allocates stack space for every if/case branch local when no optimizations
// are enabled. https://github.com/apple/swift-protobuf/issues/1034 and
// https://github.com/apple/swift-protobuf/issues/1182
try { if let v = self._verb {
try visitor.visitSingularStringField(value: v, fieldNumber: 1)
}
if let v = self._path {
} }()
try { if let v = self._path {
try visitor.visitSingularStringField(value: v, fieldNumber: 2)
}
if let v = self._body {
} }()
try { if let v = self._body {
try visitor.visitSingularBytesField(value: v, fieldNumber: 3)
}
if let v = self._requestID {
} }()
try { if let v = self._requestID {
try visitor.visitSingularUInt64Field(value: v, fieldNumber: 4)
}
} }()
if !self.headers.isEmpty {
try visitor.visitRepeatedStringField(value: self.headers, fieldNumber: 5)
}
@ -305,18 +309,22 @@ extension WebSocketProtos_WebSocketResponseMessage: SwiftProtobuf.Message, Swift
}
func traverse<V: SwiftProtobuf.Visitor>(visitor: inout V) throws {
if let v = self._requestID {
// The use of inline closures is to circumvent an issue where the compiler
// allocates stack space for every if/case branch local when no optimizations
// are enabled. https://github.com/apple/swift-protobuf/issues/1034 and
// https://github.com/apple/swift-protobuf/issues/1182
try { if let v = self._requestID {
try visitor.visitSingularUInt64Field(value: v, fieldNumber: 1)
}
if let v = self._status {
} }()
try { if let v = self._status {
try visitor.visitSingularUInt32Field(value: v, fieldNumber: 2)
}
if let v = self._message {
} }()
try { if let v = self._message {
try visitor.visitSingularStringField(value: v, fieldNumber: 3)
}
if let v = self._body {
} }()
try { if let v = self._body {
try visitor.visitSingularBytesField(value: v, fieldNumber: 4)
}
} }()
if !self.headers.isEmpty {
try visitor.visitRepeatedStringField(value: self.headers, fieldNumber: 5)
}
@ -357,15 +365,19 @@ extension WebSocketProtos_WebSocketMessage: SwiftProtobuf.Message, SwiftProtobuf
}
func traverse<V: SwiftProtobuf.Visitor>(visitor: inout V) throws {
if let v = self._type {
// The use of inline closures is to circumvent an issue where the compiler
// allocates stack space for every if/case branch local when no optimizations
// are enabled. https://github.com/apple/swift-protobuf/issues/1034 and
// https://github.com/apple/swift-protobuf/issues/1182
try { if let v = self._type {
try visitor.visitSingularEnumField(value: v, fieldNumber: 1)
}
if let v = self._request {
} }()
try { if let v = self._request {
try visitor.visitSingularMessageField(value: v, fieldNumber: 2)
}
if let v = self._response {
} }()
try { if let v = self._response {
try visitor.visitSingularMessageField(value: v, fieldNumber: 3)
}
} }()
try unknownFields.traverse(visitor: &visitor)
}

View File

@ -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 ReceiptMessage receiptMessage = 5;
@ -48,6 +53,7 @@ message Content {
optional ConfigurationMessage configurationMessage = 7;
optional DataExtractionNotification dataExtractionNotification = 8;
optional UnsendRequest unsendRequest = 9;
optional MessageRequestResponse messageRequestResponse = 10;
}
message KeyPair {
@ -179,6 +185,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;

View File

@ -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) {

View File

@ -16,9 +16,11 @@ extension MessageReceiver {
case let message as ExpirationTimerUpdate: handleExpirationTimerUpdate(message, using: transaction)
case let message as ConfigurationMessage: handleConfigurationMessage(message, using: transaction)
case let message as UnsendRequest: handleUnsendRequest(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")
@ -188,15 +190,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!
@ -204,11 +214,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.getWithContactSessionID(sessionID, transaction: 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 {
@ -339,6 +392,22 @@ extension MessageReceiver {
// Mark previous messages as read if there is a sync message
OWSReadReceiptManager.shared().markAsReadLocally(beforeSortId: tsOutgoingMessage.sortId, thread: thread)
}
// 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 }
@ -427,10 +496,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)
@ -439,18 +524,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
@ -694,4 +785,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.getWithContactSessionID(senderId, transaction: 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
)
}
}

View File

@ -125,6 +125,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 }
return nil
}()

View File

@ -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()

View File

@ -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

View File

@ -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;

View File

@ -47,6 +47,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;

View File

@ -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 {

View File

@ -39,57 +39,94 @@ 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() }
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()
}
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)"
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()
}
default: return self.completeSilenty()
}
default: return self.completeSilenty()
}
if (senderPublicKey == userPublicKey) {
// Ignore PNs for messages sent by the current user
// after handling the message. Otherwise the closed
@ -98,21 +135,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)
}

View File

@ -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

View File

@ -44,4 +44,8 @@ public final class Colors : NSObject {
@objc public static var pathsBuilding: UIColor { UIColor(named: "session_paths_building")! }
@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")! }
}

View File

@ -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
}
}

View File

@ -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
}
}

View File

@ -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
}
}

View File

@ -0,0 +1,38 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0x00",
"green" : "0x00",
"red" : "0x00"
}
},
"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
}
}

View File

@ -5,9 +5,9 @@
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "96",
"green" : "96",
"red" : "96"
"blue" : "0x60",
"green" : "0x60",
"red" : "0x60"
}
},
"idiom" : "universal"
@ -23,9 +23,9 @@
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "179",
"green" : "179",
"red" : "179"
"blue" : "0xB3",
"green" : "0xB3",
"red" : "0xB3"
}
},
"idiom" : "universal"

View File

@ -96,6 +96,25 @@ public extension UIView {
return constraint
}
@discardableResult
func set(_ dimension: Dimension, to otherDimension: Dimension, of view: UIView, withOffset offset: CGFloat = 0) -> NSLayoutConstraint {
translatesAutoresizingMaskIntoConstraints = false
let otherAnchor: NSLayoutAnchor<NSLayoutDimension> = {
switch otherDimension {
case .width: return view.widthAnchor
case .height: return view.heightAnchor
}
}()
let constraint: NSLayoutConstraint = {
switch dimension {
case .width: return widthAnchor.constraint(equalTo: otherAnchor, constant: offset)
case .height: return heightAnchor.constraint(equalTo: otherAnchor, constant: offset)
}
}()
constraint.isActive = true
return constraint
}
@discardableResult
func set(_ dimension: Dimension, greaterThanOrEqualTo size: CGFloat) -> NSLayoutConstraint {
translatesAutoresizingMaskIntoConstraints = false

View File

@ -7,6 +7,7 @@ public enum SNUserDefaults {
case hasViewedSeed
case hasSeenLinkPreviewSuggestion
case isUsingFullAPNs
case hasHiddenMessageRequests
}
public enum Date : Swift.String {

View File

@ -0,0 +1,56 @@
@objc(SNMessageRequestsMigration)
public class MessageRequestsMigration : OWSDatabaseMigration {
@objc
class func migrationId() -> String {
return "002"
}
override public func runUp(completion: @escaping OWSDatabaseMigrationCompletion) {
self.doMigrationAsync(completion: completion)
}
private func doMigrationAsync(completion: @escaping OWSDatabaseMigrationCompletion) {
var contacts: Set<Contact> = Set()
var threads: [TSThread] = []
TSThread.enumerateCollectionObjects { object, _ in
guard let thread: TSThread = object as? TSThread else { return }
if let contactThread: TSContactThread = thread as? TSContactThread {
let sessionId: String = contactThread.contactSessionID()
if let contact: Contact = Storage.shared.getContact(with: sessionId) {
contact.isApproved = true
contact.didApproveMe = true
contacts.insert(contact)
}
}
else if let groupThread: TSGroupThread = thread as? TSGroupThread, groupThread.isClosedGroup {
let groupAdmins: [String] = groupThread.groupModel.groupAdminIds
groupAdmins.forEach { sessionId in
if let contact: Contact = Storage.shared.getContact(with: sessionId) {
contact.isApproved = true
contact.didApproveMe = true
contacts.insert(contact)
}
}
}
threads.append(thread)
}
Storage.write(with: { transaction in
contacts.forEach { contact in
Storage.shared.setContact(contact, using: transaction)
}
threads.forEach { thread in
thread.save(with: transaction)
}
self.save(with: transaction) // Intentionally capture self
}, completion: {
completion()
})
}
}

View File

@ -26,6 +26,7 @@ NS_ASSUME_NONNULL_BEGIN
- (NSArray<OWSDatabaseMigration *> *)allMigrations
{
return @[
[SNMessageRequestsMigration new],
[SNContactsMigration new]
];
}

View File

@ -209,8 +209,8 @@ public class MediaMessageView: UIView, OWSAudioPlayerDelegate {
let button: UIButton = UIButton()
button.translatesAutoresizingMaskIntoConstraints = false
button.clipsToBounds = true
button.setBackgroundImage(UIColor.white.toImage(), for: .normal)
button.setBackgroundImage(UIColor.white.darken(by: 0.2).toImage(), for: .highlighted)
button.setBackgroundImage(UIColor.white.toImage(isDarkMode: isDarkMode), for: .normal)
button.setBackgroundImage(UIColor.white.darken(by: 0.2).toImage(isDarkMode: isDarkMode), for: .highlighted)
button.addTarget(self, action: #selector(audioPlayPauseButtonPressed), for: .touchUpInside)
button.isHidden = true

View File

@ -1,52 +0,0 @@
extension ConfigurationMessage {
public static func getCurrent() -> 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
Storage.read { 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
}
}
var truncatedContacts = storage.getAllContacts()
if truncatedContacts.count > 200 { truncatedContacts = Set(Array(truncatedContacts)[0..<200]) }
truncatedContacts.forEach { contact in
let publicKey = contact.sessionID
let threadID = TSContactThread.threadID(fromContactSessionID: publicKey)
guard let thread = TSContactThread.fetch(uniqueId: threadID, transaction: transaction), thread.shouldBeVisible
&& !SSKEnvironment.shared.blockingManager.isRecipientIdBlocked(publicKey) else { return }
let profilePictureURL = contact.profilePictureURL
let profileKey = contact.profileEncryptionKey?.keyData
let contact = ConfigurationMessage.Contact(publicKey: publicKey, displayName: contact.name ?? publicKey,
profilePictureURL: profilePictureURL, profileKey: profileKey)
contacts.insert(contact)
contactCount += 1
}
}
return ConfigurationMessage(displayName: displayName, profilePictureURL: profilePictureURL, profileKey: profileKey,
closedGroups: closedGroups, openGroups: openGroups, contacts: contacts)
}
}

View File

@ -75,8 +75,12 @@ NS_ASSUME_NONNULL_BEGIN
// FIXME: Confusingly, `allGroups` includes contact threads as well
for (NSString *groupID in allGroups) {
TSThread *thread = [TSThread fetchObjectWithUniqueID:groupID transaction:transaction];
if (thread.isMuted) { continue; }
// Don't increase the count for muted threads or message requests
if (thread.isMuted || thread.isMessageRequest) { continue; }
BOOL isGroupThread = thread.isGroupThread;
[unreadMessages enumerateKeysAndObjectsInGroup:groupID
usingBlock:^(NSString *collection, NSString *key, id object, NSUInteger index, BOOL *stop) {
if (![object conformsToProtocol:@protocol(OWSReadTracking)]) {

View File

@ -24,12 +24,24 @@ public extension UIColor {
// MARK: - Functions
func toImage() -> UIImage {
func toImage(isDarkMode: Bool) -> UIImage {
let bounds: CGRect = CGRect(x: 0, y: 0, width: 1, height: 1)
let renderer: UIGraphicsImageRenderer = UIGraphicsImageRenderer(bounds: bounds)
return renderer.image { rendererContext in
rendererContext.cgContext.setFillColor(self.cgColor)
if #available(iOS 13.0, *) {
rendererContext.cgContext
.setFillColor(
self.resolvedColor(
// Note: This is needed for '.cgColor' to support dark mode
with: UITraitCollection(userInterfaceStyle: isDarkMode ? .dark : .light)
).cgColor
)
}
else {
rendererContext.cgContext.setFillColor(self.cgColor)
}
rendererContext.cgContext.fill(bounds)
}
}

View File

@ -47,11 +47,15 @@ NS_ASSUME_NONNULL_BEGIN
OWSLogInfo(@"Checking migrations. currentVersion: %@, lastRanVersion: %@", currentVersion, previousVersion);
if (!previousVersion) {
OWSLogInfo(@"No previous version found. Probably first launch since install - nothing to migrate.");
OWSDatabaseMigrationRunner *runner = [[OWSDatabaseMigrationRunner alloc] init];
// Note: We need to run the migrations here anyway to ensure that they don't run on subsequent launches
// and result in unexpected data changes (eg. 'MessageRequestsMigration' auto-approves all threads
// if this happens on the 2nd launch then any threads created during the 1st launch which haven't
// been approved would get auto-approved, allowing the user to use contacts which haven't approved
// comms to appear as options when creating closed groups)
OWSLogInfo(@"No previous version found. Probably first launch since install - running migrations so they don't run on second launch.");
dispatch_async(dispatch_get_main_queue(), ^{
completion();
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[[[OWSDatabaseMigrationRunner alloc] init] runAllOutstandingWithCompletion:completion];
});
return;
}