diff --git a/Session.xcodeproj/project.pbxproj b/Session.xcodeproj/project.pbxproj index bb4c643ff..8929ff078 100644 --- a/Session.xcodeproj/project.pbxproj +++ b/Session.xcodeproj/project.pbxproj @@ -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 = ""; }; 4503F1BC20470A5B00CEE724 /* classic.aifc */ = {isa = PBXFileReference; lastKnownFileType = file; path = classic.aifc; sourceTree = ""; }; 4509E7991DD653700025A59F /* WebRTC.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WebRTC.framework; path = ThirdParty/WebRTC/Build/WebRTC.framework; sourceTree = ""; }; - 450DF2041E0D74AC003D14BE /* Platform.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Platform.swift; sourceTree = ""; }; 450DF2081E0DD2C6003D14BE /* UserNotificationsAdaptee.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = UserNotificationsAdaptee.swift; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; 451166BF1FD86B98000739BA /* AccountManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AccountManager.swift; sourceTree = ""; }; 451A13B01E13DED2000A50FD /* AppNotifications.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = AppNotifications.swift; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; @@ -1699,7 +1722,6 @@ C3A76A8C25DB83F90074CB90 /* PermissionMissingModal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PermissionMissingModal.swift; sourceTree = ""; }; C3A8AF752665B03900A467FE /* hi */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = hi; path = hi.lproj/Localizable.strings; sourceTree = ""; }; C3A8AF762665F97A00A467FE /* fi */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fi; path = fi.lproj/Localizable.strings; sourceTree = ""; }; - C3AAFFDE25AE96FF0089E6DD /* ConfigurationMessage+Convenience.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ConfigurationMessage+Convenience.swift"; sourceTree = ""; }; C3AAFFF125AE99710089E6DD /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; C3ADC66026426688005F1414 /* ShareVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareVC.swift; sourceTree = ""; }; C3AECBEA24EF5244005743DE /* fa */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fa; path = fa.lproj/Localizable.strings; sourceTree = ""; }; @@ -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 = ""; }; 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 = ""; }; + FD5D201027AA331F00FEA984 /* ConfigurationMessage+Convenience.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ConfigurationMessage+Convenience.swift"; sourceTree = ""; }; + FD659ABF27A7649600F12C02 /* MessageRequestsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageRequestsViewController.swift; sourceTree = ""; }; FD705A8B278CDB5600F16121 /* SAEScreenLockViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SAEScreenLockViewController.swift; sourceTree = ""; }; FD705A8D278CE29800F16121 /* String+Localization.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+Localization.swift"; sourceTree = ""; }; FD705A8F278CEBBC00F16121 /* ShareAppExtensionContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareAppExtensionContext.swift; sourceTree = ""; }; FD705A91278D051200F16121 /* ReusableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReusableView.swift; sourceTree = ""; }; FD705A93278D052B00F16121 /* UITableView+ReusableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UITableView+ReusableView.swift"; sourceTree = ""; }; FD705A97278E9F4D00F16121 /* UIColor+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIColor+Extensions.swift"; sourceTree = ""; }; + FD859EFF27C4691300510D0C /* MockDataGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockDataGenerator.swift; sourceTree = ""; }; + FD88BAD827A7439C00BBC442 /* MessageRequestsCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageRequestsCell.swift; sourceTree = ""; }; + FD88BADA27A750F200BBC442 /* MessageRequestsMigration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageRequestsMigration.swift; sourceTree = ""; }; 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 = ""; }; 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 = ""; }; 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 = ""; }; @@ -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 = ""; @@ -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 = ""; @@ -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 = ""; @@ -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 = ""; @@ -3599,6 +3632,22 @@ path = Session; sourceTree = ""; }; + FD659ABE27A7648200F12C02 /* Message Requests */ = { + isa = PBXGroup; + children = ( + FD659ABF27A7649600F12C02 /* MessageRequestsViewController.swift */, + ); + path = "Message Requests"; + sourceTree = ""; + }; + FD88BAD727A7438E00BBC442 /* Views */ = { + isa = PBXGroup; + children = ( + FD88BAD827A7439C00BBC442 /* MessageRequestsCell.swift */, + ); + path = Views; + sourceTree = ""; + }; /* 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 */ diff --git a/Session/Conversations/ConversationVC+Interaction.swift b/Session/Conversations/ConversationVC+Interaction.swift index 77ed515bb..589a85f89 100644 --- a/Session/Conversations/ConversationVC+Interaction.swift +++ b/Session/Conversations/ConversationVC+Interaction.swift @@ -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) + } +} diff --git a/Session/Conversations/ConversationVC.swift b/Session/Conversations/ConversationVC.swift index f46d31ea9..0dc29690d 100644 --- a/Session/Conversations/ConversationVC.swift +++ b/Session/Conversations/ConversationVC.swift @@ -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() diff --git a/Session/Conversations/Input View/ExpandingAttachmentsButton.swift b/Session/Conversations/Input View/ExpandingAttachmentsButton.swift index 8652d57c6..18c155f5d 100644 --- a/Session/Conversations/Input View/ExpandingAttachmentsButton.swift +++ b/Session/Conversations/Input View/ExpandingAttachmentsButton.swift @@ -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() diff --git a/Session/Conversations/Input View/InputView.swift b/Session/Conversations/Input View/InputView.swift index 9b78dc9e6..8f9c0ff26 100644 --- a/Session/Conversations/Input View/InputView.swift +++ b/Session/Conversations/Input View/InputView.swift @@ -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 diff --git a/Session/Conversations/Input View/InputViewButton.swift b/Session/Conversations/Input View/InputViewButton.swift index 082442faa..43166a5f0 100644 --- a/Session/Conversations/Input View/InputViewButton.swift +++ b/Session/Conversations/Input View/InputViewButton.swift @@ -98,6 +98,8 @@ final class InputViewButton : UIView { // We want to detect both taps and long presses override func touchesBegan(_ touches: Set, 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, with event: UIEvent?) { + guard isUserInteractionEnabled else { return } + if isLongPress { delegate?.handleInputViewButtonLongPressMoved(self, with: touches.first!) } } override func touchesEnded(_ touches: Set, with event: UIEvent?) { + guard isUserInteractionEnabled else { return } + collapse() if !isLongPress { delegate?.handleInputViewButtonTapped(self) diff --git a/Session/Conversations/Settings/OWSConversationSettingsViewController.m b/Session/Conversations/Settings/OWSConversationSettingsViewController.m index 345a09bc5..5585b155d 100644 --- a/Session/Conversations/Settings/OWSConversationSettingsViewController.m +++ b/Session/Conversations/Settings/OWSConversationSettingsViewController.m @@ -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; diff --git a/Session/Conversations/Views & Modals/MessagesTableView.swift b/Session/Conversations/Views & Modals/MessagesTableView.swift index d5648f2bb..033664ca8 100644 --- a/Session/Conversations/Views & Modals/MessagesTableView.swift +++ b/Session/Conversations/Views & Modals/MessagesTableView.swift @@ -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() diff --git a/Session/Home/HomeVC.swift b/Session/Home/HomeVC.swift index 6ea8fc636..f14ada4e0 100644 --- a/Session/Home/HomeVC.swift +++ b/Session/Home/HomeVC.swift @@ -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(§ionChanges, rowChanges: &rowChanges, for: notifications, with: threads) - guard sectionChanges.count > 0 || rowChanges.count > 0 else { return } + + // Separate out the changes for new message requests and the inbox (so we can avoid updating for + // new messages within an existing message request) + let messageRequestChanges = rowChanges + .compactMap { $0 as? YapDatabaseViewRowChange } + .filter { $0.originalGroup == TSMessageRequestGroup || $0.finalGroup == TSMessageRequestGroup } + let inboxRowChanges = rowChanges + .compactMap { $0 as? YapDatabaseViewRowChange } + .filter { $0.originalGroup == TSInboxGroup || $0.finalGroup == TSInboxGroup } + + guard sectionChanges.count > 0 || inboxRowChanges.count > 0 || messageRequestChanges.count > 0 else { return } + tableView.beginUpdates() - rowChanges.forEach { rowChange in - let rowChange = rowChange as! YapDatabaseViewRowChange + + // If we need to unhide the message request row and then re-insert it + if !messageRequestChanges.isEmpty { + if 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 } diff --git a/Session/Home/Message Requests/MessageRequestsViewController.swift b/Session/Home/Message Requests/MessageRequestsViewController.swift new file mode 100644 index 000000000..dfb4e1b73 --- /dev/null +++ b/Session/Home/Message Requests/MessageRequestsViewController.swift @@ -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(§ionChanges, rowChanges: &rowChanges, for: notifications, with: threads)` + // line. This causes `tableView.endUpdates()` to crash with an `NSInternalInconsistencyException`. + let threads = threads! + + // Create a stable state for the connection and jump to the latest commit + let notifications = dbConnection.beginLongLivedReadTransaction() + + guard !notifications.isEmpty else { return } + + let ext = dbConnection.ext(TSThreadDatabaseViewExtensionName) as! YapDatabaseViewConnection + let hasChanges = ext.hasChanges(forGroup: TSMessageRequestGroup, in: notifications) + + guard hasChanges else { return } + + if let firstChangeSet = notifications[0].userInfo { + let firstSnapshot = firstChangeSet[YapDatabaseSnapshotKey] as! UInt64 + + if threads.snapshotOfLastUpdate != firstSnapshot - 1 { + return reload() // The code below will crash if we try to process multiple commits at once + } + } + + var sectionChanges = NSArray() + var rowChanges = NSArray() + ext.getSectionChanges(§ionChanges, rowChanges: &rowChanges, for: notifications, with: threads) + + guard sectionChanges.count > 0 || rowChanges.count > 0 else { return } + + tableView.beginUpdates() + + rowChanges.forEach { rowChange in + let rowChange = rowChange as! YapDatabaseViewRowChange + let key = rowChange.collectionKey.key + threadViewModelCache[key] = nil + switch rowChange.type { + case .delete: tableView.deleteRows(at: [ rowChange.indexPath! ], with: UITableView.RowAnimation.automatic) + case .insert: tableView.insertRows(at: [ rowChange.newIndexPath! ], with: UITableView.RowAnimation.automatic) + case .update: tableView.reloadRows(at: [ rowChange.indexPath! ], with: UITableView.RowAnimation.automatic) + default: break + } + } + tableView.endUpdates() + + // HACK: Moves can have conflicts with the other 3 types of change. + // Just batch perform all the moves separately to prevent crashing. + // Since all the changes are from the original state to the final state, + // it will still be correct if we pick the moves out. + + tableView.beginUpdates() + + rowChanges.forEach { rowChange in + let rowChange = rowChange as! YapDatabaseViewRowChange + let key = rowChange.collectionKey.key + threadViewModelCache[key] = nil + + switch rowChange.type { + case .move: tableView.moveRow(at: rowChange.indexPath!, to: rowChange.newIndexPath!) + default: break + } + } + + tableView.endUpdates() + clearAllButton.isHidden = (messageRequestCount == 0) + emptyStateLabel.isHidden = (messageRequestCount != 0) + } + + @objc private func handleProfileDidChangeNotification(_ notification: Notification) { + tableView.reloadData() // TODO: Just reload the affected cell + } + + @objc private func handleBlockedContactsUpdatedNotification(_ notification: Notification) { + tableView.reloadData() // TODO: Just reload the affected cell + } + + @objc override internal func handleAppModeChangedNotification(_ notification: Notification) { + super.handleAppModeChangedNotification(notification) + + let gradient = Gradients.homeVCFade + fadeView.setGradient(gradient) // Re-do the gradient + tableView.reloadData() + } + + // MARK: - UITableViewDelegate + + func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + tableView.deselectRow(at: indexPath, animated: true) + + guard let thread = self.thread(at: indexPath.row) else { return } + + let conversationVC = ConversationVC(thread: thread) + self.navigationController?.pushViewController(conversationVC, animated: true) + } + + func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool { + return true + } + + func tableView(_ tableView: UITableView, editActionsForRowAt indexPath: IndexPath) -> [UITableViewRowAction]? { + guard let thread = self.thread(at: indexPath.row) else { return [] } + + let delete = UITableViewRowAction(style: .destructive, title: NSLocalizedString("TXT_DELETE_TITLE", comment: "")) { [weak self] _, _ in + self?.delete(thread) + } + delete.backgroundColor = Colors.destructive + + return [ delete ] + } + + // MARK: - Interaction + + private func updateContactAndThread(thread: TSThread, with transaction: YapDatabaseReadWriteTransaction, onComplete: ((Bool) -> ())? = nil) { + guard let contactThread: TSContactThread = thread as? TSContactThread else { + onComplete?(false) + return + } + + var needsSync: Bool = false + + // Update the contact + let sessionId: String = contactThread.contactSessionID() + + if let contact: Contact = Storage.shared.getContact(with: sessionId), (contact.isApproved || !contact.isBlocked) { + contact.isApproved = false + contact.isBlocked = true + + Storage.shared.setContact(contact, using: transaction) + needsSync = true + } + + // Delete all thread content + thread.removeAllThreadInteractions(with: transaction) + thread.remove(with: transaction) + + onComplete?(needsSync) + } + + @objc private func clearAllTapped() { + let threadCount: Int = Int(messageRequestCount) + let threads: [TSThread] = (0.. 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 + } + } +} diff --git a/Session/Home/Views/MessageRequestsCell.swift b/Session/Home/Views/MessageRequestsCell.swift new file mode 100644 index 000000000..5229a9540 --- /dev/null +++ b/Session/Home/Views/MessageRequestsCell.swift @@ -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) + } +} diff --git a/Session/Meta/AppDelegate.swift b/Session/Meta/AppDelegate.swift index 56e4100b2..30525183d 100644 --- a/Session/Meta/AppDelegate.swift +++ b/Session/Meta/AppDelegate.swift @@ -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 { - guard Storage.shared.getUser()?.name != nil, - let configurationMessage = ConfigurationMessage.getCurrent() else { return Promise.value(()) } + func forceSyncConfigurationNowIfNeeded(with transaction: YapDatabaseReadWriteTransaction? = nil) -> Promise { + 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.pending() Storage.writeSync { transaction in diff --git a/Session/Meta/Images.xcassets/Session/message_requests.imageset/Contents.json b/Session/Meta/Images.xcassets/Session/message_requests.imageset/Contents.json new file mode 100644 index 000000000..8e181d63c --- /dev/null +++ b/Session/Meta/Images.xcassets/Session/message_requests.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "message_requests.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Session/Meta/Images.xcassets/Session/message_requests.imageset/message_requests.pdf b/Session/Meta/Images.xcassets/Session/message_requests.imageset/message_requests.pdf new file mode 100644 index 000000000..0e8bd99cb --- /dev/null +++ b/Session/Meta/Images.xcassets/Session/message_requests.imageset/message_requests.pdf @@ -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 \ No newline at end of file diff --git a/Session/Meta/Translations/de.lproj/Localizable.strings b/Session/Meta/Translations/de.lproj/Localizable.strings index a911dfe5c..8f96b9859 100644 --- a/Session/Meta/Translations/de.lproj/Localizable.strings +++ b/Session/Meta/Translations/de.lproj/Localizable.strings @@ -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"; diff --git a/Session/Meta/Translations/en.lproj/Localizable.strings b/Session/Meta/Translations/en.lproj/Localizable.strings index 5b12ac396..b3fe25911 100644 --- a/Session/Meta/Translations/en.lproj/Localizable.strings +++ b/Session/Meta/Translations/en.lproj/Localizable.strings @@ -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"; diff --git a/Session/Meta/Translations/es.lproj/Localizable.strings b/Session/Meta/Translations/es.lproj/Localizable.strings index b6ad30d65..0e9317aec 100644 --- a/Session/Meta/Translations/es.lproj/Localizable.strings +++ b/Session/Meta/Translations/es.lproj/Localizable.strings @@ -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"; diff --git a/Session/Meta/Translations/fa.lproj/Localizable.strings b/Session/Meta/Translations/fa.lproj/Localizable.strings index c5ce00e23..8574b6a04 100644 --- a/Session/Meta/Translations/fa.lproj/Localizable.strings +++ b/Session/Meta/Translations/fa.lproj/Localizable.strings @@ -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"; diff --git a/Session/Meta/Translations/fi.lproj/Localizable.strings b/Session/Meta/Translations/fi.lproj/Localizable.strings index 6dfc22e77..1d7471f51 100644 --- a/Session/Meta/Translations/fi.lproj/Localizable.strings +++ b/Session/Meta/Translations/fi.lproj/Localizable.strings @@ -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"; diff --git a/Session/Meta/Translations/fr.lproj/Localizable.strings b/Session/Meta/Translations/fr.lproj/Localizable.strings index d7a0fe364..a6b214ceb 100644 --- a/Session/Meta/Translations/fr.lproj/Localizable.strings +++ b/Session/Meta/Translations/fr.lproj/Localizable.strings @@ -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"; diff --git a/Session/Meta/Translations/hi.lproj/Localizable.strings b/Session/Meta/Translations/hi.lproj/Localizable.strings index 9a1902018..f8c56eb4f 100644 --- a/Session/Meta/Translations/hi.lproj/Localizable.strings +++ b/Session/Meta/Translations/hi.lproj/Localizable.strings @@ -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"; diff --git a/Session/Meta/Translations/hr.lproj/Localizable.strings b/Session/Meta/Translations/hr.lproj/Localizable.strings index a9f374297..800c669ca 100644 --- a/Session/Meta/Translations/hr.lproj/Localizable.strings +++ b/Session/Meta/Translations/hr.lproj/Localizable.strings @@ -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"; diff --git a/Session/Meta/Translations/id-ID.lproj/Localizable.strings b/Session/Meta/Translations/id-ID.lproj/Localizable.strings index 6a605d07e..6974734bd 100644 --- a/Session/Meta/Translations/id-ID.lproj/Localizable.strings +++ b/Session/Meta/Translations/id-ID.lproj/Localizable.strings @@ -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"; diff --git a/Session/Meta/Translations/it.lproj/Localizable.strings b/Session/Meta/Translations/it.lproj/Localizable.strings index 843eb35bf..540cdb2b0 100644 --- a/Session/Meta/Translations/it.lproj/Localizable.strings +++ b/Session/Meta/Translations/it.lproj/Localizable.strings @@ -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"; diff --git a/Session/Meta/Translations/ja.lproj/Localizable.strings b/Session/Meta/Translations/ja.lproj/Localizable.strings index 40270d169..e1ca6cc61 100644 --- a/Session/Meta/Translations/ja.lproj/Localizable.strings +++ b/Session/Meta/Translations/ja.lproj/Localizable.strings @@ -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"; diff --git a/Session/Meta/Translations/nl.lproj/Localizable.strings b/Session/Meta/Translations/nl.lproj/Localizable.strings index 91128b670..081045a66 100644 --- a/Session/Meta/Translations/nl.lproj/Localizable.strings +++ b/Session/Meta/Translations/nl.lproj/Localizable.strings @@ -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"; diff --git a/Session/Meta/Translations/pl.lproj/Localizable.strings b/Session/Meta/Translations/pl.lproj/Localizable.strings index ce7c1ab5f..7602b2a50 100644 --- a/Session/Meta/Translations/pl.lproj/Localizable.strings +++ b/Session/Meta/Translations/pl.lproj/Localizable.strings @@ -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"; diff --git a/Session/Meta/Translations/pt_BR.lproj/Localizable.strings b/Session/Meta/Translations/pt_BR.lproj/Localizable.strings index fb2f3345a..62cfa2db2 100644 --- a/Session/Meta/Translations/pt_BR.lproj/Localizable.strings +++ b/Session/Meta/Translations/pt_BR.lproj/Localizable.strings @@ -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"; diff --git a/Session/Meta/Translations/ru.lproj/Localizable.strings b/Session/Meta/Translations/ru.lproj/Localizable.strings index ab7cb8cdf..e47932db1 100644 --- a/Session/Meta/Translations/ru.lproj/Localizable.strings +++ b/Session/Meta/Translations/ru.lproj/Localizable.strings @@ -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"; diff --git a/Session/Meta/Translations/si.lproj/Localizable.strings b/Session/Meta/Translations/si.lproj/Localizable.strings index 149e7892f..7f4297832 100644 --- a/Session/Meta/Translations/si.lproj/Localizable.strings +++ b/Session/Meta/Translations/si.lproj/Localizable.strings @@ -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"; diff --git a/Session/Meta/Translations/sk.lproj/Localizable.strings b/Session/Meta/Translations/sk.lproj/Localizable.strings index 0d1a0eb95..8c46a741b 100644 --- a/Session/Meta/Translations/sk.lproj/Localizable.strings +++ b/Session/Meta/Translations/sk.lproj/Localizable.strings @@ -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"; diff --git a/Session/Meta/Translations/sv.lproj/Localizable.strings b/Session/Meta/Translations/sv.lproj/Localizable.strings index 1b865f3ff..9f4917279 100644 --- a/Session/Meta/Translations/sv.lproj/Localizable.strings +++ b/Session/Meta/Translations/sv.lproj/Localizable.strings @@ -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"; diff --git a/Session/Meta/Translations/th.lproj/Localizable.strings b/Session/Meta/Translations/th.lproj/Localizable.strings index badb957e3..3ee9c078d 100644 --- a/Session/Meta/Translations/th.lproj/Localizable.strings +++ b/Session/Meta/Translations/th.lproj/Localizable.strings @@ -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"; diff --git a/Session/Meta/Translations/vi-VN.lproj/Localizable.strings b/Session/Meta/Translations/vi-VN.lproj/Localizable.strings index eba66c9ab..5a0131436 100644 --- a/Session/Meta/Translations/vi-VN.lproj/Localizable.strings +++ b/Session/Meta/Translations/vi-VN.lproj/Localizable.strings @@ -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"; diff --git a/Session/Meta/Translations/zh-Hant.lproj/Localizable.strings b/Session/Meta/Translations/zh-Hant.lproj/Localizable.strings index 54672311b..4c9fc2786 100644 --- a/Session/Meta/Translations/zh-Hant.lproj/Localizable.strings +++ b/Session/Meta/Translations/zh-Hant.lproj/Localizable.strings @@ -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"; diff --git a/Session/Meta/Translations/zh_CN.lproj/Localizable.strings b/Session/Meta/Translations/zh_CN.lproj/Localizable.strings index 39db28334..0d09f4d0e 100644 --- a/Session/Meta/Translations/zh_CN.lproj/Localizable.strings +++ b/Session/Meta/Translations/zh_CN.lproj/Localizable.strings @@ -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"; diff --git a/Session/Notifications/AppNotifications.swift b/Session/Notifications/AppNotifications.swift index 52eae7ed1..65223c62d 100644 --- a/Session/Notifications/AppNotifications.swift +++ b/Session/Notifications/AppNotifications.swift @@ -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 @@ -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 + ) } } diff --git a/Session/Notifications/PushRegistrationManager.swift b/Session/Notifications/PushRegistrationManager.swift index fc8b2f600..595e95c12 100644 --- a/Session/Notifications/PushRegistrationManager.swift +++ b/Session/Notifications/PushRegistrationManager.swift @@ -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: "") diff --git a/Session/Settings/SettingsVC.swift b/Session/Settings/SettingsVC.swift index 79a652762..2fca22c93 100644 --- a/Session/Settings/SettingsVC.swift +++ b/Session/Settings/SettingsVC.swift @@ -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 diff --git a/Session/Shared/ConversationCell.swift b/Session/Shared/ConversationCell.swift index 2f6a47f2f..3d56f4ecb 100644 --- a/Session/Shared/ConversationCell.swift +++ b/Session/Shared/ConversationCell.swift @@ -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 diff --git a/Session/Sheets & Modals/Modal.swift b/Session/Sheets & Modals/Modal.swift index b925d5fb4..91c6bcd14 100644 --- a/Session/Sheets & Modals/Modal.swift +++ b/Session/Sheets & Modals/Modal.swift @@ -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, 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) + } } diff --git a/Session/Utilities/ContactUtilities.swift b/Session/Utilities/ContactUtilities.swift index 61c7550b0..2ecaa2641 100644 --- a/Session/Utilities/ContactUtilities.swift +++ b/Session/Utilities/ContactUtilities.swift @@ -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) } } diff --git a/Session/Utilities/MockDataGenerator.swift b/Session/Utilities/MockDataGenerator.swift new file mode 100644 index 000000000..5a559b6df --- /dev/null +++ b/Session/Utilities/MockDataGenerator.swift @@ -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(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.. 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.. Set { var result: Set = [] 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 { + var result: Set = [] + transaction.enumerateRows(inCollection: Storage.contactCollection) { _, object, _, _ in + guard let contact = object as? Contact else { return } + result.insert(contact) } return result } diff --git a/SessionMessagingKit/Database/TSDatabaseView.h b/SessionMessagingKit/Database/TSDatabaseView.h index 689ff804c..b4484abee 100644 --- a/SessionMessagingKit/Database/TSDatabaseView.h +++ b/SessionMessagingKit/Database/TSDatabaseView.h @@ -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; diff --git a/SessionMessagingKit/Database/TSDatabaseView.m b/SessionMessagingKit/Database/TSDatabaseView.m index d899289e4..aa7ab2837 100644 --- a/SessionMessagingKit/Database/TSDatabaseView.m +++ b/SessionMessagingKit/Database/TSDatabaseView.m @@ -9,14 +9,20 @@ #import "TSIncomingMessage.h" #import "TSOutgoingMessage.h" #import "TSThread.h" +#import "OWSBlockingManager.h" #import #import #import +#import +#import +#import 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 { diff --git a/SessionMessagingKit/Messages/Control Messages/ConfigurationMessage+Convenience.swift b/SessionMessagingKit/Messages/Control Messages/ConfigurationMessage+Convenience.swift new file mode 100644 index 000000000..6575575da --- /dev/null +++ b/SessionMessagingKit/Messages/Control Messages/ConfigurationMessage+Convenience.swift @@ -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 = [] + var openGroups: Set = [] + var contacts: Set = [] + 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 + ) + } +} diff --git a/SessionMessagingKit/Messages/Control Messages/ConfigurationMessage.swift b/SessionMessagingKit/Messages/Control Messages/ConfigurationMessage.swift index 20dcc7ee6..00eea0aa0 100644 --- a/SessionMessagingKit/Messages/Control Messages/ConfigurationMessage.swift +++ b/SessionMessagingKit/Messages/Control Messages/ConfigurationMessage.swift @@ -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 { diff --git a/SessionMessagingKit/Messages/Control Messages/MessageRequestResponse.swift b/SessionMessagingKit/Messages/Control Messages/MessageRequestResponse.swift new file mode 100644 index 000000000..29b57684f --- /dev/null +++ b/SessionMessagingKit/Messages/Control Messages/MessageRequestResponse.swift @@ -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) + ) + """ + } +} diff --git a/SessionMessagingKit/Messages/Signal/TSInfoMessage.h b/SessionMessagingKit/Messages/Signal/TSInfoMessage.h index 745ffd0d6..d04ce1d6e 100644 --- a/SessionMessagingKit/Messages/Signal/TSInfoMessage.h +++ b/SessionMessagingKit/Messages/Signal/TSInfoMessage.h @@ -15,7 +15,8 @@ typedef NS_ENUM(NSInteger, TSInfoMessageType) { TSInfoMessageTypeGroupCurrentUserLeft, TSInfoMessageTypeDisappearingMessagesUpdate, TSInfoMessageTypeScreenshotNotification, - TSInfoMessageTypeMediaSavedNotification + TSInfoMessageTypeMediaSavedNotification, + TSInfoMessageTypeMessageRequestAccepted }; @property (atomic, readonly) TSInfoMessageType messageType; diff --git a/SessionMessagingKit/Messages/Signal/TSInfoMessage.m b/SessionMessagingKit/Messages/Signal/TSInfoMessage.m index 67bb6b339..5785196a6 100644 --- a/SessionMessagingKit/Messages/Signal/TSInfoMessage.m +++ b/SessionMessagingKit/Messages/Signal/TSInfoMessage.m @@ -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; } diff --git a/SessionMessagingKit/Protos/Generated/SNProto.swift b/SessionMessagingKit/Protos/Generated/SNProto.swift index 8d4f98a4e..14a51e4fe 100644 --- a/SessionMessagingKit/Protos/Generated/SNProto.swift +++ b/SessionMessagingKit/Protos/Generated/SNProto.swift @@ -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) { diff --git a/SessionMessagingKit/Protos/Generated/SessionProtos.pb.swift b/SessionMessagingKit/Protos/Generated/SessionProtos.pb.swift index ea16218a6..650d50394 100644 --- a/SessionMessagingKit/Protos/Generated/SessionProtos.pb.swift +++ b/SessionMessagingKit/Protos/Generated/SessionProtos.pb.swift @@ -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(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(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(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(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(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(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(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(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(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(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(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(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(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(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(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(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(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(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(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(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(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(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) } diff --git a/SessionMessagingKit/Protos/Generated/WebSocketResources.pb.swift b/SessionMessagingKit/Protos/Generated/WebSocketResources.pb.swift index 4c6491fa2..737e40ce6 100644 --- a/SessionMessagingKit/Protos/Generated/WebSocketResources.pb.swift +++ b/SessionMessagingKit/Protos/Generated/WebSocketResources.pb.swift @@ -249,18 +249,22 @@ extension WebSocketProtos_WebSocketRequestMessage: SwiftProtobuf.Message, SwiftP } func traverse(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(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(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) } diff --git a/SessionMessagingKit/Protos/SessionProtos.proto b/SessionMessagingKit/Protos/SessionProtos.proto index 5b1999255..7e45916cc 100644 --- a/SessionMessagingKit/Protos/SessionProtos.proto +++ b/SessionMessagingKit/Protos/SessionProtos.proto @@ -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; diff --git a/SessionMessagingKit/Sending & Receiving/Blocking/OWSBlockingManager.m b/SessionMessagingKit/Sending & Receiving/Blocking/OWSBlockingManager.m index 97e347ad1..c814fa73c 100644 --- a/SessionMessagingKit/Sending & Receiving/Blocking/OWSBlockingManager.m +++ b/SessionMessagingKit/Sending & Receiving/Blocking/OWSBlockingManager.m @@ -12,6 +12,7 @@ #import "TSGroupThread.h" #import "YapDatabaseConnection+OWS.h" #import +#import 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 *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) { diff --git a/SessionMessagingKit/Sending & Receiving/MessageReceiver+Handling.swift b/SessionMessagingKit/Sending & Receiving/MessageReceiver+Handling.swift index dd44b29b0..de65ce5cf 100644 --- a/SessionMessagingKit/Sending & Receiving/MessageReceiver+Handling.swift +++ b/SessionMessagingKit/Sending & Receiving/MessageReceiver+Handling.swift @@ -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 + ) + } } diff --git a/SessionMessagingKit/Sending & Receiving/MessageReceiver.swift b/SessionMessagingKit/Sending & Receiving/MessageReceiver.swift index 13ca16e8e..ba5a69bb8 100644 --- a/SessionMessagingKit/Sending & Receiving/MessageReceiver.swift +++ b/SessionMessagingKit/Sending & Receiving/MessageReceiver.swift @@ -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 }() diff --git a/SessionMessagingKit/Sending & Receiving/Typing Indicators/TypingIndicators.swift b/SessionMessagingKit/Sending & Receiving/Typing Indicators/TypingIndicators.swift index 2639466db..c532e6231 100644 --- a/SessionMessagingKit/Sending & Receiving/Typing Indicators/TypingIndicators.swift +++ b/SessionMessagingKit/Sending & Receiving/Typing Indicators/TypingIndicators.swift @@ -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() diff --git a/SessionMessagingKit/Storage.swift b/SessionMessagingKit/Storage.swift index 4444307d3..127cce708 100644 --- a/SessionMessagingKit/Storage.swift +++ b/SessionMessagingKit/Storage.swift @@ -18,6 +18,7 @@ public protocol SessionMessagingKitStorageProtocol { func getUserED25519KeyPair() -> Box.KeyPair? func getUser() -> Contact? func getAllContacts() -> Set + func getAllContacts(with transaction: YapDatabaseReadTransaction) -> Set // MARK: - Closed Groups diff --git a/SessionMessagingKit/Threads/TSContactThread.m b/SessionMessagingKit/Threads/TSContactThread.m index c69994802..0a2a1e9b6 100644 --- a/SessionMessagingKit/Threads/TSContactThread.m +++ b/SessionMessagingKit/Threads/TSContactThread.m @@ -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; diff --git a/SessionMessagingKit/Threads/TSThread.h b/SessionMessagingKit/Threads/TSThread.h index dcf5346c3..5809068d1 100644 --- a/SessionMessagingKit/Threads/TSThread.h +++ b/SessionMessagingKit/Threads/TSThread.h @@ -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; diff --git a/SessionMessagingKit/Threads/TSThread.m b/SessionMessagingKit/Threads/TSThread.m index 94402dc2f..91484bba7 100644 --- a/SessionMessagingKit/Threads/TSThread.m +++ b/SessionMessagingKit/Threads/TSThread.m @@ -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 { diff --git a/SessionNotificationServiceExtension/NotificationServiceExtension.swift b/SessionNotificationServiceExtension/NotificationServiceExtension.swift index 133c08736..1a227569f 100644 --- a/SessionNotificationServiceExtension/NotificationServiceExtension.swift +++ b/SessionNotificationServiceExtension/NotificationServiceExtension.swift @@ -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) } diff --git a/SessionShareExtension/ThreadPickerVC.swift b/SessionShareExtension/ThreadPickerVC.swift index 3e975d51d..0e0ff5db3 100644 --- a/SessionShareExtension/ThreadPickerVC.swift +++ b/SessionShareExtension/ThreadPickerVC.swift @@ -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 diff --git a/SessionUIKit/Style Guide/Colors.swift b/SessionUIKit/Style Guide/Colors.swift index 8fa8dfba3..9f6c9c1c0 100644 --- a/SessionUIKit/Style Guide/Colors.swift +++ b/SessionUIKit/Style Guide/Colors.swift @@ -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")! } } diff --git a/SessionUIKit/Style Guide/Colors.xcassets/session_message_requests_bubble.colorset/Contents.json b/SessionUIKit/Style Guide/Colors.xcassets/session_message_requests_bubble.colorset/Contents.json new file mode 100644 index 000000000..afc366197 --- /dev/null +++ b/SessionUIKit/Style Guide/Colors.xcassets/session_message_requests_bubble.colorset/Contents.json @@ -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 + } +} diff --git a/SessionUIKit/Style Guide/Colors.xcassets/session_message_requests_icon.colorset/Contents.json b/SessionUIKit/Style Guide/Colors.xcassets/session_message_requests_icon.colorset/Contents.json new file mode 100644 index 000000000..daf38898d --- /dev/null +++ b/SessionUIKit/Style Guide/Colors.xcassets/session_message_requests_icon.colorset/Contents.json @@ -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 + } +} diff --git a/SessionUIKit/Style Guide/Colors.xcassets/session_message_requests_info_text.colorset/Contents.json b/SessionUIKit/Style Guide/Colors.xcassets/session_message_requests_info_text.colorset/Contents.json new file mode 100644 index 000000000..0553af194 --- /dev/null +++ b/SessionUIKit/Style Guide/Colors.xcassets/session_message_requests_info_text.colorset/Contents.json @@ -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 + } +} diff --git a/SessionUIKit/Style Guide/Colors.xcassets/session_message_requests_title.colorset/Contents.json b/SessionUIKit/Style Guide/Colors.xcassets/session_message_requests_title.colorset/Contents.json new file mode 100644 index 000000000..8018a8e57 --- /dev/null +++ b/SessionUIKit/Style Guide/Colors.xcassets/session_message_requests_title.colorset/Contents.json @@ -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 + } +} diff --git a/SessionUIKit/Style Guide/Colors.xcassets/session_pin_icon.colorset/Contents.json b/SessionUIKit/Style Guide/Colors.xcassets/session_pin_icon.colorset/Contents.json index dea6b5f39..505c4666c 100644 --- a/SessionUIKit/Style Guide/Colors.xcassets/session_pin_icon.colorset/Contents.json +++ b/SessionUIKit/Style Guide/Colors.xcassets/session_pin_icon.colorset/Contents.json @@ -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" diff --git a/SessionUIKit/Utilities/UIView+Constraints.swift b/SessionUIKit/Utilities/UIView+Constraints.swift index e62e27940..61e6e4f36 100644 --- a/SessionUIKit/Utilities/UIView+Constraints.swift +++ b/SessionUIKit/Utilities/UIView+Constraints.swift @@ -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 = { + 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 diff --git a/SessionUtilitiesKit/General/SNUserDefaults.swift b/SessionUtilitiesKit/General/SNUserDefaults.swift index 8a0fad35b..0e27a2a3b 100644 --- a/SessionUtilitiesKit/General/SNUserDefaults.swift +++ b/SessionUtilitiesKit/General/SNUserDefaults.swift @@ -7,6 +7,7 @@ public enum SNUserDefaults { case hasViewedSeed case hasSeenLinkPreviewSuggestion case isUsingFullAPNs + case hasHiddenMessageRequests } public enum Date : Swift.String { diff --git a/SignalUtilitiesKit/Database/Migrations/MessageRequestsMigration.swift b/SignalUtilitiesKit/Database/Migrations/MessageRequestsMigration.swift new file mode 100644 index 000000000..e22c31aaa --- /dev/null +++ b/SignalUtilitiesKit/Database/Migrations/MessageRequestsMigration.swift @@ -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 = 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() + }) + } +} diff --git a/SignalUtilitiesKit/Database/Migrations/OWSDatabaseMigrationRunner.m b/SignalUtilitiesKit/Database/Migrations/OWSDatabaseMigrationRunner.m index fd8ea2d2c..a7d38515a 100644 --- a/SignalUtilitiesKit/Database/Migrations/OWSDatabaseMigrationRunner.m +++ b/SignalUtilitiesKit/Database/Migrations/OWSDatabaseMigrationRunner.m @@ -26,6 +26,7 @@ NS_ASSUME_NONNULL_BEGIN - (NSArray *)allMigrations { return @[ + [SNMessageRequestsMigration new], [SNContactsMigration new] ]; } diff --git a/SignalUtilitiesKit/Media Viewing & Editing/MediaMessageView.swift b/SignalUtilitiesKit/Media Viewing & Editing/MediaMessageView.swift index 20a1e0ef5..51e6dc4de 100644 --- a/SignalUtilitiesKit/Media Viewing & Editing/MediaMessageView.swift +++ b/SignalUtilitiesKit/Media Viewing & Editing/MediaMessageView.swift @@ -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 diff --git a/SignalUtilitiesKit/Messaging/ConfigurationMessage+Convenience.swift b/SignalUtilitiesKit/Messaging/ConfigurationMessage+Convenience.swift deleted file mode 100644 index ac4d6da5d..000000000 --- a/SignalUtilitiesKit/Messaging/ConfigurationMessage+Convenience.swift +++ /dev/null @@ -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 = [] - var openGroups: Set = [] - var contacts: Set = [] - 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) - } -} diff --git a/SignalUtilitiesKit/Messaging/OWSMessageUtils.m b/SignalUtilitiesKit/Messaging/OWSMessageUtils.m index 9f7d6ea41..fe8d0a4d1 100644 --- a/SignalUtilitiesKit/Messaging/OWSMessageUtils.m +++ b/SignalUtilitiesKit/Messaging/OWSMessageUtils.m @@ -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)]) { diff --git a/SignalUtilitiesKit/Utilities/UIColor+Extensions.swift b/SignalUtilitiesKit/Utilities/UIColor+Extensions.swift index 19aa376ce..5001f50ef 100644 --- a/SignalUtilitiesKit/Utilities/UIColor+Extensions.swift +++ b/SignalUtilitiesKit/Utilities/UIColor+Extensions.swift @@ -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) } } diff --git a/SignalUtilitiesKit/Utilities/VersionMigrations.m b/SignalUtilitiesKit/Utilities/VersionMigrations.m index 221ae165d..5198baf42 100644 --- a/SignalUtilitiesKit/Utilities/VersionMigrations.m +++ b/SignalUtilitiesKit/Utilities/VersionMigrations.m @@ -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; }