mirror of
https://github.com/oxen-io/session-ios.git
synced 2023-12-13 21:30:14 +01:00
Merge branch 'dev' of https://github.com/oxen-io/session-ios into preformance-improvement
This commit is contained in:
commit
f22672ccd7
82 changed files with 3775 additions and 509 deletions
|
@ -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 */; };
|
||||
|
@ -681,7 +680,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 */; };
|
||||
|
@ -780,12 +778,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 */
|
||||
|
@ -901,6 +907,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 */
|
||||
|
@ -931,6 +944,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 */
|
||||
|
@ -1032,7 +1056,6 @@
|
|||
4503F1BB20470A5B00CEE724 /* classic-quiet.aifc */ = {isa = PBXFileReference; lastKnownFileType = file; path = "classic-quiet.aifc"; sourceTree = "<group>"; };
|
||||
4503F1BC20470A5B00CEE724 /* classic.aifc */ = {isa = PBXFileReference; lastKnownFileType = file; path = classic.aifc; sourceTree = "<group>"; };
|
||||
4509E7991DD653700025A59F /* WebRTC.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WebRTC.framework; path = ThirdParty/WebRTC/Build/WebRTC.framework; sourceTree = "<group>"; };
|
||||
450DF2041E0D74AC003D14BE /* Platform.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Platform.swift; sourceTree = "<group>"; };
|
||||
450DF2081E0DD2C6003D14BE /* UserNotificationsAdaptee.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = UserNotificationsAdaptee.swift; sourceTree = "<group>"; xcLanguageSpecificationIdentifier = xcode.lang.swift; };
|
||||
451166BF1FD86B98000739BA /* AccountManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AccountManager.swift; sourceTree = "<group>"; };
|
||||
451A13B01E13DED2000A50FD /* AppNotifications.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = AppNotifications.swift; sourceTree = "<group>"; xcLanguageSpecificationIdentifier = xcode.lang.swift; };
|
||||
|
@ -1711,7 +1734,6 @@
|
|||
C3A76A8C25DB83F90074CB90 /* PermissionMissingModal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PermissionMissingModal.swift; sourceTree = "<group>"; };
|
||||
C3A8AF752665B03900A467FE /* hi */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = hi; path = hi.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||
C3A8AF762665F97A00A467FE /* fi */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fi; path = fi.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||
C3AAFFDE25AE96FF0089E6DD /* ConfigurationMessage+Convenience.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ConfigurationMessage+Convenience.swift"; sourceTree = "<group>"; };
|
||||
C3AAFFF125AE99710089E6DD /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
||||
C3ADC66026426688005F1414 /* ShareVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareVC.swift; sourceTree = "<group>"; };
|
||||
C3AECBEA24EF5244005743DE /* fa */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fa; path = fa.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||
|
@ -1814,12 +1836,18 @@
|
|||
F9BBF530D71905BA9007675F /* Pods-SessionShareExtension.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SessionShareExtension.debug.xcconfig"; path = "Pods/Target Support Files/Pods-SessionShareExtension/Pods-SessionShareExtension.debug.xcconfig"; sourceTree = "<group>"; };
|
||||
FC3BD9871A30A790005B96BB /* Social.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Social.framework; path = System/Library/Frameworks/Social.framework; sourceTree = SDKROOT; };
|
||||
FCB11D8B1A129A76002F93FB /* CoreMedia.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreMedia.framework; path = System/Library/Frameworks/CoreMedia.framework; sourceTree = SDKROOT; };
|
||||
FD5D200E27AA2B6000FEA984 /* MessageRequestResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageRequestResponse.swift; sourceTree = "<group>"; };
|
||||
FD5D201027AA331F00FEA984 /* ConfigurationMessage+Convenience.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ConfigurationMessage+Convenience.swift"; sourceTree = "<group>"; };
|
||||
FD659ABF27A7649600F12C02 /* MessageRequestsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageRequestsViewController.swift; sourceTree = "<group>"; };
|
||||
FD705A8B278CDB5600F16121 /* SAEScreenLockViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SAEScreenLockViewController.swift; sourceTree = "<group>"; };
|
||||
FD705A8D278CE29800F16121 /* String+Localization.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+Localization.swift"; sourceTree = "<group>"; };
|
||||
FD705A8F278CEBBC00F16121 /* ShareAppExtensionContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareAppExtensionContext.swift; sourceTree = "<group>"; };
|
||||
FD705A91278D051200F16121 /* ReusableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReusableView.swift; sourceTree = "<group>"; };
|
||||
FD705A93278D052B00F16121 /* UITableView+ReusableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UITableView+ReusableView.swift"; sourceTree = "<group>"; };
|
||||
FD705A97278E9F4D00F16121 /* UIColor+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIColor+Extensions.swift"; sourceTree = "<group>"; };
|
||||
FD859EFF27C4691300510D0C /* MockDataGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockDataGenerator.swift; sourceTree = "<group>"; };
|
||||
FD88BAD827A7439C00BBC442 /* MessageRequestsCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageRequestsCell.swift; sourceTree = "<group>"; };
|
||||
FD88BADA27A750F200BBC442 /* MessageRequestsMigration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageRequestsMigration.swift; sourceTree = "<group>"; };
|
||||
FD9039443F7CB729CF71350E /* Pods-GlobalDependencies-FrameworkAndExtensionDependencies-SessionNotificationServiceExtension.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-GlobalDependencies-FrameworkAndExtensionDependencies-SessionNotificationServiceExtension.debug.xcconfig"; path = "Pods/Target Support Files/Pods-GlobalDependencies-FrameworkAndExtensionDependencies-SessionNotificationServiceExtension/Pods-GlobalDependencies-FrameworkAndExtensionDependencies-SessionNotificationServiceExtension.debug.xcconfig"; sourceTree = "<group>"; };
|
||||
FEDBAE1B98C49BBE8C87F575 /* Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionUtilitiesKit.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionUtilitiesKit.debug.xcconfig"; path = "Pods/Target Support Files/Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionUtilitiesKit/Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionUtilitiesKit.debug.xcconfig"; sourceTree = "<group>"; };
|
||||
FF9BA33D021B115B1F5B4E46 /* Pods-SessionMessagingKit.app store release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SessionMessagingKit.app store release.xcconfig"; path = "Pods/Target Support Files/Pods-SessionMessagingKit/Pods-SessionMessagingKit.app store release.xcconfig"; sourceTree = "<group>"; };
|
||||
|
@ -1892,6 +1920,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 */,
|
||||
);
|
||||
|
@ -2042,7 +2071,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 */,
|
||||
|
@ -2057,6 +2085,7 @@
|
|||
B83F2B87240CB75A000A54AB /* UIImage+Scaling.swift */,
|
||||
C31A6C59247F214E001123EF /* UIView+Glow.swift */,
|
||||
C3548F0724456AB6009433A8 /* UIView+Wrapping.swift */,
|
||||
FD859EFF27C4691300510D0C /* MockDataGenerator.swift */,
|
||||
);
|
||||
path = Utilities;
|
||||
sourceTree = "<group>";
|
||||
|
@ -2465,7 +2494,9 @@
|
|||
B8F5F60225EDE16F003BF8D4 /* DataExtractionNotification.swift */,
|
||||
C300A5E62554B07300555489 /* ExpirationTimerUpdate.swift */,
|
||||
C3DA9C0625AE7396008F7C7E /* ConfigurationMessage.swift */,
|
||||
FD5D201027AA331F00FEA984 /* ConfigurationMessage+Convenience.swift */,
|
||||
7B4C75CA26B37E0F0000AC89 /* UnsendRequest.swift */,
|
||||
FD5D200E27AA2B6000FEA984 /* MessageRequestResponse.swift */,
|
||||
);
|
||||
path = "Control Messages";
|
||||
sourceTree = "<group>";
|
||||
|
@ -2833,9 +2864,11 @@
|
|||
C360968E25AD16E8008B62B2 /* Home */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
7BA7F4B9279F9F3700B3A466 /* GlobalSearch */,
|
||||
FD659ABE27A7648200F12C02 /* Message Requests */,
|
||||
FD88BAD727A7438E00BBC442 /* Views */,
|
||||
B8BB82A4238F627000BA5194 /* HomeVC.swift */,
|
||||
B83F2B85240C7B8F000A54AB /* NewConversationButtonSet.swift */,
|
||||
7BA7F4B9279F9F3700B3A466 /* GlobalSearch */,
|
||||
);
|
||||
path = Home;
|
||||
sourceTree = "<group>";
|
||||
|
@ -3050,6 +3083,7 @@
|
|||
children = (
|
||||
B8B32044258C117C0020074B /* ContactsMigration.swift */,
|
||||
7B1D74AD27C346220030B423 /* UnreadMentionMigtation.swift */,
|
||||
FD88BADA27A750F200BBC442 /* MessageRequestsMigration.swift */,
|
||||
C38EF271255B6D79007E1867 /* OWSDatabaseMigration.h */,
|
||||
C38EF270255B6D79007E1867 /* OWSDatabaseMigration.m */,
|
||||
C38EF26F255B6D79007E1867 /* OWSDatabaseMigrationRunner.h */,
|
||||
|
@ -3154,7 +3188,6 @@
|
|||
C38EF2E3255B6DB9007E1867 /* OWSUnreadIndicator.m */,
|
||||
C33FDAE8255A580500E217F9 /* OWSMessageUtils.h */,
|
||||
C33FDBD7255A581900E217F9 /* OWSMessageUtils.m */,
|
||||
C3AAFFDE25AE96FF0089E6DD /* ConfigurationMessage+Convenience.swift */,
|
||||
);
|
||||
path = Messaging;
|
||||
sourceTree = "<group>";
|
||||
|
@ -3617,6 +3650,22 @@
|
|||
path = Session;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
FD659ABE27A7648200F12C02 /* Message Requests */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
FD659ABF27A7649600F12C02 /* MessageRequestsViewController.swift */,
|
||||
);
|
||||
path = "Message Requests";
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
FD88BAD727A7438E00BBC442 /* Views */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
FD88BAD827A7439C00BBC442 /* MessageRequestsCell.swift */,
|
||||
);
|
||||
path = Views;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXGroup section */
|
||||
|
||||
/* Begin PBXHeadersBuildPhase section */
|
||||
|
@ -3912,10 +3961,12 @@
|
|||
C3C2A6EC25539DE700C340D1 /* Sources */,
|
||||
C3C2A6ED25539DE700C340D1 /* Frameworks */,
|
||||
C3C2A6EE25539DE700C340D1 /* Resources */,
|
||||
FDC438A227BA2B8A00C60D73 /* Embed Frameworks */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
FDC438A127BA2B8A00C60D73 /* PBXTargetDependency */,
|
||||
);
|
||||
name = SessionMessagingKit;
|
||||
productName = SessionMessagingKit;
|
||||
|
@ -4569,13 +4620,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 */,
|
||||
7B1D74AE27C346220030B423 /* UnreadMentionMigtation.swift in Sources */,
|
||||
B8F5F52925EC4F8A003BF8D4 /* BlockListUIUtils.m in Sources */,
|
||||
|
@ -4692,6 +4743,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 */,
|
||||
|
@ -4750,6 +4802,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 */,
|
||||
|
@ -4879,7 +4932,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 */,
|
||||
|
@ -4887,11 +4939,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 */,
|
||||
|
@ -4943,6 +4997,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 */,
|
||||
|
@ -5076,6 +5131,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 */
|
||||
|
|
|
@ -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
|
||||
|
||||
Storage.shared.write(
|
||||
with: { transaction in
|
||||
tsMessage.save(with: transaction as! YapDatabaseReadWriteTransaction)
|
||||
}, completion: { [weak self] in
|
||||
},
|
||||
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)
|
||||
|
@ -1007,3 +1059,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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import SessionUIKit
|
||||
import SessionMessagingKit
|
||||
|
||||
// TODO:
|
||||
// • Slight paging glitch when scrolling up and loading more content
|
||||
|
@ -6,10 +8,13 @@
|
|||
|
||||
final class ConversationVC : BaseVC, ConversationViewModelDelegate, OWSConversationSettingsViewDelegate, ConversationSearchControllerDelegate, UITableViewDataSource, UITableViewDelegate {
|
||||
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?
|
||||
|
@ -92,7 +97,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
|
||||
|
@ -100,13 +108,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()
|
||||
|
@ -127,8 +143,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 {
|
||||
|
@ -145,6 +159,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).
|
||||
|
@ -161,6 +273,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
|
||||
|
@ -186,9 +299,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)
|
||||
|
@ -199,8 +352,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)
|
||||
|
@ -220,6 +372,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()
|
||||
|
|
|
@ -3,6 +3,16 @@ final class ExpandingAttachmentsButton : UIView, InputViewButtonDelegate {
|
|||
private weak var delegate: ExpandingAttachmentsButtonDelegate?
|
||||
private var isExpanded = false { didSet { expandOrCollapse() } }
|
||||
|
||||
override var isUserInteractionEnabled: Bool {
|
||||
didSet {
|
||||
gifButton.isUserInteractionEnabled = isUserInteractionEnabled
|
||||
documentButton.isUserInteractionEnabled = isUserInteractionEnabled
|
||||
libraryButton.isUserInteractionEnabled = isUserInteractionEnabled
|
||||
cameraButton.isUserInteractionEnabled = isUserInteractionEnabled
|
||||
mainButton.isUserInteractionEnabled = isUserInteractionEnabled
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Constraints
|
||||
private lazy var gifButtonContainerBottomConstraint = gifButtonContainer.pin(.bottom, to: .bottom, of: self)
|
||||
private lazy var documentButtonContainerBottomConstraint = documentButtonContainer.pin(.bottom, to: .bottom, of: self)
|
||||
|
@ -134,7 +144,8 @@ final class ExpandingAttachmentsButton : UIView, InputViewButtonDelegate {
|
|||
}
|
||||
|
||||
// MARK: Delegate
|
||||
protocol ExpandingAttachmentsButtonDelegate : class {
|
||||
|
||||
protocol ExpandingAttachmentsButtonDelegate: AnyObject {
|
||||
|
||||
func handleGIFButtonTapped()
|
||||
func handleDocumentButtonTapped()
|
||||
|
|
|
@ -1,5 +1,13 @@
|
|||
import UIKit
|
||||
import SessionUIKit
|
||||
|
||||
final class InputView : UIView, InputViewButtonDelegate, InputTextViewDelegate, QuoteViewDelegate, LinkPreviewViewDelegate, MentionSelectionViewDelegate {
|
||||
enum MessageTypes {
|
||||
case all
|
||||
case textOnly
|
||||
case none
|
||||
}
|
||||
|
||||
private weak var delegate: InputViewDelegate?
|
||||
var quoteDraftInfo: (model: OWSQuotedReplyModel, isOutgoing: Bool)? { didSet { handleQuoteDraftChanged() } }
|
||||
var linkPreviewInfo: (url: String, draft: OWSLinkPreviewDraft?)?
|
||||
|
@ -16,10 +24,18 @@ final class InputView : UIView, InputViewButtonDelegate, InputTextViewDelegate,
|
|||
set { inputTextView.text = newValue }
|
||||
}
|
||||
|
||||
var enabledMessageTypes: MessageTypes = .all {
|
||||
didSet {
|
||||
setEnabledMessageTypes(enabledMessageTypes, message: nil)
|
||||
}
|
||||
}
|
||||
|
||||
override var intrinsicContentSize: CGSize { CGSize.zero }
|
||||
var lastSearchedText: String? { nil }
|
||||
|
||||
// MARK: UI Components
|
||||
|
||||
private var bottomStackView: UIStackView?
|
||||
private lazy var attachmentsButton = ExpandingAttachmentsButton(delegate: delegate)
|
||||
|
||||
private lazy var voiceMessageButton: InputViewButton = {
|
||||
|
@ -29,6 +45,7 @@ final class InputView : UIView, InputViewButtonDelegate, InputTextViewDelegate,
|
|||
return result
|
||||
}()
|
||||
|
||||
|
||||
private lazy var sendButton: InputViewButton = {
|
||||
let result = InputViewButton(icon: #imageLiteral(resourceName: "ArrowUp"), isSendButton: true, delegate: self)
|
||||
result.isHidden = true
|
||||
|
@ -67,6 +84,17 @@ final class InputView : UIView, InputViewButtonDelegate, InputTextViewDelegate,
|
|||
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()
|
||||
|
||||
// MARK: Settings
|
||||
|
@ -109,6 +137,7 @@ final class InputView : UIView, InputViewButtonDelegate, InputTextViewDelegate,
|
|||
bottomStackView.axis = .horizontal
|
||||
bottomStackView.spacing = Values.smallSpacing
|
||||
bottomStackView.alignment = .center
|
||||
self.bottomStackView = bottomStackView
|
||||
// Main stack view
|
||||
let mainStackView = UIStackView(arrangedSubviews: [ additionalContentContainer, bottomStackView ])
|
||||
mainStackView.axis = .vertical
|
||||
|
@ -119,6 +148,14 @@ final class InputView : UIView, InputViewButtonDelegate, InputTextViewDelegate,
|
|||
mainStackView.pin(.top, to: .bottom, of: separator)
|
||||
mainStackView.pin([ UIView.HorizontalEdge.leading, UIView.HorizontalEdge.trailing ], to: self)
|
||||
mainStackView.pin(.bottom, to: .bottom, of: self)
|
||||
|
||||
addSubview(disabledInputLabel)
|
||||
|
||||
disabledInputLabel.pin(.top, to: .top, of: mainStackView)
|
||||
disabledInputLabel.pin(.left, to: .left, of: mainStackView)
|
||||
disabledInputLabel.pin(.right, to: .right, of: mainStackView)
|
||||
disabledInputLabel.set(.height, to: InputViewButton.expandedSize)
|
||||
|
||||
// Mentions
|
||||
insertSubview(mentionsViewContainer, belowSubview: mainStackView)
|
||||
mentionsViewContainer.pin([ UIView.HorizontalEdge.left, UIView.HorizontalEdge.right ], to: self)
|
||||
|
@ -168,6 +205,9 @@ final class InputView : UIView, InputViewButtonDelegate, InputTextViewDelegate,
|
|||
}
|
||||
|
||||
private func autoGenerateLinkPreviewIfPossible() {
|
||||
// Don't allow link previews on 'none' or 'textOnly' input
|
||||
guard enabledMessageTypes == .all else { return }
|
||||
|
||||
// Suggest that the user enable link previews if they haven't already and we haven't
|
||||
// told them about link previews yet
|
||||
let text = inputTextView.text!
|
||||
|
@ -216,6 +256,29 @@ final class InputView : UIView, InputViewButtonDelegate, InputTextViewDelegate,
|
|||
}.retainUntilComplete()
|
||||
}
|
||||
|
||||
func setEnabledMessageTypes(_ messageTypes: MessageTypes, message: String?) {
|
||||
guard enabledMessageTypes != messageTypes else { return }
|
||||
|
||||
enabledMessageTypes = messageTypes
|
||||
disabledInputLabel.text = (message ?? "")
|
||||
|
||||
attachmentsButton.isUserInteractionEnabled = (messageTypes == .all)
|
||||
voiceMessageButton.isUserInteractionEnabled = (messageTypes == .all)
|
||||
|
||||
UIView.animate(withDuration: 0.3) { [weak self] in
|
||||
self?.bottomStackView?.alpha = (messageTypes != .none ? 1 : 0)
|
||||
self?.attachmentsButton.alpha = (messageTypes == .all ?
|
||||
1 :
|
||||
(messageTypes == .textOnly ? 0.4 : 0)
|
||||
)
|
||||
self?.voiceMessageButton.alpha = (messageTypes == .all ?
|
||||
1 :
|
||||
(messageTypes == .textOnly ? 0.4 : 0)
|
||||
)
|
||||
self?.disabledInputLabel.alpha = (messageTypes != .none ? 0 : 1)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Interaction
|
||||
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
||||
// Needed so that the user can tap the buttons when the expanding attachments button is expanded
|
||||
|
|
|
@ -98,6 +98,8 @@ final class InputViewButton : UIView {
|
|||
// We want to detect both taps and long presses
|
||||
|
||||
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
|
||||
guard isUserInteractionEnabled else { return }
|
||||
|
||||
UIImpactFeedbackGenerator(style: .heavy).impactOccurred()
|
||||
expand()
|
||||
invalidateLongPressIfNeeded()
|
||||
|
@ -109,12 +111,16 @@ final class InputViewButton : UIView {
|
|||
}
|
||||
|
||||
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
|
||||
guard isUserInteractionEnabled else { return }
|
||||
|
||||
if isLongPress {
|
||||
delegate?.handleInputViewButtonLongPressMoved(self, with: touches.first!)
|
||||
}
|
||||
}
|
||||
|
||||
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
|
||||
guard isUserInteractionEnabled else { return }
|
||||
|
||||
collapse()
|
||||
if !isLongPress {
|
||||
delegate?.handleInputViewButtonTapped(self)
|
||||
|
|
|
@ -359,6 +359,13 @@ CGFloat kIconViewLength = 24;
|
|||
[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 ]];
|
||||
topRow.spacing = strongSelf.iconSpacing;
|
||||
|
@ -431,6 +438,13 @@ CGFloat kIconViewLength = 24;
|
|||
[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;
|
||||
|
||||
cell.accessibilityIdentifier = ACCESSIBILITY_IDENTIFIER_WITH_NAME(
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
@ -36,6 +40,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)
|
||||
|
@ -135,7 +140,7 @@ final class HomeVC : BaseVC, UITableViewDataSource, UITableViewDelegate, NewConv
|
|||
notificationCenter.addObserver(self, selector: #selector(handleBlockedContactsUpdatedNotification(_:)), name: .blockedContactsUpdated, object: nil)
|
||||
notificationCenter.addObserver(self, selector: #selector(applicationDidBecomeActive(_:)), name: .OWSApplicationDidBecomeActive, 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
|
||||
|
@ -174,18 +179,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 {
|
||||
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()
|
||||
guard !isReloading else { return }
|
||||
|
@ -213,27 +242,93 @@ 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)
|
||||
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
|
||||
}
|
||||
}
|
||||
|
@ -247,8 +342,17 @@ 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!)
|
||||
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
|
||||
}
|
||||
}
|
||||
|
@ -318,26 +422,20 @@ final class HomeVC : BaseVC, UITableViewDataSource, UITableViewDelegate, NewConv
|
|||
tableView.reloadData()
|
||||
}
|
||||
|
||||
// MARK: Interaction
|
||||
func handleContinueButtonTapped(from seedReminderView: SeedReminderView) {
|
||||
let seedVC = SeedVC()
|
||||
let navigationController = OWSNavigationController(rootViewController: seedVC)
|
||||
present(navigationController, animated: true, completion: nil)
|
||||
}
|
||||
// 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)
|
||||
tableView.deselectRow(at: indexPath, animated: true)
|
||||
}
|
||||
|
||||
@objc func show(_ thread: TSThread, with action: ConversationViewAction, highlightedMessageID: String?, animated: Bool) {
|
||||
DispatchMainThreadSafe {
|
||||
if let presentedVC = self.presentedViewController {
|
||||
presentedVC.dismiss(animated: false, completion: nil)
|
||||
}
|
||||
let conversationVC = ConversationVC(thread: thread)
|
||||
self.navigationController?.setViewControllers([ self, conversationVC ], animated: true)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -346,6 +444,21 @@ final class HomeVC : BaseVC, UITableViewDataSource, UITableViewDelegate, NewConv
|
|||
}
|
||||
|
||||
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: "")
|
||||
|
@ -397,6 +510,25 @@ final class HomeVC : BaseVC, UITableViewDataSource, UITableViewDelegate, NewConv
|
|||
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)
|
||||
}
|
||||
|
||||
@objc func show(_ thread: TSThread, with action: ConversationViewAction, highlightedMessageID: String?, animated: Bool) {
|
||||
DispatchMainThreadSafe {
|
||||
if let presentedVC = self.presentedViewController {
|
||||
presentedVC.dismiss(animated: false, completion: nil)
|
||||
}
|
||||
let conversationVC = ConversationVC(thread: thread)
|
||||
self.navigationController?.setViewControllers([ self, conversationVC ], animated: true)
|
||||
}
|
||||
}
|
||||
|
||||
private func delete(_ thread: TSThread) {
|
||||
let openGroupV2 = Storage.shared.getV2OpenGroup(for: thread.uniqueId!)
|
||||
|
@ -460,8 +592,9 @@ final class HomeVC : BaseVC, UITableViewDataSource, UITableViewDelegate, NewConv
|
|||
private func thread(at index: Int) -> TSThread? {
|
||||
var thread: TSThread? = nil
|
||||
dbConnection.read { transaction in
|
||||
// Note: Section needs to be '1' as we now have 'TSMessageRequests' as the 0th section
|
||||
let ext = transaction.ext(TSThreadDatabaseViewExtensionName) as! YapDatabaseViewTransaction
|
||||
thread = ext.object(atRow: UInt(index), inSection: 0, with: self.threads) as! TSThread?
|
||||
thread = ext.object(atRow: UInt(index), inSection: 1, with: self.threads) as? TSThread
|
||||
}
|
||||
return thread
|
||||
}
|
||||
|
|
|
@ -0,0 +1,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..<threadCount).compactMap { self.thread(at: $0) }
|
||||
var needsSync: Bool = false
|
||||
|
||||
let alertVC: UIAlertController = UIAlertController(title: NSLocalizedString("MESSAGE_REQUESTS_CLEAR_ALL_CONFIRMATION_TITLE", comment: ""), message: nil, preferredStyle: .actionSheet)
|
||||
alertVC.addAction(UIAlertAction(title: NSLocalizedString("MESSAGE_REQUESTS_CLEAR_ALL_CONFIRMATION_ACTON", comment: ""), style: .destructive) { _ in
|
||||
// Clear the requests
|
||||
Storage.write(
|
||||
with: { [weak self] transaction in
|
||||
threads.forEach { thread in
|
||||
if let uniqueId: String = thread.uniqueId {
|
||||
Storage.shared.cancelPendingMessageSendJobs(for: uniqueId, using: transaction)
|
||||
}
|
||||
|
||||
self?.updateContactAndThread(thread: thread, with: transaction) { threadNeedsSync in
|
||||
if threadNeedsSync {
|
||||
needsSync = true
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
completion: {
|
||||
// Block all the contacts
|
||||
threads.forEach { thread in
|
||||
if let sessionId: String = (thread as? TSContactThread)?.contactSessionID(), !OWSBlockingManager.shared().isRecipientIdBlocked(sessionId) {
|
||||
OWSBlockingManager.shared().addBlockedPhoneNumber(sessionId)
|
||||
}
|
||||
}
|
||||
|
||||
// Force a config sync (must run on the main thread)
|
||||
if needsSync {
|
||||
DispatchQueue.main.async {
|
||||
if let appDelegate = UIApplication.shared.delegate as? AppDelegate {
|
||||
appDelegate.forceSyncConfigurationNowIfNeeded().retainUntilComplete()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
})
|
||||
alertVC.addAction(UIAlertAction(title: NSLocalizedString("TXT_CANCEL_TITLE", comment: ""), style: .cancel, handler: nil))
|
||||
self.present(alertVC, animated: true, completion: nil)
|
||||
}
|
||||
|
||||
private func delete(_ thread: TSThread) {
|
||||
guard let uniqueId: String = thread.uniqueId else { return }
|
||||
|
||||
let alertVC: UIAlertController = UIAlertController(title: NSLocalizedString("MESSAGE_REQUESTS_DELETE_CONFIRMATION_ACTON", comment: ""), message: nil, preferredStyle: .actionSheet)
|
||||
alertVC.addAction(UIAlertAction(title: NSLocalizedString("TXT_DELETE_TITLE", comment: ""), style: .destructive) { _ in
|
||||
Storage.write(
|
||||
with: { [weak self] transaction in
|
||||
Storage.shared.cancelPendingMessageSendJobs(for: uniqueId, using: transaction)
|
||||
self?.updateContactAndThread(thread: thread, with: transaction)
|
||||
},
|
||||
completion: {
|
||||
// Block the contact
|
||||
if let sessionId: String = (thread as? TSContactThread)?.contactSessionID(), !OWSBlockingManager.shared().isRecipientIdBlocked(sessionId) {
|
||||
OWSBlockingManager.shared().addBlockedPhoneNumber(sessionId)
|
||||
}
|
||||
|
||||
// Force a config sync (must run on the main thread)
|
||||
DispatchQueue.main.async {
|
||||
if let appDelegate = UIApplication.shared.delegate as? AppDelegate {
|
||||
appDelegate.forceSyncConfigurationNowIfNeeded().retainUntilComplete()
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
})
|
||||
alertVC.addAction(UIAlertAction(title: NSLocalizedString("TXT_CANCEL_TITLE", comment: ""), style: .cancel, handler: nil))
|
||||
self.present(alertVC, animated: true, completion: nil)
|
||||
}
|
||||
|
||||
// MARK: - Convenience
|
||||
|
||||
private func thread(at index: Int) -> TSThread? {
|
||||
var thread: TSThread? = nil
|
||||
|
||||
dbConnection.read { transaction in
|
||||
let ext: YapDatabaseViewTransaction? = transaction.ext(TSThreadDatabaseViewExtensionName) as? YapDatabaseViewTransaction
|
||||
thread = ext?.object(atRow: UInt(index), inSection: 0, with: self.threads) as? TSThread
|
||||
}
|
||||
|
||||
return thread
|
||||
}
|
||||
|
||||
private func threadViewModel(at index: Int) -> ThreadViewModel? {
|
||||
guard let thread = thread(at: index), let uniqueId: String = thread.uniqueId else { return nil }
|
||||
|
||||
if let cachedThreadViewModel = threadViewModelCache[uniqueId] {
|
||||
return cachedThreadViewModel
|
||||
}
|
||||
else {
|
||||
var threadViewModel: ThreadViewModel? = nil
|
||||
dbConnection.read { transaction in
|
||||
threadViewModel = ThreadViewModel(thread: thread, transaction: transaction)
|
||||
}
|
||||
threadViewModelCache[uniqueId] = threadViewModel
|
||||
|
||||
return threadViewModel
|
||||
}
|
||||
}
|
||||
}
|
134
Session/Home/Views/MessageRequestsCell.swift
Normal file
134
Session/Home/Views/MessageRequestsCell.swift
Normal file
|
@ -0,0 +1,134 @@
|
|||
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
|
||||
|
||||
import UIKit
|
||||
import SessionUIKit
|
||||
|
||||
class MessageRequestsCell: UITableViewCell {
|
||||
static let reuseIdentifier = "MessageRequestsCell"
|
||||
|
||||
// MARK: - Initialization
|
||||
|
||||
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
||||
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
||||
|
||||
setUpViewHierarchy()
|
||||
setupLayout()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
super.init(coder: coder)
|
||||
|
||||
setUpViewHierarchy()
|
||||
setupLayout()
|
||||
}
|
||||
|
||||
// MARK: - UI
|
||||
|
||||
private let iconContainerView: UIView = {
|
||||
let result: UIView = UIView()
|
||||
result.translatesAutoresizingMaskIntoConstraints = false
|
||||
result.clipsToBounds = true
|
||||
result.backgroundColor = Colors.sessionMessageRequestsBubble
|
||||
result.layer.cornerRadius = (Values.mediumProfilePictureSize / 2)
|
||||
|
||||
return result
|
||||
}()
|
||||
|
||||
private let iconImageView: UIImageView = {
|
||||
let result: UIImageView = UIImageView(image: #imageLiteral(resourceName: "message_requests").withRenderingMode(.alwaysTemplate))
|
||||
result.translatesAutoresizingMaskIntoConstraints = false
|
||||
result.tintColor = Colors.sessionMessageRequestsIcon
|
||||
|
||||
return result
|
||||
}()
|
||||
|
||||
private let titleLabel: UILabel = {
|
||||
let result: UILabel = UILabel()
|
||||
result.translatesAutoresizingMaskIntoConstraints = false
|
||||
result.setContentHuggingPriority(.defaultHigh, for: .horizontal)
|
||||
result.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
|
||||
result.font = .boldSystemFont(ofSize: Values.mediumFontSize)
|
||||
result.text = NSLocalizedString("MESSAGE_REQUESTS_TITLE", comment: "")
|
||||
result.textColor = Colors.sessionMessageRequestsTitle
|
||||
result.lineBreakMode = .byTruncatingTail
|
||||
|
||||
return result
|
||||
}()
|
||||
|
||||
private let unreadCountView: UIView = {
|
||||
let result: UIView = UIView()
|
||||
result.translatesAutoresizingMaskIntoConstraints = false
|
||||
result.clipsToBounds = true
|
||||
result.backgroundColor = Colors.text.withAlphaComponent(Values.veryLowOpacity)
|
||||
result.layer.cornerRadius = (ConversationCell.unreadCountViewSize / 2)
|
||||
|
||||
return result
|
||||
}()
|
||||
|
||||
private let unreadCountLabel: UILabel = {
|
||||
let result = UILabel()
|
||||
result.translatesAutoresizingMaskIntoConstraints = false
|
||||
result.font = .boldSystemFont(ofSize: Values.verySmallFontSize)
|
||||
result.textColor = Colors.text
|
||||
result.textAlignment = .center
|
||||
|
||||
return result
|
||||
}()
|
||||
|
||||
private func setUpViewHierarchy() {
|
||||
backgroundColor = Colors.cellBackground
|
||||
selectedBackgroundView = UIView()
|
||||
selectedBackgroundView?.backgroundColor = Colors.cellSelected
|
||||
|
||||
|
||||
contentView.addSubview(iconContainerView)
|
||||
contentView.addSubview(titleLabel)
|
||||
contentView.addSubview(unreadCountView)
|
||||
|
||||
iconContainerView.addSubview(iconImageView)
|
||||
unreadCountView.addSubview(unreadCountLabel)
|
||||
}
|
||||
|
||||
// MARK: - Layout
|
||||
|
||||
private func setupLayout() {
|
||||
NSLayoutConstraint.activate([
|
||||
contentView.heightAnchor.constraint(equalToConstant: 68),
|
||||
|
||||
iconContainerView.leftAnchor.constraint(
|
||||
equalTo: contentView.leftAnchor,
|
||||
// Need 'accentLineThickness' to line up correctly with the 'ConversationCell'
|
||||
constant: (Values.accentLineThickness + Values.mediumSpacing)
|
||||
),
|
||||
iconContainerView.centerYAnchor.constraint(equalTo: contentView.centerYAnchor),
|
||||
iconContainerView.widthAnchor.constraint(equalToConstant: Values.mediumProfilePictureSize),
|
||||
iconContainerView.heightAnchor.constraint(equalToConstant: Values.mediumProfilePictureSize),
|
||||
|
||||
iconImageView.centerXAnchor.constraint(equalTo: iconContainerView.centerXAnchor),
|
||||
iconImageView.centerYAnchor.constraint(equalTo: iconContainerView.centerYAnchor),
|
||||
iconImageView.widthAnchor.constraint(equalToConstant: 25),
|
||||
iconImageView.heightAnchor.constraint(equalToConstant: 22),
|
||||
|
||||
titleLabel.leftAnchor.constraint(equalTo: iconContainerView.rightAnchor, constant: Values.mediumSpacing),
|
||||
titleLabel.rightAnchor.constraint(lessThanOrEqualTo: contentView.rightAnchor, constant: -Values.mediumSpacing),
|
||||
titleLabel.centerYAnchor.constraint(equalTo: contentView.centerYAnchor),
|
||||
|
||||
unreadCountView.leftAnchor.constraint(equalTo: titleLabel.rightAnchor, constant: (Values.smallSpacing / 2)),
|
||||
unreadCountView.centerYAnchor.constraint(equalTo: titleLabel.centerYAnchor),
|
||||
unreadCountView.widthAnchor.constraint(equalToConstant: ConversationCell.unreadCountViewSize),
|
||||
unreadCountView.heightAnchor.constraint(equalToConstant: ConversationCell.unreadCountViewSize),
|
||||
|
||||
unreadCountLabel.topAnchor.constraint(equalTo: unreadCountView.topAnchor),
|
||||
unreadCountLabel.leftAnchor.constraint(equalTo: unreadCountView.leftAnchor),
|
||||
unreadCountLabel.rightAnchor.constraint(equalTo: unreadCountView.rightAnchor),
|
||||
unreadCountLabel.bottomAnchor.constraint(equalTo: unreadCountView.bottomAnchor)
|
||||
])
|
||||
}
|
||||
|
||||
// MARK: - Content
|
||||
|
||||
func update(with count: Int) {
|
||||
unreadCountLabel.text = "\(count)"
|
||||
unreadCountView.isHidden = (count <= 0)
|
||||
}
|
||||
}
|
|
@ -14,12 +14,19 @@ extension AppDelegate {
|
|||
let job = MessageSendJob(message: configurationMessage, destination: destination)
|
||||
JobQueue.shared.add(job, using: transaction)
|
||||
}
|
||||
|
||||
// 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(with transaction: YapDatabaseReadWriteTransaction? = nil) -> Promise<Void> {
|
||||
guard Storage.shared.getUser()?.name != nil, let configurationMessage = ConfigurationMessage.getCurrent(with: transaction) else {
|
||||
return Promise.value(())
|
||||
}
|
||||
|
||||
func forceSyncConfigurationNowIfNeeded() -> Promise<Void> {
|
||||
guard Storage.shared.getUser()?.name != nil,
|
||||
let configurationMessage = ConfigurationMessage.getCurrent() else { return Promise.value(()) }
|
||||
let destination = Message.Destination.contact(publicKey: getUserHexEncodedPublicKey())
|
||||
let (promise, seal) = Promise<Void>.pending()
|
||||
Storage.writeSync { transaction in
|
||||
|
|
12
Session/Meta/Images.xcassets/Session/message_requests.imageset/Contents.json
vendored
Normal file
12
Session/Meta/Images.xcassets/Session/message_requests.imageset/Contents.json
vendored
Normal file
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "message_requests.pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
125
Session/Meta/Images.xcassets/Session/message_requests.imageset/message_requests.pdf
vendored
Normal file
125
Session/Meta/Images.xcassets/Session/message_requests.imageset/message_requests.pdf
vendored
Normal file
|
@ -0,0 +1,125 @@
|
|||
%PDF-1.7
|
||||
|
||||
1 0 obj
|
||||
<< >>
|
||||
endobj
|
||||
|
||||
2 0 obj
|
||||
<< /Length 3 0 R >>
|
||||
stream
|
||||
/DeviceRGB CS
|
||||
/DeviceRGB cs
|
||||
q
|
||||
1.000000 0.000000 -0.000000 1.000000 0.000000 -0.001099 cm
|
||||
0.000000 0.000000 0.000000 scn
|
||||
21.607576 22.001099 m
|
||||
3.393768 22.001099 l
|
||||
1.522707 22.001099 0.000053 20.440092 0.000053 18.520739 c
|
||||
0.000053 1.402527 l
|
||||
-0.002093 1.156628 0.061090 0.914558 0.183163 0.701015 c
|
||||
0.305237 0.487473 0.481828 0.310095 0.694923 0.186979 c
|
||||
0.900043 0.066381 1.133498 0.002249 1.371506 0.001116 c
|
||||
1.609514 -0.000017 1.843569 0.061890 2.049829 0.180531 c
|
||||
6.724900 2.833986 l
|
||||
7.053913 3.022442 7.426339 3.122168 7.805599 3.123371 c
|
||||
21.606285 3.123371 l
|
||||
23.477345 3.123371 25.000000 4.684376 25.000000 6.603730 c
|
||||
25.000000 18.514294 l
|
||||
25.001291 20.436871 23.478638 22.001099 21.607576 22.001099 c
|
||||
h
|
||||
23.775423 6.603730 m
|
||||
23.775423 5.359823 22.803120 4.347942 21.607576 4.347942 c
|
||||
7.806890 4.347942 l
|
||||
7.216724 4.345131 6.637146 4.191124 6.123580 3.900650 c
|
||||
1.445283 1.244619 l
|
||||
1.425066 1.232325 1.401854 1.225824 1.378185 1.225824 c
|
||||
1.354515 1.225824 1.331300 1.232325 1.311082 1.244619 c
|
||||
1.283574 1.260406 1.261027 1.283550 1.245980 1.311449 c
|
||||
1.230933 1.339348 1.223987 1.370895 1.225920 1.402527 c
|
||||
1.225920 18.520739 l
|
||||
1.225920 19.764645 2.198225 20.776527 3.393768 20.776527 c
|
||||
21.607576 20.776527 l
|
||||
22.803120 20.776527 23.775423 19.761423 23.775423 18.514294 c
|
||||
23.775423 6.603730 l
|
||||
h
|
||||
f
|
||||
n
|
||||
Q
|
||||
q
|
||||
1.000000 0.000000 -0.000000 1.000000 0.000000 15.369263 cm
|
||||
0.000000 0.000000 0.000000 scn
|
||||
12.584548 1.987679 m
|
||||
10.633485 1.987679 9.374066 1.053783 9.333419 -1.165269 c
|
||||
11.107699 -1.165269 l
|
||||
11.202542 0.052213 11.785154 0.377046 12.584548 0.377046 c
|
||||
13.383942 0.377046 13.789767 -0.123739 13.789767 -0.718623 c
|
||||
13.789767 -1.733728 13.467169 -1.936749 11.730955 -3.073668 c
|
||||
11.730955 -4.643053 l
|
||||
13.519432 -4.643053 l
|
||||
13.519432 -3.303759 l
|
||||
14.521417 -2.802974 15.648563 -1.990891 15.648563 -0.516248 c
|
||||
15.648563 0.958394 14.589163 1.987679 12.584548 1.987679 c
|
||||
h
|
||||
f
|
||||
n
|
||||
Q
|
||||
q
|
||||
1.000000 0.000000 -0.000000 1.000000 0.000000 20.065186 cm
|
||||
0.000000 0.000000 0.000000 scn
|
||||
13.627180 -10.381148 m
|
||||
11.568368 -10.381148 l
|
||||
11.568368 -12.315970 l
|
||||
13.627180 -12.315970 l
|
||||
13.627180 -10.381148 l
|
||||
h
|
||||
f
|
||||
n
|
||||
Q
|
||||
|
||||
endstream
|
||||
endobj
|
||||
|
||||
3 0 obj
|
||||
2088
|
||||
endobj
|
||||
|
||||
4 0 obj
|
||||
<< /Annots []
|
||||
/Type /Page
|
||||
/MediaBox [ 0.000000 0.000000 25.000000 22.000000 ]
|
||||
/Resources 1 0 R
|
||||
/Contents 2 0 R
|
||||
/Parent 5 0 R
|
||||
>>
|
||||
endobj
|
||||
|
||||
5 0 obj
|
||||
<< /Kids [ 4 0 R ]
|
||||
/Count 1
|
||||
/Type /Pages
|
||||
>>
|
||||
endobj
|
||||
|
||||
6 0 obj
|
||||
<< /Pages 5 0 R
|
||||
/Type /Catalog
|
||||
>>
|
||||
endobj
|
||||
|
||||
xref
|
||||
0 7
|
||||
0000000000 65535 f
|
||||
0000000010 00000 n
|
||||
0000000034 00000 n
|
||||
0000002178 00000 n
|
||||
0000002201 00000 n
|
||||
0000002374 00000 n
|
||||
0000002448 00000 n
|
||||
trailer
|
||||
<< /ID [ (some) (id) ]
|
||||
/Root 6 0 R
|
||||
/Size 7
|
||||
>>
|
||||
startxref
|
||||
2507
|
||||
%%EOF
|
|
@ -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";
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -4,6 +4,8 @@
|
|||
|
||||
import Foundation
|
||||
import PromiseKit
|
||||
import SessionMessagingKit
|
||||
import SignalUtilitiesKit
|
||||
|
||||
/// There are two primary components in our system notification integration:
|
||||
///
|
||||
|
@ -88,7 +90,7 @@ let kNotificationDelayForBackgroumdPoll: TimeInterval = 5
|
|||
let kAudioNotificationsThrottleCount = 2
|
||||
let kAudioNotificationsThrottleInterval: TimeInterval = 5
|
||||
|
||||
protocol NotificationPresenterAdaptee: class {
|
||||
protocol NotificationPresenterAdaptee: AnyObject {
|
||||
|
||||
func registerNotificationSettings() -> Promise<Void>
|
||||
|
||||
|
@ -157,10 +159,27 @@ 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 threads = transaction.ext(TSThreadDatabaseViewExtensionName) as! YapDatabaseViewTransaction
|
||||
let numMessageRequests = threads.numberOfItems(inGroup: TSMessageRequestGroup)
|
||||
|
||||
// Allow this to show a notification if there are no message requests (ie. this is the first one)
|
||||
guard numMessageRequests <= 1 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 +204,44 @@ public class NotificationPresenter: NSObject, NotificationsProtocol {
|
|||
let senderName = Storage.shared.getContact(with: incomingMessage.authorId, using: transaction)?.displayName(for: context) ?? incomingMessage.authorId
|
||||
|
||||
let notificationTitle: String?
|
||||
var notificationBody: String?
|
||||
let previewType = preferences.notificationPreviewType(with: transaction)
|
||||
|
||||
switch previewType {
|
||||
case .noNameNoPreview:
|
||||
notificationTitle = "Session"
|
||||
|
||||
case .nameNoPreview, .namePreview:
|
||||
switch thread {
|
||||
case is TSContactThread:
|
||||
notificationTitle = senderName
|
||||
notificationTitle = (thread.isMessageRequest() ? "Session" : senderName)
|
||||
|
||||
case is TSGroupThread:
|
||||
var groupName = thread.name(with: transaction)
|
||||
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?
|
||||
switch previewType {
|
||||
case .noNameNoPreview, .nameNoPreview:
|
||||
notificationBody = NotificationStrings.incomingMessageBody
|
||||
case .namePreview:
|
||||
notificationBody = messageText
|
||||
default:
|
||||
notificationBody = NotificationStrings.incomingMessageBody
|
||||
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 +257,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,
|
||||
|
||||
self.adaptee.notify(
|
||||
category: category,
|
||||
title: notificationTitle,
|
||||
body: notificationBody ?? "",
|
||||
userInfo: userInfo,
|
||||
sound: sound,
|
||||
replacingIdentifier: identifier)
|
||||
replacingIdentifier: identifier
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -53,9 +53,9 @@ public enum PushRegistrationError: Error {
|
|||
return firstly {
|
||||
self.registerUserNotificationSettings()
|
||||
}.then { () -> Promise<(pushToken: String, voipToken: String)> in
|
||||
guard !Platform.isSimulator else {
|
||||
#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: "")
|
||||
|
|
|
@ -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)),
|
||||
|
@ -509,6 +511,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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import UIKit
|
||||
|
||||
@objc(LKModal)
|
||||
class Modal : BaseVC {
|
||||
class Modal: BaseVC, UIGestureRecognizerDelegate {
|
||||
private(set) var verticalCenteringConstraint: NSLayoutConstraint!
|
||||
|
||||
// MARK: Components
|
||||
|
@ -38,9 +39,15 @@ class Modal : BaseVC {
|
|||
let alpha = isLightMode ? CGFloat(0.1) : Values.highOpacity
|
||||
view.backgroundColor = UIColor(hex: 0x000000).withAlphaComponent(alpha)
|
||||
cancelButton.addTarget(self, action: #selector(close), for: UIControl.Event.touchUpInside)
|
||||
|
||||
let swipeGestureRecognizer = UISwipeGestureRecognizer(target: self, action: #selector(close))
|
||||
swipeGestureRecognizer.direction = .down
|
||||
view.addGestureRecognizer(swipeGestureRecognizer)
|
||||
|
||||
let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(close))
|
||||
tapGestureRecognizer.delegate = self
|
||||
view.addGestureRecognizer(tapGestureRecognizer)
|
||||
|
||||
setUpViewHierarchy()
|
||||
}
|
||||
|
||||
|
@ -57,18 +64,17 @@ class Modal : BaseVC {
|
|||
preconditionFailure("populateContentView() is abstract and must be overridden.")
|
||||
}
|
||||
|
||||
// MARK: Interaction
|
||||
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
|
||||
let touch = touches.first!
|
||||
let location = touch.location(in: view)
|
||||
if contentView.frame.contains(location) {
|
||||
super.touchesBegan(touches, with: event)
|
||||
} else {
|
||||
close()
|
||||
}
|
||||
}
|
||||
// MARK: - Interaction
|
||||
|
||||
@objc func close() {
|
||||
dismiss(animated: true, completion: nil)
|
||||
}
|
||||
|
||||
// MARK: - UIGestureRecognizerDelegate
|
||||
|
||||
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool {
|
||||
let location: CGPoint = touch.location(in: contentView)
|
||||
|
||||
return !contentView.point(inside: location, with: nil)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,17 +6,29 @@ enum ContactUtilities {
|
|||
var result: [String] = []
|
||||
Storage.read { transaction in
|
||||
TSContactThread.enumerateCollectionObjects(with: transaction) { object, _ in
|
||||
guard let thread = object as? TSContactThread, thread.shouldBeVisible else { return }
|
||||
guard
|
||||
let thread: TSContactThread = object as? TSContactThread,
|
||||
thread.shouldBeVisible,
|
||||
Storage.shared.getContact(
|
||||
with: thread.contactSessionID(),
|
||||
using: transaction
|
||||
)?.didApproveMe == true
|
||||
else {
|
||||
return
|
||||
}
|
||||
|
||||
result.append(thread.contactSessionID())
|
||||
}
|
||||
}
|
||||
func getDisplayName(for publicKey: String) -> String {
|
||||
return Storage.shared.getContact(with: publicKey)?.displayName(for: .regular) ?? publicKey
|
||||
}
|
||||
|
||||
// Remove the current user
|
||||
if let index = result.firstIndex(of: getUserHexEncodedPublicKey()) {
|
||||
result.remove(at: index)
|
||||
}
|
||||
|
||||
// Sort alphabetically
|
||||
return result.sorted { getDisplayName(for: $0) < getDisplayName(for: $1) }
|
||||
}
|
||||
|
|
255
Session/Utilities/MockDataGenerator.swift
Normal file
255
Session/Utilities/MockDataGenerator.swift
Normal file
|
@ -0,0 +1,255 @@
|
|||
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
|
||||
|
||||
import Foundation
|
||||
import Sodium
|
||||
import SessionMessagingKit
|
||||
|
||||
enum MockDataGenerator {
|
||||
// Note: This was taken from TensorFlow's Random (https://github.com/apple/swift/blob/bc8f9e61d333b8f7a625f74d48ef0b554726e349/stdlib/public/TensorFlow/Random.swift)
|
||||
// the complex approach is needed due to an issue with Swift's randomElement(using:)
|
||||
// generation (see https://stackoverflow.com/a/64897775 for more info)
|
||||
struct ARC4RandomNumberGenerator: RandomNumberGenerator {
|
||||
var state: [UInt8] = Array(0...255)
|
||||
var iPos: UInt8 = 0
|
||||
var jPos: UInt8 = 0
|
||||
|
||||
init<T: BinaryInteger>(seed: T) {
|
||||
self.init(
|
||||
seed: (0..<(UInt64.bitWidth / UInt64.bitWidth)).map { index in
|
||||
UInt8(truncatingIfNeeded: seed >> (UInt8.bitWidth * index))
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
init(seed: [UInt8]) {
|
||||
precondition(seed.count > 0, "Length of seed must be positive")
|
||||
precondition(seed.count <= 256, "Length of seed must be at most 256")
|
||||
|
||||
// Note: Have to use a for loop instead of a 'forEach' otherwise
|
||||
// it doesn't work properly (not sure why...)
|
||||
var j: UInt8 = 0
|
||||
for i: UInt8 in 0...255 {
|
||||
j &+= S(i) &+ seed[Int(i) % seed.count]
|
||||
swapAt(i, j)
|
||||
}
|
||||
}
|
||||
|
||||
/// Produce the next random UInt64 from the stream, and advance the internal state
|
||||
mutating func next() -> UInt64 {
|
||||
// Note: Have to use a for loop instead of a 'forEach' otherwise
|
||||
// it doesn't work properly (not sure why...)
|
||||
var result: UInt64 = 0
|
||||
for _ in 0..<UInt64.bitWidth / UInt8.bitWidth {
|
||||
result <<= UInt8.bitWidth
|
||||
result += UInt64(nextByte())
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
/// Helper to access the state
|
||||
private func S(_ index: UInt8) -> UInt8 {
|
||||
return state[Int(index)]
|
||||
}
|
||||
|
||||
/// Helper to swap elements of the state
|
||||
private mutating func swapAt(_ i: UInt8, _ j: UInt8) {
|
||||
state.swapAt(Int(i), Int(j))
|
||||
}
|
||||
|
||||
/// Generates the next byte in the keystream.
|
||||
private mutating func nextByte() -> UInt8 {
|
||||
iPos &+= 1
|
||||
jPos &+= S(iPos)
|
||||
swapAt(iPos, jPos)
|
||||
return S(S(iPos) &+ S(jPos))
|
||||
}
|
||||
}
|
||||
|
||||
static func generateMockData() {
|
||||
// Don't re-generate the mock data if it already exists
|
||||
var existingMockDataThread: TSContactThread?
|
||||
|
||||
Storage.read { transaction in
|
||||
existingMockDataThread = TSContactThread.getWithContactSessionID("MockDatabaseThread", transaction: transaction)
|
||||
}
|
||||
|
||||
guard existingMockDataThread == nil else { return }
|
||||
|
||||
/// The mock data generation is quite slow, there are 3 parts which take a decent amount of time (deleting the account afterwards will also take a long time):
|
||||
/// Generating the threads & content - ~3s per 100
|
||||
/// Writing to the database - ~10s per 1000
|
||||
/// Updating the UI - ~10s per 1000
|
||||
let dmThreadCount: Int = 100
|
||||
let closedGroupThreadCount: Int = 0
|
||||
let openGroupThreadCount: Int = 0
|
||||
let maxMessagesPerThread: Int = 50
|
||||
let dmRandomSeed: Int = 1111
|
||||
let cgRandomSeed: Int = 2222
|
||||
let ogRandomSeed: Int = 3333
|
||||
|
||||
// FIXME: Make sure this data doesn't go off device somehow?
|
||||
Storage.shared.write { anyTransaction in
|
||||
guard let transaction: YapDatabaseReadWriteTransaction = anyTransaction as? YapDatabaseReadWriteTransaction else { return }
|
||||
|
||||
// First create the thread used to indicate that the mock data has been generated
|
||||
_ = TSContactThread.getOrCreateThread(withContactSessionID: "MockDatabaseThread", transaction: transaction)
|
||||
|
||||
// Multiple spaces to make it look more like words
|
||||
let stringContent: [String] = "abcdefghijklmnopqrstuvwxyz ABCDEFGHIJKLMNOPQRSTUVWXYZ 0123456789 ".map { String($0) }
|
||||
let timestampNow: TimeInterval = Date().timeIntervalSince1970
|
||||
let userSessionId: String = getUserHexEncodedPublicKey()
|
||||
|
||||
// MARK: - -- DM Thread
|
||||
var dmThreadRandomGenerator: ARC4RandomNumberGenerator = ARC4RandomNumberGenerator(seed: dmRandomSeed)
|
||||
|
||||
(0..<dmThreadCount).forEach { threadIndex in
|
||||
let data = Data((0..<16).map { _ in UInt8.random(in: (UInt8.min...UInt8.max), using: &dmThreadRandomGenerator) })
|
||||
|
||||
let randomSessionId: String = KeyPairUtilities.generate(from: data).x25519KeyPair.hexEncodedPublicKey
|
||||
let isMessageRequest: Bool = Bool.random(using: &dmThreadRandomGenerator)
|
||||
let contactNameLength: Int = ((5..<20).randomElement(using: &dmThreadRandomGenerator) ?? 0)
|
||||
let numMessages: Int = ((0..<maxMessagesPerThread).randomElement(using: &dmThreadRandomGenerator) ?? 0)
|
||||
|
||||
// Generate the thread
|
||||
let thread: TSContactThread = TSContactThread.getOrCreateThread(withContactSessionID: randomSessionId, transaction: transaction)
|
||||
thread.shouldBeVisible = true
|
||||
|
||||
// Generate the contact
|
||||
let contact = Contact(sessionID: randomSessionId)
|
||||
contact.name = (0..<contactNameLength)
|
||||
.compactMap { _ in stringContent.randomElement(using: &dmThreadRandomGenerator) }
|
||||
.joined()
|
||||
contact.isApproved = (!isMessageRequest || Bool.random(using: &dmThreadRandomGenerator))
|
||||
contact.didApproveMe = (!isMessageRequest && Bool.random(using: &dmThreadRandomGenerator))
|
||||
Storage.shared.setContact(contact, using: transaction)
|
||||
|
||||
// Generate the message history (Note: Unapproved message requests will only include incoming messages)
|
||||
(0..<numMessages).forEach { index in
|
||||
let isIncoming: Bool = (
|
||||
Bool.random(using: &dmThreadRandomGenerator) &&
|
||||
(!isMessageRequest || contact.isApproved)
|
||||
)
|
||||
let messageLength: Int = ((3..<40).randomElement(using: &dmThreadRandomGenerator) ?? 0)
|
||||
|
||||
let message: VisibleMessage = VisibleMessage()
|
||||
message.sender = (isIncoming ? randomSessionId : userSessionId)
|
||||
message.sentTimestamp = UInt64(floor(timestampNow - Double(index * 5)))
|
||||
message.text = (0..<messageLength)
|
||||
.compactMap { _ in stringContent.randomElement(using: &dmThreadRandomGenerator) }
|
||||
.joined()
|
||||
|
||||
if isIncoming {
|
||||
let tsMessage: TSOutgoingMessage = TSOutgoingMessage.from(message, associatedWith: thread, using: transaction)
|
||||
tsMessage.save(with: transaction)
|
||||
}
|
||||
else {
|
||||
let tsMessage: TSIncomingMessage = TSIncomingMessage.from(message, quotedMessage: nil, linkPreview: nil, associatedWith: thread)
|
||||
tsMessage.save(with: transaction)
|
||||
}
|
||||
}
|
||||
|
||||
// Save the thread
|
||||
thread.save(with: transaction)
|
||||
}
|
||||
|
||||
// MARK: - -- Closed Group
|
||||
var cgThreadRandomGenerator: ARC4RandomNumberGenerator = ARC4RandomNumberGenerator(seed: cgRandomSeed)
|
||||
|
||||
(0..<closedGroupThreadCount).forEach { threadIndex in
|
||||
let data = Data((0..<16).map { _ in UInt8.random(in: (UInt8.min...UInt8.max), using: &cgThreadRandomGenerator) })
|
||||
let randomGroupPublicKey: String = KeyPairUtilities.generate(from: data).x25519KeyPair.hexEncodedPublicKey
|
||||
let groupNameLength: Int = ((5..<20).randomElement(using: &cgThreadRandomGenerator) ?? 0)
|
||||
let groupName: String = (0..<groupNameLength)
|
||||
.compactMap { _ in stringContent.randomElement(using: &cgThreadRandomGenerator) }
|
||||
.joined()
|
||||
let numGroupMembers: Int = ((0..<5).randomElement(using: &cgThreadRandomGenerator) ?? 0)
|
||||
let numMessages: Int = ((0..<maxMessagesPerThread).randomElement(using: &cgThreadRandomGenerator) ?? 0)
|
||||
|
||||
// Generate the Contacts in the group
|
||||
var members: [String] = [userSessionId]
|
||||
|
||||
(0..<numGroupMembers).forEach { _ in
|
||||
let contactData = Data((0..<16).map { _ in UInt8.random(in: (UInt8.min...UInt8.max), using: &cgThreadRandomGenerator) })
|
||||
let randomSessionId: String = KeyPairUtilities.generate(from: contactData).x25519KeyPair.hexEncodedPublicKey
|
||||
let contactNameLength: Int = ((5..<20).randomElement(using: &cgThreadRandomGenerator) ?? 0)
|
||||
let contact = Contact(sessionID: randomSessionId)
|
||||
contact.name = (0..<contactNameLength)
|
||||
.compactMap { _ in stringContent.randomElement(using: &cgThreadRandomGenerator) }
|
||||
.joined()
|
||||
Storage.shared.setContact(contact, using: transaction)
|
||||
|
||||
members.append(randomSessionId)
|
||||
}
|
||||
|
||||
let groupId: Data = LKGroupUtilities.getEncodedClosedGroupIDAsData(randomGroupPublicKey)
|
||||
let group: TSGroupModel = TSGroupModel(
|
||||
title: groupName,
|
||||
memberIds: members,
|
||||
image: nil,
|
||||
groupId: groupId,
|
||||
groupType: .closedGroup,
|
||||
adminIds: [members.randomElement(using: &cgThreadRandomGenerator) ?? userSessionId]
|
||||
)
|
||||
let thread = TSGroupThread.getOrCreateThread(with: group, transaction: transaction)
|
||||
thread.shouldBeVisible = true
|
||||
thread.save(with: transaction)
|
||||
|
||||
// Add the group to the user's set of public keys to poll for and store the key pair
|
||||
let encryptionKeyPair = Curve25519.generateKeyPair()
|
||||
Storage.shared.addClosedGroupPublicKey(randomGroupPublicKey, using: transaction)
|
||||
Storage.shared.addClosedGroupEncryptionKeyPair(encryptionKeyPair, for: randomGroupPublicKey, using: transaction)
|
||||
|
||||
// Generate the message history (Note: Unapproved message requests will only include incoming messages)
|
||||
(0..<numMessages).forEach { index in
|
||||
let messageLength: Int = ((3..<40).randomElement(using: &dmThreadRandomGenerator) ?? 0)
|
||||
let message: VisibleMessage = VisibleMessage()
|
||||
message.sender = (members.randomElement(using: &cgThreadRandomGenerator) ?? userSessionId)
|
||||
message.sentTimestamp = UInt64(floor(timestampNow - Double(index * 5)))
|
||||
message.text = (0..<messageLength)
|
||||
.compactMap { _ in stringContent.randomElement(using: &dmThreadRandomGenerator) }
|
||||
.joined()
|
||||
|
||||
if message.sender != userSessionId {
|
||||
let tsMessage: TSOutgoingMessage = TSOutgoingMessage.from(message, associatedWith: thread, using: transaction)
|
||||
tsMessage.save(with: transaction)
|
||||
}
|
||||
else {
|
||||
let tsMessage: TSIncomingMessage = TSIncomingMessage.from(message, quotedMessage: nil, linkPreview: nil, associatedWith: thread)
|
||||
tsMessage.save(with: transaction)
|
||||
}
|
||||
}
|
||||
|
||||
// Save the thread
|
||||
thread.save(with: transaction)
|
||||
}
|
||||
|
||||
// MARK: - --Open Group
|
||||
var ogThreadRandomGenerator: ARC4RandomNumberGenerator = ARC4RandomNumberGenerator(seed: ogRandomSeed)
|
||||
|
||||
(0..<openGroupThreadCount).forEach { threadIndex in
|
||||
let data = Data((0..<16).map { _ in UInt8.random(in: (UInt8.min...UInt8.max), using: &ogThreadRandomGenerator) })
|
||||
let randomGroupPublicKey: String = KeyPairUtilities.generate(from: data).x25519KeyPair.hexEncodedPublicKey
|
||||
let serverNameLength: Int = ((5..<20).randomElement(using: &ogThreadRandomGenerator) ?? 0)
|
||||
let roomNameLength: Int = ((5..<20).randomElement(using: &ogThreadRandomGenerator) ?? 0)
|
||||
let serverName: String = (0..<serverNameLength)
|
||||
.compactMap { _ in stringContent.randomElement(using: &ogThreadRandomGenerator) }
|
||||
.joined()
|
||||
let roomName: String = (0..<roomNameLength)
|
||||
.compactMap { _ in stringContent.randomElement(using: &ogThreadRandomGenerator) }
|
||||
.joined()
|
||||
|
||||
// Create the open group model and the thread
|
||||
let openGroup: OpenGroupV2 = OpenGroupV2(server: serverName, room: roomName, name: roomName, publicKey: randomGroupPublicKey, imageID: nil)
|
||||
let groupId: Data = LKGroupUtilities.getEncodedOpenGroupIDAsData(openGroup.id)
|
||||
let model = TSGroupModel(title: openGroup.name, memberIds: [ userSessionId ], image: nil, groupId: groupId, groupType: .openGroup, adminIds: [])
|
||||
|
||||
let thread = TSGroupThread.getOrCreateThread(with: model, transaction: transaction)
|
||||
thread.shouldBeVisible = true
|
||||
thread.save(with: transaction)
|
||||
|
||||
Storage.shared.setV2OpenGroup(openGroup, for: thread.uniqueId!, using: transaction)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,14 +0,0 @@
|
|||
// Created by Michael Kirk on 12/23/16.
|
||||
// Copyright © 2016 Open Whisper Systems. All rights reserved.
|
||||
|
||||
import Foundation
|
||||
|
||||
struct Platform {
|
||||
static let isSimulator: Bool = {
|
||||
var isSim = false
|
||||
#if arch(i386) || arch(x86_64)
|
||||
isSim = true
|
||||
#endif
|
||||
return isSim
|
||||
}()
|
||||
}
|
|
@ -12,6 +12,12 @@ public class Contact : NSObject, NSCoding { // NSObject/NSCoding conformance is
|
|||
@objc public var threadID: String?
|
||||
/// This flag is used to determine whether we should auto-download files sent by this contact.
|
||||
@objc public var isTrusted = false
|
||||
/// This flag is used to determine whether message requests from this contact are approved
|
||||
@objc public var isApproved = false
|
||||
/// This flag is used to determine whether message requests from this contact are blocked
|
||||
@objc public var isBlocked = false
|
||||
/// This flag is used to determine whether this contact has approved the current users message request
|
||||
@objc public var didApproveMe = false
|
||||
|
||||
// MARK: Name
|
||||
/// The name of the contact. Use this whenever you need the "real", underlying name of a user (e.g. when sending a message).
|
||||
|
@ -65,6 +71,10 @@ public class Contact : NSObject, NSCoding { // NSObject/NSCoding conformance is
|
|||
if let profilePictureFileName = coder.decodeObject(forKey: "profilePictureFileName") as! String? { self.profilePictureFileName = profilePictureFileName }
|
||||
if let profileEncryptionKey = coder.decodeObject(forKey: "profilePictureEncryptionKey") as! OWSAES256Key? { self.profileEncryptionKey = profileEncryptionKey }
|
||||
if let threadID = coder.decodeObject(forKey: "threadID") as! String? { self.threadID = threadID }
|
||||
|
||||
isApproved = coder.decodeBool(forKey: "isApproved")
|
||||
isBlocked = coder.decodeBool(forKey: "isBlocked")
|
||||
didApproveMe = coder.decodeBool(forKey: "didApproveMe")
|
||||
}
|
||||
|
||||
public func encode(with coder: NSCoder) {
|
||||
|
@ -76,6 +86,9 @@ public class Contact : NSObject, NSCoding { // NSObject/NSCoding conformance is
|
|||
coder.encode(profileEncryptionKey, forKey: "profilePictureEncryptionKey")
|
||||
coder.encode(threadID, forKey: "threadID")
|
||||
coder.encode(isTrusted, forKey: "isTrusted")
|
||||
coder.encode(isApproved, forKey: "isApproved")
|
||||
coder.encode(isBlocked, forKey: "isBlocked")
|
||||
coder.encode(didApproveMe, forKey: "didApproveMe")
|
||||
}
|
||||
|
||||
// MARK: Equality
|
||||
|
|
|
@ -55,11 +55,17 @@ extension Storage {
|
|||
@objc public func getAllContacts() -> Set<Contact> {
|
||||
var result: Set<Contact> = []
|
||||
Storage.read { transaction in
|
||||
result = self.getAllContacts(with: transaction)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
@objc public func getAllContacts(with transaction: YapDatabaseReadTransaction) -> Set<Contact> {
|
||||
var result: Set<Contact> = []
|
||||
transaction.enumerateRows(inCollection: Storage.contactCollection) { _, object, _, _ in
|
||||
guard let contact = object as? Contact else { return }
|
||||
result.insert(contact)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,11 +8,14 @@
|
|||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
extern NSString *const TSInboxGroup;
|
||||
extern NSString *const TSMessageRequestGroup;
|
||||
extern NSString *const TSArchiveGroup;
|
||||
extern NSString *const TSShareExtensionGroup;
|
||||
extern NSString *const TSUnreadIncomingMessagesGroup;
|
||||
extern NSString *const TSSecondaryDevicesGroup;
|
||||
|
||||
extern NSString *const TSThreadDatabaseViewExtensionName;
|
||||
extern NSString *const TSThreadShareExtensionDatabaseViewExtensionName;
|
||||
|
||||
extern NSString *const TSMessageDatabaseViewExtensionName;
|
||||
extern NSString *const TSMessageDatabaseViewExtensionName_Legacy;
|
||||
|
|
|
@ -9,14 +9,20 @@
|
|||
#import "TSIncomingMessage.h"
|
||||
#import "TSOutgoingMessage.h"
|
||||
#import "TSThread.h"
|
||||
#import "OWSBlockingManager.h"
|
||||
#import <YapDatabase/YapDatabaseAutoView.h>
|
||||
#import <YapDatabase/YapDatabaseCrossProcessNotification.h>
|
||||
#import <YapDatabase/YapDatabaseViewTypes.h>
|
||||
#import <SessionUtilitiesKit/AppContext.h>
|
||||
#import <SessionUtilitiesKit/SessionUtilitiesKit-Swift.h>
|
||||
#import <SessionMessagingKit/SessionMessagingKit-Swift.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
NSString *const TSInboxGroup = @"TSInboxGroup";
|
||||
NSString *const TSMessageRequestGroup = @"TSMessageRequestGroup";
|
||||
NSString *const TSArchiveGroup = @"TSArchiveGroup";
|
||||
NSString *const TSShareExtensionGroup = @"TSShareExtensionGroup";
|
||||
|
||||
NSString *const TSUnreadIncomingMessagesGroup = @"TSUnreadIncomingMessagesGroup";
|
||||
NSString *const TSSecondaryDevicesGroup = @"TSSecondaryDevicesGroup";
|
||||
|
@ -25,6 +31,8 @@ NSString *const TSSecondaryDevicesGroup = @"TSSecondaryDevicesGroup";
|
|||
// -> TSThreadDatabaseViewExtensionName2 to work around https://github.com/yapstudios/YapDatabase/issues/324
|
||||
NSString *const TSThreadDatabaseViewExtensionName = @"TSThreadDatabaseViewExtensionName2";
|
||||
|
||||
NSString *const TSThreadShareExtensionDatabaseViewExtensionName = @"TSThreadShareExtensionDatabaseViewExtensionName";
|
||||
|
||||
// We sort interactions by a monotonically increasing counter.
|
||||
//
|
||||
// Previously we sorted the interactions database by local timestamp, which was problematic if the local clock changed.
|
||||
|
@ -234,7 +242,15 @@ NSString *const TSLazyRestoreAttachmentsGroup = @"TSLazyRestoreAttachmentsGroup"
|
|||
}
|
||||
TSThread *thread = (TSThread *)object;
|
||||
|
||||
if (thread.shouldBeVisible) {
|
||||
if ([thread isMessageRequestUsingTransaction:transaction]) {
|
||||
// Don't show blocked threads at all
|
||||
if ([[OWSBlockingManager sharedManager] isThreadBlocked: thread]) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
return TSMessageRequestGroup;
|
||||
}
|
||||
else if (thread.shouldBeVisible) {
|
||||
// Do nothing; we never hide threads that have ever had a message.
|
||||
} else {
|
||||
YapDatabaseViewTransaction *viewTransaction = [transaction ext:TSMessageDatabaseViewExtensionName];
|
||||
|
@ -258,6 +274,53 @@ NSString *const TSLazyRestoreAttachmentsGroup = @"TSLazyRestoreAttachmentsGroup"
|
|||
[[YapDatabaseAutoView alloc] initWithGrouping:viewGrouping sorting:viewSorting versionTag:@"4" options:options];
|
||||
|
||||
[storage asyncRegisterExtension:databaseView withName:TSThreadDatabaseViewExtensionName];
|
||||
|
||||
YapDatabaseView *shareExtensionThreadView = [storage registeredExtension:TSThreadShareExtensionDatabaseViewExtensionName];
|
||||
if (shareExtensionThreadView) {
|
||||
return;
|
||||
}
|
||||
|
||||
YapDatabaseViewGrouping *shareExtensionViewGrouping = [YapDatabaseViewGrouping withObjectBlock:^NSString *(
|
||||
YapDatabaseReadTransaction *transaction, NSString *collection, NSString *key, id object) {
|
||||
if (![object isKindOfClass:[TSThread class]]) {
|
||||
return nil;
|
||||
}
|
||||
TSThread *thread = (TSThread *)object;
|
||||
|
||||
if (thread.isMessageRequest) {
|
||||
return nil;
|
||||
}
|
||||
else {
|
||||
YapDatabaseViewTransaction *viewTransaction = [transaction ext:TSMessageDatabaseViewExtensionName];
|
||||
NSUInteger threadMessageCount = [viewTransaction numberOfItemsInGroup:thread.uniqueId];
|
||||
if (threadMessageCount < 1) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
if (!thread.isGroupThread) {
|
||||
TSContactThread *contactThead = (TSContactThread *)thread;
|
||||
SNContact *contact = [LKStorage.shared getContactWithSessionID:[contactThead contactSessionID]];
|
||||
|
||||
if (contact == nil || !contact.didApproveMe) {
|
||||
return nil;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return TSShareExtensionGroup;
|
||||
}];
|
||||
|
||||
YapDatabaseViewSorting *shareExtensionViewSorting = [self threadSorting];
|
||||
|
||||
YapDatabaseViewOptions *shareExtensionOptions = [[YapDatabaseViewOptions alloc] init];
|
||||
shareExtensionOptions.isPersistent = YES;
|
||||
shareExtensionOptions.allowedCollections =
|
||||
[[YapWhitelistBlacklist alloc] initWithWhitelist:[NSSet setWithObject:[TSThread collection]]];
|
||||
|
||||
YapDatabaseView *shareExtensionDatabaseView =
|
||||
[[YapDatabaseAutoView alloc] initWithGrouping:shareExtensionViewGrouping sorting:shareExtensionViewSorting versionTag:@"1" options:shareExtensionOptions];
|
||||
|
||||
[storage asyncRegisterExtension:shareExtensionDatabaseView withName:TSThreadShareExtensionDatabaseViewExtensionName];
|
||||
}
|
||||
|
||||
+ (YapDatabaseViewSorting *)threadSorting {
|
||||
|
|
|
@ -0,0 +1,111 @@
|
|||
|
||||
extension ConfigurationMessage {
|
||||
|
||||
public static func getCurrent(with transaction: YapDatabaseReadWriteTransaction? = nil) -> ConfigurationMessage? {
|
||||
let storage = Storage.shared
|
||||
guard let user = storage.getUser() else { return nil }
|
||||
|
||||
let displayName = user.name
|
||||
let profilePictureURL = user.profilePictureURL
|
||||
let profileKey = user.profileEncryptionKey?.keyData
|
||||
var closedGroups: Set<ClosedGroup> = []
|
||||
var openGroups: Set<String> = []
|
||||
var contacts: Set<Contact> = []
|
||||
var contactCount = 0
|
||||
|
||||
let populateDataClosure: (YapDatabaseReadTransaction) -> () = { transaction in
|
||||
TSGroupThread.enumerateCollectionObjects(with: transaction) { object, _ in
|
||||
guard let thread = object as? TSGroupThread else { return }
|
||||
|
||||
switch thread.groupModel.groupType {
|
||||
case .closedGroup:
|
||||
guard thread.isCurrentUserMemberInGroup() else { return }
|
||||
|
||||
let groupID = thread.groupModel.groupId
|
||||
let groupPublicKey = LKGroupUtilities.getDecodedGroupID(groupID)
|
||||
|
||||
guard storage.isClosedGroup(groupPublicKey), let encryptionKeyPair = storage.getLatestClosedGroupEncryptionKeyPair(for: groupPublicKey) else {
|
||||
return
|
||||
}
|
||||
|
||||
let closedGroup = ClosedGroup(
|
||||
publicKey: groupPublicKey,
|
||||
name: thread.groupModel.groupName!,
|
||||
encryptionKeyPair: encryptionKeyPair,
|
||||
members: Set(thread.groupModel.groupMemberIds),
|
||||
admins: Set(thread.groupModel.groupAdminIds),
|
||||
expirationTimer: thread.disappearingMessagesDuration(with: transaction)
|
||||
)
|
||||
closedGroups.insert(closedGroup)
|
||||
|
||||
case .openGroup:
|
||||
if let v2OpenGroup = storage.getV2OpenGroup(for: thread.uniqueId!) {
|
||||
openGroups.insert("\(v2OpenGroup.server)/\(v2OpenGroup.room)?public_key=\(v2OpenGroup.publicKey)")
|
||||
}
|
||||
|
||||
default: break
|
||||
}
|
||||
}
|
||||
|
||||
let currentUserPublicKey: String = getUserHexEncodedPublicKey()
|
||||
var truncatedContacts = storage.getAllContacts(with: transaction)
|
||||
|
||||
if truncatedContacts.count > 200 {
|
||||
truncatedContacts = Set(Array(truncatedContacts)[0..<200])
|
||||
}
|
||||
|
||||
truncatedContacts.forEach { contact in
|
||||
let publicKey = contact.sessionID
|
||||
let threadID = TSContactThread.threadID(fromContactSessionID: publicKey)
|
||||
|
||||
// Want to sync contacts for visible threads and blocked contacts between devices
|
||||
guard
|
||||
publicKey != currentUserPublicKey && (
|
||||
TSContactThread.fetch(uniqueId: threadID, transaction: transaction)?.shouldBeVisible == true ||
|
||||
SSKEnvironment.shared.blockingManager.isRecipientIdBlocked(publicKey)
|
||||
)
|
||||
else {
|
||||
return
|
||||
}
|
||||
|
||||
// Can just default the 'hasX' values to true as they will be set to this
|
||||
// when converting to proto anyway
|
||||
let profilePictureURL = contact.profilePictureURL
|
||||
let profileKey = contact.profileEncryptionKey?.keyData
|
||||
let contact = ConfigurationMessage.Contact(
|
||||
publicKey: publicKey,
|
||||
displayName: (contact.name ?? publicKey),
|
||||
profilePictureURL: profilePictureURL,
|
||||
profileKey: profileKey,
|
||||
hasIsApproved: true,
|
||||
isApproved: contact.isApproved,
|
||||
hasIsBlocked: true,
|
||||
isBlocked: contact.isBlocked,
|
||||
hasDidApproveMe: true,
|
||||
didApproveMe: contact.didApproveMe
|
||||
)
|
||||
|
||||
contacts.insert(contact)
|
||||
contactCount += 1
|
||||
}
|
||||
}
|
||||
|
||||
// If we are provided with a transaction then read the data based on the state of the database
|
||||
// from within the transaction rather than the state in disk
|
||||
if let transaction: YapDatabaseReadWriteTransaction = transaction {
|
||||
populateDataClosure(transaction)
|
||||
}
|
||||
else {
|
||||
Storage.read { transaction in populateDataClosure(transaction) }
|
||||
}
|
||||
|
||||
return ConfigurationMessage(
|
||||
displayName: displayName,
|
||||
profilePictureURL: profilePictureURL,
|
||||
profileKey: profileKey,
|
||||
closedGroups: closedGroups,
|
||||
openGroups: openGroups,
|
||||
contacts: contacts
|
||||
)
|
||||
}
|
||||
}
|
|
@ -194,13 +194,37 @@ extension ConfigurationMessage {
|
|||
public var profilePictureURL: String?
|
||||
public var profileKey: Data?
|
||||
|
||||
public var hasIsApproved: Bool
|
||||
public var isApproved: Bool
|
||||
public var hasIsBlocked: Bool
|
||||
public var isBlocked: Bool
|
||||
public var hasDidApproveMe: Bool
|
||||
public var didApproveMe: Bool
|
||||
|
||||
public var isValid: Bool { publicKey != nil && displayName != nil }
|
||||
|
||||
public init(publicKey: String, displayName: String, profilePictureURL: String?, profileKey: Data?) {
|
||||
public init(
|
||||
publicKey: String,
|
||||
displayName: String,
|
||||
profilePictureURL: String?,
|
||||
profileKey: Data?,
|
||||
hasIsApproved: Bool,
|
||||
isApproved: Bool,
|
||||
hasIsBlocked: Bool,
|
||||
isBlocked: Bool,
|
||||
hasDidApproveMe: Bool,
|
||||
didApproveMe: Bool
|
||||
) {
|
||||
self.publicKey = publicKey
|
||||
self.displayName = displayName
|
||||
self.profilePictureURL = profilePictureURL
|
||||
self.profileKey = profileKey
|
||||
self.hasIsApproved = hasIsApproved
|
||||
self.isApproved = isApproved
|
||||
self.hasIsBlocked = hasIsBlocked
|
||||
self.isBlocked = isBlocked
|
||||
self.hasDidApproveMe = hasDidApproveMe
|
||||
self.didApproveMe = didApproveMe
|
||||
}
|
||||
|
||||
public required init?(coder: NSCoder) {
|
||||
|
@ -210,6 +234,12 @@ extension ConfigurationMessage {
|
|||
self.displayName = displayName
|
||||
self.profilePictureURL = coder.decodeObject(forKey: "profilePictureURL") as! String?
|
||||
self.profileKey = coder.decodeObject(forKey: "profileKey") as! Data?
|
||||
self.hasIsApproved = (coder.decodeObject(forKey: "hasIsApproved") as? Bool ?? false)
|
||||
self.isApproved = (coder.decodeObject(forKey: "isApproved") as? Bool ?? false)
|
||||
self.hasIsBlocked = (coder.decodeObject(forKey: "hasIsBlocked") as? Bool ?? false)
|
||||
self.isBlocked = (coder.decodeObject(forKey: "isBlocked") as? Bool ?? false)
|
||||
self.hasDidApproveMe = (coder.decodeObject(forKey: "hasDidApproveMe") as? Bool ?? false)
|
||||
self.didApproveMe = (coder.decodeObject(forKey: "didApproveMe") as? Bool ?? false)
|
||||
}
|
||||
|
||||
public func encode(with coder: NSCoder) {
|
||||
|
@ -217,14 +247,28 @@ extension ConfigurationMessage {
|
|||
coder.encode(displayName, forKey: "displayName")
|
||||
coder.encode(profilePictureURL, forKey: "profilePictureURL")
|
||||
coder.encode(profileKey, forKey: "profileKey")
|
||||
coder.encode(hasIsApproved, forKey: "hasIsApproved")
|
||||
coder.encode(isApproved, forKey: "isApproved")
|
||||
coder.encode(hasIsBlocked, forKey: "hasIsBlocked")
|
||||
coder.encode(isBlocked, forKey: "isBlocked")
|
||||
coder.encode(hasDidApproveMe, forKey: "hasDidApproveMe")
|
||||
coder.encode(didApproveMe, forKey: "didApproveMe")
|
||||
}
|
||||
|
||||
public static func fromProto(_ proto: SNProtoConfigurationMessageContact) -> Contact? {
|
||||
let publicKey = proto.publicKey.toHexString()
|
||||
let displayName = proto.name
|
||||
let profilePictureURL = proto.profilePicture
|
||||
let profileKey = proto.profileKey
|
||||
let result = Contact(publicKey: publicKey, displayName: displayName, profilePictureURL: profilePictureURL, profileKey: profileKey)
|
||||
let result: Contact = Contact(
|
||||
publicKey: proto.publicKey.toHexString(),
|
||||
displayName: proto.name,
|
||||
profilePictureURL: proto.profilePicture,
|
||||
profileKey: proto.profileKey,
|
||||
hasIsApproved: proto.hasIsApproved,
|
||||
isApproved: proto.isApproved,
|
||||
hasIsBlocked: proto.hasIsBlocked,
|
||||
isBlocked: proto.isBlocked,
|
||||
hasDidApproveMe: proto.hasDidApproveMe,
|
||||
didApproveMe: proto.didApproveMe
|
||||
)
|
||||
|
||||
guard result.isValid else { return nil }
|
||||
return result
|
||||
}
|
||||
|
@ -235,6 +279,11 @@ extension ConfigurationMessage {
|
|||
let result = SNProtoConfigurationMessageContact.builder(publicKey: Data(hex: publicKey), name: displayName)
|
||||
if let profilePictureURL = profilePictureURL { result.setProfilePicture(profilePictureURL) }
|
||||
if let profileKey = profileKey { result.setProfileKey(profileKey) }
|
||||
|
||||
if hasIsApproved { result.setIsApproved(isApproved) }
|
||||
if hasIsBlocked { result.setIsBlocked(isBlocked) }
|
||||
if hasDidApproveMe { result.setDidApproveMe(didApproveMe) }
|
||||
|
||||
do {
|
||||
return try result.build()
|
||||
} catch {
|
||||
|
|
|
@ -0,0 +1,63 @@
|
|||
import SessionUtilitiesKit
|
||||
|
||||
@objc(SNMessageRequestResponse)
|
||||
public final class MessageRequestResponse: ControlMessage {
|
||||
public var isApproved: Bool
|
||||
|
||||
// MARK: - Initialization
|
||||
|
||||
public init(isApproved: Bool) {
|
||||
self.isApproved = isApproved
|
||||
|
||||
super.init()
|
||||
}
|
||||
|
||||
// MARK: - Coding
|
||||
|
||||
public required init?(coder: NSCoder) {
|
||||
guard let isApproved: Bool = coder.decodeObject(forKey: "isApproved") as? Bool else { return nil }
|
||||
|
||||
self.isApproved = isApproved
|
||||
|
||||
super.init(coder: coder)
|
||||
}
|
||||
|
||||
public override func encode(with coder: NSCoder) {
|
||||
super.encode(with: coder)
|
||||
|
||||
coder.encode(isApproved, forKey: "isApproved")
|
||||
}
|
||||
|
||||
// MARK: - Proto Conversion
|
||||
|
||||
public override class func fromProto(_ proto: SNProtoContent) -> MessageRequestResponse? {
|
||||
guard let messageRequestResponseProto = proto.messageRequestResponse else { return nil }
|
||||
|
||||
let isApproved = messageRequestResponseProto.isApproved
|
||||
|
||||
return MessageRequestResponse(isApproved: isApproved)
|
||||
}
|
||||
|
||||
public override func toProto(using transaction: YapDatabaseReadWriteTransaction) -> SNProtoContent? {
|
||||
let messageRequestResponseProto = SNProtoMessageRequestResponse.builder(isApproved: isApproved)
|
||||
let contentProto = SNProtoContent.builder()
|
||||
|
||||
do {
|
||||
contentProto.setMessageRequestResponse(try messageRequestResponseProto.build())
|
||||
return try contentProto.build()
|
||||
} catch {
|
||||
SNLog("Couldn't construct unsend request proto from: \(self).")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Description
|
||||
|
||||
public override var description: String {
|
||||
"""
|
||||
MessageRequestResponse(
|
||||
isApproved: \(isApproved)
|
||||
)
|
||||
"""
|
||||
}
|
||||
}
|
|
@ -15,7 +15,8 @@ typedef NS_ENUM(NSInteger, TSInfoMessageType) {
|
|||
TSInfoMessageTypeGroupCurrentUserLeft,
|
||||
TSInfoMessageTypeDisappearingMessagesUpdate,
|
||||
TSInfoMessageTypeScreenshotNotification,
|
||||
TSInfoMessageTypeMediaSavedNotification
|
||||
TSInfoMessageTypeMediaSavedNotification,
|
||||
TSInfoMessageTypeMessageRequestAccepted
|
||||
};
|
||||
|
||||
@property (atomic, readonly) TSInfoMessageType messageType;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -229,6 +229,28 @@ struct SessionProtos_UnsendRequest {
|
|||
fileprivate var _author: String? = nil
|
||||
}
|
||||
|
||||
struct SessionProtos_MessageRequestResponse {
|
||||
// SwiftProtobuf.Message conformance is added in an extension below. See the
|
||||
// `Message` and `Message+*Additions` files in the SwiftProtobuf library for
|
||||
// methods supported on all messages.
|
||||
|
||||
/// @required
|
||||
var isApproved: Bool {
|
||||
get {return _isApproved ?? false}
|
||||
set {_isApproved = newValue}
|
||||
}
|
||||
/// Returns true if `isApproved` has been explicitly set.
|
||||
var hasIsApproved: Bool {return self._isApproved != nil}
|
||||
/// Clears the value of `isApproved`. Subsequent reads from it will return its default value.
|
||||
mutating func clearIsApproved() {self._isApproved = nil}
|
||||
|
||||
var unknownFields = SwiftProtobuf.UnknownStorage()
|
||||
|
||||
init() {}
|
||||
|
||||
fileprivate var _isApproved: Bool? = nil
|
||||
}
|
||||
|
||||
struct SessionProtos_Content {
|
||||
// SwiftProtobuf.Message conformance is added in an extension below. See the
|
||||
// `Message` and `Message+*Additions` files in the SwiftProtobuf library for
|
||||
|
@ -288,6 +310,15 @@ struct SessionProtos_Content {
|
|||
/// Clears the value of `unsendRequest`. Subsequent reads from it will return its default value.
|
||||
mutating func clearUnsendRequest() {self._unsendRequest = nil}
|
||||
|
||||
var messageRequestResponse: SessionProtos_MessageRequestResponse {
|
||||
get {return _messageRequestResponse ?? SessionProtos_MessageRequestResponse()}
|
||||
set {_messageRequestResponse = newValue}
|
||||
}
|
||||
/// Returns true if `messageRequestResponse` has been explicitly set.
|
||||
var hasMessageRequestResponse: Bool {return self._messageRequestResponse != nil}
|
||||
/// Clears the value of `messageRequestResponse`. Subsequent reads from it will return its default value.
|
||||
mutating func clearMessageRequestResponse() {self._messageRequestResponse = nil}
|
||||
|
||||
var unknownFields = SwiftProtobuf.UnknownStorage()
|
||||
|
||||
init() {}
|
||||
|
@ -298,6 +329,7 @@ struct SessionProtos_Content {
|
|||
fileprivate var _configurationMessage: SessionProtos_ConfigurationMessage? = nil
|
||||
fileprivate var _dataExtractionNotification: SessionProtos_DataExtractionNotification? = nil
|
||||
fileprivate var _unsendRequest: SessionProtos_UnsendRequest? = nil
|
||||
fileprivate var _messageRequestResponse: SessionProtos_MessageRequestResponse? = nil
|
||||
}
|
||||
|
||||
struct SessionProtos_KeyPair {
|
||||
|
@ -1076,6 +1108,36 @@ struct SessionProtos_ConfigurationMessage {
|
|||
/// Clears the value of `profileKey`. Subsequent reads from it will return its default value.
|
||||
mutating func clearProfileKey() {self._profileKey = nil}
|
||||
|
||||
/// added for msg requests
|
||||
var isApproved: Bool {
|
||||
get {return _isApproved ?? false}
|
||||
set {_isApproved = newValue}
|
||||
}
|
||||
/// Returns true if `isApproved` has been explicitly set.
|
||||
var hasIsApproved: Bool {return self._isApproved != nil}
|
||||
/// Clears the value of `isApproved`. Subsequent reads from it will return its default value.
|
||||
mutating func clearIsApproved() {self._isApproved = nil}
|
||||
|
||||
/// added for msg requests
|
||||
var isBlocked: Bool {
|
||||
get {return _isBlocked ?? false}
|
||||
set {_isBlocked = newValue}
|
||||
}
|
||||
/// Returns true if `isBlocked` has been explicitly set.
|
||||
var hasIsBlocked: Bool {return self._isBlocked != nil}
|
||||
/// Clears the value of `isBlocked`. Subsequent reads from it will return its default value.
|
||||
mutating func clearIsBlocked() {self._isBlocked = nil}
|
||||
|
||||
/// added for msg requests
|
||||
var didApproveMe: Bool {
|
||||
get {return _didApproveMe ?? false}
|
||||
set {_didApproveMe = newValue}
|
||||
}
|
||||
/// Returns true if `didApproveMe` has been explicitly set.
|
||||
var hasDidApproveMe: Bool {return self._didApproveMe != nil}
|
||||
/// Clears the value of `didApproveMe`. Subsequent reads from it will return its default value.
|
||||
mutating func clearDidApproveMe() {self._didApproveMe = nil}
|
||||
|
||||
var unknownFields = SwiftProtobuf.UnknownStorage()
|
||||
|
||||
init() {}
|
||||
|
@ -1084,6 +1146,9 @@ struct SessionProtos_ConfigurationMessage {
|
|||
fileprivate var _name: String? = nil
|
||||
fileprivate var _profilePicture: String? = nil
|
||||
fileprivate var _profileKey: Data? = nil
|
||||
fileprivate var _isApproved: Bool? = nil
|
||||
fileprivate var _isBlocked: Bool? = nil
|
||||
fileprivate var _didApproveMe: Bool? = nil
|
||||
}
|
||||
|
||||
init() {}
|
||||
|
@ -1456,24 +1521,28 @@ extension SessionProtos_Envelope: SwiftProtobuf.Message, SwiftProtobuf._MessageI
|
|||
}
|
||||
|
||||
func traverse<V: SwiftProtobuf.Visitor>(visitor: inout V) throws {
|
||||
if let v = self._type {
|
||||
// The use of inline closures is to circumvent an issue where the compiler
|
||||
// allocates stack space for every if/case branch local when no optimizations
|
||||
// are enabled. https://github.com/apple/swift-protobuf/issues/1034 and
|
||||
// https://github.com/apple/swift-protobuf/issues/1182
|
||||
try { if let v = self._type {
|
||||
try visitor.visitSingularEnumField(value: v, fieldNumber: 1)
|
||||
}
|
||||
if let v = self._source {
|
||||
} }()
|
||||
try { if let v = self._source {
|
||||
try visitor.visitSingularStringField(value: v, fieldNumber: 2)
|
||||
}
|
||||
if let v = self._timestamp {
|
||||
} }()
|
||||
try { if let v = self._timestamp {
|
||||
try visitor.visitSingularUInt64Field(value: v, fieldNumber: 5)
|
||||
}
|
||||
if let v = self._sourceDevice {
|
||||
} }()
|
||||
try { if let v = self._sourceDevice {
|
||||
try visitor.visitSingularUInt32Field(value: v, fieldNumber: 7)
|
||||
}
|
||||
if let v = self._content {
|
||||
} }()
|
||||
try { if let v = self._content {
|
||||
try visitor.visitSingularBytesField(value: v, fieldNumber: 8)
|
||||
}
|
||||
if let v = self._serverTimestamp {
|
||||
} }()
|
||||
try { if let v = self._serverTimestamp {
|
||||
try visitor.visitSingularUInt64Field(value: v, fieldNumber: 10)
|
||||
}
|
||||
} }()
|
||||
try unknownFields.traverse(visitor: &visitor)
|
||||
}
|
||||
|
||||
|
@ -1523,12 +1592,16 @@ extension SessionProtos_TypingMessage: SwiftProtobuf.Message, SwiftProtobuf._Mes
|
|||
}
|
||||
|
||||
func traverse<V: SwiftProtobuf.Visitor>(visitor: inout V) throws {
|
||||
if let v = self._timestamp {
|
||||
// The use of inline closures is to circumvent an issue where the compiler
|
||||
// allocates stack space for every if/case branch local when no optimizations
|
||||
// are enabled. https://github.com/apple/swift-protobuf/issues/1034 and
|
||||
// https://github.com/apple/swift-protobuf/issues/1182
|
||||
try { if let v = self._timestamp {
|
||||
try visitor.visitSingularUInt64Field(value: v, fieldNumber: 1)
|
||||
}
|
||||
if let v = self._action {
|
||||
} }()
|
||||
try { if let v = self._action {
|
||||
try visitor.visitSingularEnumField(value: v, fieldNumber: 2)
|
||||
}
|
||||
} }()
|
||||
try unknownFields.traverse(visitor: &visitor)
|
||||
}
|
||||
|
||||
|
@ -1574,12 +1647,16 @@ extension SessionProtos_UnsendRequest: SwiftProtobuf.Message, SwiftProtobuf._Mes
|
|||
}
|
||||
|
||||
func traverse<V: SwiftProtobuf.Visitor>(visitor: inout V) throws {
|
||||
if let v = self._timestamp {
|
||||
// The use of inline closures is to circumvent an issue where the compiler
|
||||
// allocates stack space for every if/case branch local when no optimizations
|
||||
// are enabled. https://github.com/apple/swift-protobuf/issues/1034 and
|
||||
// https://github.com/apple/swift-protobuf/issues/1182
|
||||
try { if let v = self._timestamp {
|
||||
try visitor.visitSingularUInt64Field(value: v, fieldNumber: 1)
|
||||
}
|
||||
if let v = self._author {
|
||||
} }()
|
||||
try { if let v = self._author {
|
||||
try visitor.visitSingularStringField(value: v, fieldNumber: 2)
|
||||
}
|
||||
} }()
|
||||
try unknownFields.traverse(visitor: &visitor)
|
||||
}
|
||||
|
||||
|
@ -1591,6 +1668,47 @@ extension SessionProtos_UnsendRequest: SwiftProtobuf.Message, SwiftProtobuf._Mes
|
|||
}
|
||||
}
|
||||
|
||||
extension SessionProtos_MessageRequestResponse: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding {
|
||||
static let protoMessageName: String = _protobuf_package + ".MessageRequestResponse"
|
||||
static let _protobuf_nameMap: SwiftProtobuf._NameMap = [
|
||||
1: .same(proto: "isApproved"),
|
||||
]
|
||||
|
||||
public var isInitialized: Bool {
|
||||
if self._isApproved == nil {return false}
|
||||
return true
|
||||
}
|
||||
|
||||
mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
|
||||
while let fieldNumber = try decoder.nextFieldNumber() {
|
||||
// The use of inline closures is to circumvent an issue where the compiler
|
||||
// allocates stack space for every case branch when no optimizations are
|
||||
// enabled. https://github.com/apple/swift-protobuf/issues/1034
|
||||
switch fieldNumber {
|
||||
case 1: try { try decoder.decodeSingularBoolField(value: &self._isApproved) }()
|
||||
default: break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func traverse<V: SwiftProtobuf.Visitor>(visitor: inout V) throws {
|
||||
// The use of inline closures is to circumvent an issue where the compiler
|
||||
// allocates stack space for every if/case branch local when no optimizations
|
||||
// are enabled. https://github.com/apple/swift-protobuf/issues/1034 and
|
||||
// https://github.com/apple/swift-protobuf/issues/1182
|
||||
try { if let v = self._isApproved {
|
||||
try visitor.visitSingularBoolField(value: v, fieldNumber: 1)
|
||||
} }()
|
||||
try unknownFields.traverse(visitor: &visitor)
|
||||
}
|
||||
|
||||
static func ==(lhs: SessionProtos_MessageRequestResponse, rhs: SessionProtos_MessageRequestResponse) -> Bool {
|
||||
if lhs._isApproved != rhs._isApproved {return false}
|
||||
if lhs.unknownFields != rhs.unknownFields {return false}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
extension SessionProtos_Content: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding {
|
||||
static let protoMessageName: String = _protobuf_package + ".Content"
|
||||
static let _protobuf_nameMap: SwiftProtobuf._NameMap = [
|
||||
|
@ -1600,6 +1718,7 @@ extension SessionProtos_Content: SwiftProtobuf.Message, SwiftProtobuf._MessageIm
|
|||
7: .same(proto: "configurationMessage"),
|
||||
8: .same(proto: "dataExtractionNotification"),
|
||||
9: .same(proto: "unsendRequest"),
|
||||
10: .same(proto: "messageRequestResponse"),
|
||||
]
|
||||
|
||||
public var isInitialized: Bool {
|
||||
|
@ -1609,6 +1728,7 @@ extension SessionProtos_Content: SwiftProtobuf.Message, SwiftProtobuf._MessageIm
|
|||
if let v = self._configurationMessage, !v.isInitialized {return false}
|
||||
if let v = self._dataExtractionNotification, !v.isInitialized {return false}
|
||||
if let v = self._unsendRequest, !v.isInitialized {return false}
|
||||
if let v = self._messageRequestResponse, !v.isInitialized {return false}
|
||||
return true
|
||||
}
|
||||
|
||||
|
@ -1624,30 +1744,38 @@ extension SessionProtos_Content: SwiftProtobuf.Message, SwiftProtobuf._MessageIm
|
|||
case 7: try { try decoder.decodeSingularMessageField(value: &self._configurationMessage) }()
|
||||
case 8: try { try decoder.decodeSingularMessageField(value: &self._dataExtractionNotification) }()
|
||||
case 9: try { try decoder.decodeSingularMessageField(value: &self._unsendRequest) }()
|
||||
case 10: try { try decoder.decodeSingularMessageField(value: &self._messageRequestResponse) }()
|
||||
default: break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func traverse<V: SwiftProtobuf.Visitor>(visitor: inout V) throws {
|
||||
if let v = self._dataMessage {
|
||||
// The use of inline closures is to circumvent an issue where the compiler
|
||||
// allocates stack space for every if/case branch local when no optimizations
|
||||
// are enabled. https://github.com/apple/swift-protobuf/issues/1034 and
|
||||
// https://github.com/apple/swift-protobuf/issues/1182
|
||||
try { if let v = self._dataMessage {
|
||||
try visitor.visitSingularMessageField(value: v, fieldNumber: 1)
|
||||
}
|
||||
if let v = self._receiptMessage {
|
||||
} }()
|
||||
try { if let v = self._receiptMessage {
|
||||
try visitor.visitSingularMessageField(value: v, fieldNumber: 5)
|
||||
}
|
||||
if let v = self._typingMessage {
|
||||
} }()
|
||||
try { if let v = self._typingMessage {
|
||||
try visitor.visitSingularMessageField(value: v, fieldNumber: 6)
|
||||
}
|
||||
if let v = self._configurationMessage {
|
||||
} }()
|
||||
try { if let v = self._configurationMessage {
|
||||
try visitor.visitSingularMessageField(value: v, fieldNumber: 7)
|
||||
}
|
||||
if let v = self._dataExtractionNotification {
|
||||
} }()
|
||||
try { if let v = self._dataExtractionNotification {
|
||||
try visitor.visitSingularMessageField(value: v, fieldNumber: 8)
|
||||
}
|
||||
if let v = self._unsendRequest {
|
||||
} }()
|
||||
try { if let v = self._unsendRequest {
|
||||
try visitor.visitSingularMessageField(value: v, fieldNumber: 9)
|
||||
}
|
||||
} }()
|
||||
try { if let v = self._messageRequestResponse {
|
||||
try visitor.visitSingularMessageField(value: v, fieldNumber: 10)
|
||||
} }()
|
||||
try unknownFields.traverse(visitor: &visitor)
|
||||
}
|
||||
|
||||
|
@ -1658,6 +1786,7 @@ extension SessionProtos_Content: SwiftProtobuf.Message, SwiftProtobuf._MessageIm
|
|||
if lhs._configurationMessage != rhs._configurationMessage {return false}
|
||||
if lhs._dataExtractionNotification != rhs._dataExtractionNotification {return false}
|
||||
if lhs._unsendRequest != rhs._unsendRequest {return false}
|
||||
if lhs._messageRequestResponse != rhs._messageRequestResponse {return false}
|
||||
if lhs.unknownFields != rhs.unknownFields {return false}
|
||||
return true
|
||||
}
|
||||
|
@ -1690,12 +1819,16 @@ extension SessionProtos_KeyPair: SwiftProtobuf.Message, SwiftProtobuf._MessageIm
|
|||
}
|
||||
|
||||
func traverse<V: SwiftProtobuf.Visitor>(visitor: inout V) throws {
|
||||
if let v = self._publicKey {
|
||||
// The use of inline closures is to circumvent an issue where the compiler
|
||||
// allocates stack space for every if/case branch local when no optimizations
|
||||
// are enabled. https://github.com/apple/swift-protobuf/issues/1034 and
|
||||
// https://github.com/apple/swift-protobuf/issues/1182
|
||||
try { if let v = self._publicKey {
|
||||
try visitor.visitSingularBytesField(value: v, fieldNumber: 1)
|
||||
}
|
||||
if let v = self._privateKey {
|
||||
} }()
|
||||
try { if let v = self._privateKey {
|
||||
try visitor.visitSingularBytesField(value: v, fieldNumber: 2)
|
||||
}
|
||||
} }()
|
||||
try unknownFields.traverse(visitor: &visitor)
|
||||
}
|
||||
|
||||
|
@ -1733,12 +1866,16 @@ extension SessionProtos_DataExtractionNotification: SwiftProtobuf.Message, Swift
|
|||
}
|
||||
|
||||
func traverse<V: SwiftProtobuf.Visitor>(visitor: inout V) throws {
|
||||
if let v = self._type {
|
||||
// The use of inline closures is to circumvent an issue where the compiler
|
||||
// allocates stack space for every if/case branch local when no optimizations
|
||||
// are enabled. https://github.com/apple/swift-protobuf/issues/1034 and
|
||||
// https://github.com/apple/swift-protobuf/issues/1182
|
||||
try { if let v = self._type {
|
||||
try visitor.visitSingularEnumField(value: v, fieldNumber: 1)
|
||||
}
|
||||
if let v = self._timestamp {
|
||||
} }()
|
||||
try { if let v = self._timestamp {
|
||||
try visitor.visitSingularUInt64Field(value: v, fieldNumber: 2)
|
||||
}
|
||||
} }()
|
||||
try unknownFields.traverse(visitor: &visitor)
|
||||
}
|
||||
|
||||
|
@ -1859,45 +1996,49 @@ extension SessionProtos_DataMessage: SwiftProtobuf.Message, SwiftProtobuf._Messa
|
|||
|
||||
func traverse<V: SwiftProtobuf.Visitor>(visitor: inout V) throws {
|
||||
try withExtendedLifetime(_storage) { (_storage: _StorageClass) in
|
||||
if let v = _storage._body {
|
||||
// The use of inline closures is to circumvent an issue where the compiler
|
||||
// allocates stack space for every if/case branch local when no optimizations
|
||||
// are enabled. https://github.com/apple/swift-protobuf/issues/1034 and
|
||||
// https://github.com/apple/swift-protobuf/issues/1182
|
||||
try { if let v = _storage._body {
|
||||
try visitor.visitSingularStringField(value: v, fieldNumber: 1)
|
||||
}
|
||||
} }()
|
||||
if !_storage._attachments.isEmpty {
|
||||
try visitor.visitRepeatedMessageField(value: _storage._attachments, fieldNumber: 2)
|
||||
}
|
||||
if let v = _storage._group {
|
||||
try { if let v = _storage._group {
|
||||
try visitor.visitSingularMessageField(value: v, fieldNumber: 3)
|
||||
}
|
||||
if let v = _storage._flags {
|
||||
} }()
|
||||
try { if let v = _storage._flags {
|
||||
try visitor.visitSingularUInt32Field(value: v, fieldNumber: 4)
|
||||
}
|
||||
if let v = _storage._expireTimer {
|
||||
} }()
|
||||
try { if let v = _storage._expireTimer {
|
||||
try visitor.visitSingularUInt32Field(value: v, fieldNumber: 5)
|
||||
}
|
||||
if let v = _storage._profileKey {
|
||||
} }()
|
||||
try { if let v = _storage._profileKey {
|
||||
try visitor.visitSingularBytesField(value: v, fieldNumber: 6)
|
||||
}
|
||||
if let v = _storage._timestamp {
|
||||
} }()
|
||||
try { if let v = _storage._timestamp {
|
||||
try visitor.visitSingularUInt64Field(value: v, fieldNumber: 7)
|
||||
}
|
||||
if let v = _storage._quote {
|
||||
} }()
|
||||
try { if let v = _storage._quote {
|
||||
try visitor.visitSingularMessageField(value: v, fieldNumber: 8)
|
||||
}
|
||||
} }()
|
||||
if !_storage._preview.isEmpty {
|
||||
try visitor.visitRepeatedMessageField(value: _storage._preview, fieldNumber: 10)
|
||||
}
|
||||
if let v = _storage._profile {
|
||||
try { if let v = _storage._profile {
|
||||
try visitor.visitSingularMessageField(value: v, fieldNumber: 101)
|
||||
}
|
||||
if let v = _storage._openGroupInvitation {
|
||||
} }()
|
||||
try { if let v = _storage._openGroupInvitation {
|
||||
try visitor.visitSingularMessageField(value: v, fieldNumber: 102)
|
||||
}
|
||||
if let v = _storage._closedGroupControlMessage {
|
||||
} }()
|
||||
try { if let v = _storage._closedGroupControlMessage {
|
||||
try visitor.visitSingularMessageField(value: v, fieldNumber: 104)
|
||||
}
|
||||
if let v = _storage._syncTarget {
|
||||
} }()
|
||||
try { if let v = _storage._syncTarget {
|
||||
try visitor.visitSingularStringField(value: v, fieldNumber: 105)
|
||||
}
|
||||
} }()
|
||||
}
|
||||
try unknownFields.traverse(visitor: &visitor)
|
||||
}
|
||||
|
@ -1967,15 +2108,19 @@ extension SessionProtos_DataMessage.Quote: SwiftProtobuf.Message, SwiftProtobuf.
|
|||
}
|
||||
|
||||
func traverse<V: SwiftProtobuf.Visitor>(visitor: inout V) throws {
|
||||
if let v = self._id {
|
||||
// The use of inline closures is to circumvent an issue where the compiler
|
||||
// allocates stack space for every if/case branch local when no optimizations
|
||||
// are enabled. https://github.com/apple/swift-protobuf/issues/1034 and
|
||||
// https://github.com/apple/swift-protobuf/issues/1182
|
||||
try { if let v = self._id {
|
||||
try visitor.visitSingularUInt64Field(value: v, fieldNumber: 1)
|
||||
}
|
||||
if let v = self._author {
|
||||
} }()
|
||||
try { if let v = self._author {
|
||||
try visitor.visitSingularStringField(value: v, fieldNumber: 2)
|
||||
}
|
||||
if let v = self._text {
|
||||
} }()
|
||||
try { if let v = self._text {
|
||||
try visitor.visitSingularStringField(value: v, fieldNumber: 3)
|
||||
}
|
||||
} }()
|
||||
if !self.attachments.isEmpty {
|
||||
try visitor.visitRepeatedMessageField(value: self.attachments, fieldNumber: 4)
|
||||
}
|
||||
|
@ -2022,18 +2167,22 @@ extension SessionProtos_DataMessage.Quote.QuotedAttachment: SwiftProtobuf.Messag
|
|||
}
|
||||
|
||||
func traverse<V: SwiftProtobuf.Visitor>(visitor: inout V) throws {
|
||||
if let v = self._contentType {
|
||||
// The use of inline closures is to circumvent an issue where the compiler
|
||||
// allocates stack space for every if/case branch local when no optimizations
|
||||
// are enabled. https://github.com/apple/swift-protobuf/issues/1034 and
|
||||
// https://github.com/apple/swift-protobuf/issues/1182
|
||||
try { if let v = self._contentType {
|
||||
try visitor.visitSingularStringField(value: v, fieldNumber: 1)
|
||||
}
|
||||
if let v = self._fileName {
|
||||
} }()
|
||||
try { if let v = self._fileName {
|
||||
try visitor.visitSingularStringField(value: v, fieldNumber: 2)
|
||||
}
|
||||
if let v = self._thumbnail {
|
||||
} }()
|
||||
try { if let v = self._thumbnail {
|
||||
try visitor.visitSingularMessageField(value: v, fieldNumber: 3)
|
||||
}
|
||||
if let v = self._flags {
|
||||
} }()
|
||||
try { if let v = self._flags {
|
||||
try visitor.visitSingularUInt32Field(value: v, fieldNumber: 4)
|
||||
}
|
||||
} }()
|
||||
try unknownFields.traverse(visitor: &visitor)
|
||||
}
|
||||
|
||||
|
@ -2082,15 +2231,19 @@ extension SessionProtos_DataMessage.Preview: SwiftProtobuf.Message, SwiftProtobu
|
|||
}
|
||||
|
||||
func traverse<V: SwiftProtobuf.Visitor>(visitor: inout V) throws {
|
||||
if let v = self._url {
|
||||
// The use of inline closures is to circumvent an issue where the compiler
|
||||
// allocates stack space for every if/case branch local when no optimizations
|
||||
// are enabled. https://github.com/apple/swift-protobuf/issues/1034 and
|
||||
// https://github.com/apple/swift-protobuf/issues/1182
|
||||
try { if let v = self._url {
|
||||
try visitor.visitSingularStringField(value: v, fieldNumber: 1)
|
||||
}
|
||||
if let v = self._title {
|
||||
} }()
|
||||
try { if let v = self._title {
|
||||
try visitor.visitSingularStringField(value: v, fieldNumber: 2)
|
||||
}
|
||||
if let v = self._image {
|
||||
} }()
|
||||
try { if let v = self._image {
|
||||
try visitor.visitSingularMessageField(value: v, fieldNumber: 3)
|
||||
}
|
||||
} }()
|
||||
try unknownFields.traverse(visitor: &visitor)
|
||||
}
|
||||
|
||||
|
@ -2124,12 +2277,16 @@ extension SessionProtos_DataMessage.LokiProfile: SwiftProtobuf.Message, SwiftPro
|
|||
}
|
||||
|
||||
func traverse<V: SwiftProtobuf.Visitor>(visitor: inout V) throws {
|
||||
if let v = self._displayName {
|
||||
// The use of inline closures is to circumvent an issue where the compiler
|
||||
// allocates stack space for every if/case branch local when no optimizations
|
||||
// are enabled. https://github.com/apple/swift-protobuf/issues/1034 and
|
||||
// https://github.com/apple/swift-protobuf/issues/1182
|
||||
try { if let v = self._displayName {
|
||||
try visitor.visitSingularStringField(value: v, fieldNumber: 1)
|
||||
}
|
||||
if let v = self._profilePicture {
|
||||
} }()
|
||||
try { if let v = self._profilePicture {
|
||||
try visitor.visitSingularStringField(value: v, fieldNumber: 2)
|
||||
}
|
||||
} }()
|
||||
try unknownFields.traverse(visitor: &visitor)
|
||||
}
|
||||
|
||||
|
@ -2168,12 +2325,16 @@ extension SessionProtos_DataMessage.OpenGroupInvitation: SwiftProtobuf.Message,
|
|||
}
|
||||
|
||||
func traverse<V: SwiftProtobuf.Visitor>(visitor: inout V) throws {
|
||||
if let v = self._url {
|
||||
// The use of inline closures is to circumvent an issue where the compiler
|
||||
// allocates stack space for every if/case branch local when no optimizations
|
||||
// are enabled. https://github.com/apple/swift-protobuf/issues/1034 and
|
||||
// https://github.com/apple/swift-protobuf/issues/1182
|
||||
try { if let v = self._url {
|
||||
try visitor.visitSingularStringField(value: v, fieldNumber: 1)
|
||||
}
|
||||
if let v = self._name {
|
||||
} }()
|
||||
try { if let v = self._name {
|
||||
try visitor.visitSingularStringField(value: v, fieldNumber: 3)
|
||||
}
|
||||
} }()
|
||||
try unknownFields.traverse(visitor: &visitor)
|
||||
}
|
||||
|
||||
|
@ -2225,18 +2386,22 @@ extension SessionProtos_DataMessage.ClosedGroupControlMessage: SwiftProtobuf.Mes
|
|||
}
|
||||
|
||||
func traverse<V: SwiftProtobuf.Visitor>(visitor: inout V) throws {
|
||||
if let v = self._type {
|
||||
// The use of inline closures is to circumvent an issue where the compiler
|
||||
// allocates stack space for every if/case branch local when no optimizations
|
||||
// are enabled. https://github.com/apple/swift-protobuf/issues/1034 and
|
||||
// https://github.com/apple/swift-protobuf/issues/1182
|
||||
try { if let v = self._type {
|
||||
try visitor.visitSingularEnumField(value: v, fieldNumber: 1)
|
||||
}
|
||||
if let v = self._publicKey {
|
||||
} }()
|
||||
try { if let v = self._publicKey {
|
||||
try visitor.visitSingularBytesField(value: v, fieldNumber: 2)
|
||||
}
|
||||
if let v = self._name {
|
||||
} }()
|
||||
try { if let v = self._name {
|
||||
try visitor.visitSingularStringField(value: v, fieldNumber: 3)
|
||||
}
|
||||
if let v = self._encryptionKeyPair {
|
||||
} }()
|
||||
try { if let v = self._encryptionKeyPair {
|
||||
try visitor.visitSingularMessageField(value: v, fieldNumber: 4)
|
||||
}
|
||||
} }()
|
||||
if !self.members.isEmpty {
|
||||
try visitor.visitRepeatedBytesField(value: self.members, fieldNumber: 5)
|
||||
}
|
||||
|
@ -2246,9 +2411,9 @@ extension SessionProtos_DataMessage.ClosedGroupControlMessage: SwiftProtobuf.Mes
|
|||
if !self.wrappers.isEmpty {
|
||||
try visitor.visitRepeatedMessageField(value: self.wrappers, fieldNumber: 7)
|
||||
}
|
||||
if let v = self._expirationTimer {
|
||||
try { if let v = self._expirationTimer {
|
||||
try visitor.visitSingularUInt32Field(value: v, fieldNumber: 8)
|
||||
}
|
||||
} }()
|
||||
try unknownFields.traverse(visitor: &visitor)
|
||||
}
|
||||
|
||||
|
@ -2305,12 +2470,16 @@ extension SessionProtos_DataMessage.ClosedGroupControlMessage.KeyPairWrapper: Sw
|
|||
}
|
||||
|
||||
func traverse<V: SwiftProtobuf.Visitor>(visitor: inout V) throws {
|
||||
if let v = self._publicKey {
|
||||
// The use of inline closures is to circumvent an issue where the compiler
|
||||
// allocates stack space for every if/case branch local when no optimizations
|
||||
// are enabled. https://github.com/apple/swift-protobuf/issues/1034 and
|
||||
// https://github.com/apple/swift-protobuf/issues/1182
|
||||
try { if let v = self._publicKey {
|
||||
try visitor.visitSingularBytesField(value: v, fieldNumber: 1)
|
||||
}
|
||||
if let v = self._encryptedKeyPair {
|
||||
} }()
|
||||
try { if let v = self._encryptedKeyPair {
|
||||
try visitor.visitSingularBytesField(value: v, fieldNumber: 2)
|
||||
}
|
||||
} }()
|
||||
try unknownFields.traverse(visitor: &visitor)
|
||||
}
|
||||
|
||||
|
@ -2357,21 +2526,25 @@ extension SessionProtos_ConfigurationMessage: SwiftProtobuf.Message, SwiftProtob
|
|||
}
|
||||
|
||||
func traverse<V: SwiftProtobuf.Visitor>(visitor: inout V) throws {
|
||||
// The use of inline closures is to circumvent an issue where the compiler
|
||||
// allocates stack space for every if/case branch local when no optimizations
|
||||
// are enabled. https://github.com/apple/swift-protobuf/issues/1034 and
|
||||
// https://github.com/apple/swift-protobuf/issues/1182
|
||||
if !self.closedGroups.isEmpty {
|
||||
try visitor.visitRepeatedMessageField(value: self.closedGroups, fieldNumber: 1)
|
||||
}
|
||||
if !self.openGroups.isEmpty {
|
||||
try visitor.visitRepeatedStringField(value: self.openGroups, fieldNumber: 2)
|
||||
}
|
||||
if let v = self._displayName {
|
||||
try { if let v = self._displayName {
|
||||
try visitor.visitSingularStringField(value: v, fieldNumber: 3)
|
||||
}
|
||||
if let v = self._profilePicture {
|
||||
} }()
|
||||
try { if let v = self._profilePicture {
|
||||
try visitor.visitSingularStringField(value: v, fieldNumber: 4)
|
||||
}
|
||||
if let v = self._profileKey {
|
||||
} }()
|
||||
try { if let v = self._profileKey {
|
||||
try visitor.visitSingularBytesField(value: v, fieldNumber: 5)
|
||||
}
|
||||
} }()
|
||||
if !self.contacts.isEmpty {
|
||||
try visitor.visitRepeatedMessageField(value: self.contacts, fieldNumber: 6)
|
||||
}
|
||||
|
@ -2424,24 +2597,28 @@ extension SessionProtos_ConfigurationMessage.ClosedGroup: SwiftProtobuf.Message,
|
|||
}
|
||||
|
||||
func traverse<V: SwiftProtobuf.Visitor>(visitor: inout V) throws {
|
||||
if let v = self._publicKey {
|
||||
// The use of inline closures is to circumvent an issue where the compiler
|
||||
// allocates stack space for every if/case branch local when no optimizations
|
||||
// are enabled. https://github.com/apple/swift-protobuf/issues/1034 and
|
||||
// https://github.com/apple/swift-protobuf/issues/1182
|
||||
try { if let v = self._publicKey {
|
||||
try visitor.visitSingularBytesField(value: v, fieldNumber: 1)
|
||||
}
|
||||
if let v = self._name {
|
||||
} }()
|
||||
try { if let v = self._name {
|
||||
try visitor.visitSingularStringField(value: v, fieldNumber: 2)
|
||||
}
|
||||
if let v = self._encryptionKeyPair {
|
||||
} }()
|
||||
try { if let v = self._encryptionKeyPair {
|
||||
try visitor.visitSingularMessageField(value: v, fieldNumber: 3)
|
||||
}
|
||||
} }()
|
||||
if !self.members.isEmpty {
|
||||
try visitor.visitRepeatedBytesField(value: self.members, fieldNumber: 4)
|
||||
}
|
||||
if !self.admins.isEmpty {
|
||||
try visitor.visitRepeatedBytesField(value: self.admins, fieldNumber: 5)
|
||||
}
|
||||
if let v = self._expirationTimer {
|
||||
try { if let v = self._expirationTimer {
|
||||
try visitor.visitSingularUInt32Field(value: v, fieldNumber: 6)
|
||||
}
|
||||
} }()
|
||||
try unknownFields.traverse(visitor: &visitor)
|
||||
}
|
||||
|
||||
|
@ -2464,6 +2641,9 @@ extension SessionProtos_ConfigurationMessage.Contact: SwiftProtobuf.Message, Swi
|
|||
2: .same(proto: "name"),
|
||||
3: .same(proto: "profilePicture"),
|
||||
4: .same(proto: "profileKey"),
|
||||
5: .same(proto: "isApproved"),
|
||||
6: .same(proto: "isBlocked"),
|
||||
7: .same(proto: "didApproveMe"),
|
||||
]
|
||||
|
||||
public var isInitialized: Bool {
|
||||
|
@ -2482,24 +2662,40 @@ extension SessionProtos_ConfigurationMessage.Contact: SwiftProtobuf.Message, Swi
|
|||
case 2: try { try decoder.decodeSingularStringField(value: &self._name) }()
|
||||
case 3: try { try decoder.decodeSingularStringField(value: &self._profilePicture) }()
|
||||
case 4: try { try decoder.decodeSingularBytesField(value: &self._profileKey) }()
|
||||
case 5: try { try decoder.decodeSingularBoolField(value: &self._isApproved) }()
|
||||
case 6: try { try decoder.decodeSingularBoolField(value: &self._isBlocked) }()
|
||||
case 7: try { try decoder.decodeSingularBoolField(value: &self._didApproveMe) }()
|
||||
default: break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func traverse<V: SwiftProtobuf.Visitor>(visitor: inout V) throws {
|
||||
if let v = self._publicKey {
|
||||
// The use of inline closures is to circumvent an issue where the compiler
|
||||
// allocates stack space for every if/case branch local when no optimizations
|
||||
// are enabled. https://github.com/apple/swift-protobuf/issues/1034 and
|
||||
// https://github.com/apple/swift-protobuf/issues/1182
|
||||
try { if let v = self._publicKey {
|
||||
try visitor.visitSingularBytesField(value: v, fieldNumber: 1)
|
||||
}
|
||||
if let v = self._name {
|
||||
} }()
|
||||
try { if let v = self._name {
|
||||
try visitor.visitSingularStringField(value: v, fieldNumber: 2)
|
||||
}
|
||||
if let v = self._profilePicture {
|
||||
} }()
|
||||
try { if let v = self._profilePicture {
|
||||
try visitor.visitSingularStringField(value: v, fieldNumber: 3)
|
||||
}
|
||||
if let v = self._profileKey {
|
||||
} }()
|
||||
try { if let v = self._profileKey {
|
||||
try visitor.visitSingularBytesField(value: v, fieldNumber: 4)
|
||||
}
|
||||
} }()
|
||||
try { if let v = self._isApproved {
|
||||
try visitor.visitSingularBoolField(value: v, fieldNumber: 5)
|
||||
} }()
|
||||
try { if let v = self._isBlocked {
|
||||
try visitor.visitSingularBoolField(value: v, fieldNumber: 6)
|
||||
} }()
|
||||
try { if let v = self._didApproveMe {
|
||||
try visitor.visitSingularBoolField(value: v, fieldNumber: 7)
|
||||
} }()
|
||||
try unknownFields.traverse(visitor: &visitor)
|
||||
}
|
||||
|
||||
|
@ -2508,6 +2704,9 @@ extension SessionProtos_ConfigurationMessage.Contact: SwiftProtobuf.Message, Swi
|
|||
if lhs._name != rhs._name {return false}
|
||||
if lhs._profilePicture != rhs._profilePicture {return false}
|
||||
if lhs._profileKey != rhs._profileKey {return false}
|
||||
if lhs._isApproved != rhs._isApproved {return false}
|
||||
if lhs._isBlocked != rhs._isBlocked {return false}
|
||||
if lhs._didApproveMe != rhs._didApproveMe {return false}
|
||||
if lhs.unknownFields != rhs.unknownFields {return false}
|
||||
return true
|
||||
}
|
||||
|
@ -2539,9 +2738,13 @@ extension SessionProtos_ReceiptMessage: SwiftProtobuf.Message, SwiftProtobuf._Me
|
|||
}
|
||||
|
||||
func traverse<V: SwiftProtobuf.Visitor>(visitor: inout V) throws {
|
||||
if let v = self._type {
|
||||
// The use of inline closures is to circumvent an issue where the compiler
|
||||
// allocates stack space for every if/case branch local when no optimizations
|
||||
// are enabled. https://github.com/apple/swift-protobuf/issues/1034 and
|
||||
// https://github.com/apple/swift-protobuf/issues/1182
|
||||
try { if let v = self._type {
|
||||
try visitor.visitSingularEnumField(value: v, fieldNumber: 1)
|
||||
}
|
||||
} }()
|
||||
if !self.timestamp.isEmpty {
|
||||
try visitor.visitRepeatedUInt64Field(value: self.timestamp, fieldNumber: 2)
|
||||
}
|
||||
|
@ -2609,42 +2812,46 @@ extension SessionProtos_AttachmentPointer: SwiftProtobuf.Message, SwiftProtobuf.
|
|||
}
|
||||
|
||||
func traverse<V: SwiftProtobuf.Visitor>(visitor: inout V) throws {
|
||||
if let v = self._id {
|
||||
// The use of inline closures is to circumvent an issue where the compiler
|
||||
// allocates stack space for every if/case branch local when no optimizations
|
||||
// are enabled. https://github.com/apple/swift-protobuf/issues/1034 and
|
||||
// https://github.com/apple/swift-protobuf/issues/1182
|
||||
try { if let v = self._id {
|
||||
try visitor.visitSingularFixed64Field(value: v, fieldNumber: 1)
|
||||
}
|
||||
if let v = self._contentType {
|
||||
} }()
|
||||
try { if let v = self._contentType {
|
||||
try visitor.visitSingularStringField(value: v, fieldNumber: 2)
|
||||
}
|
||||
if let v = self._key {
|
||||
} }()
|
||||
try { if let v = self._key {
|
||||
try visitor.visitSingularBytesField(value: v, fieldNumber: 3)
|
||||
}
|
||||
if let v = self._size {
|
||||
} }()
|
||||
try { if let v = self._size {
|
||||
try visitor.visitSingularUInt32Field(value: v, fieldNumber: 4)
|
||||
}
|
||||
if let v = self._thumbnail {
|
||||
} }()
|
||||
try { if let v = self._thumbnail {
|
||||
try visitor.visitSingularBytesField(value: v, fieldNumber: 5)
|
||||
}
|
||||
if let v = self._digest {
|
||||
} }()
|
||||
try { if let v = self._digest {
|
||||
try visitor.visitSingularBytesField(value: v, fieldNumber: 6)
|
||||
}
|
||||
if let v = self._fileName {
|
||||
} }()
|
||||
try { if let v = self._fileName {
|
||||
try visitor.visitSingularStringField(value: v, fieldNumber: 7)
|
||||
}
|
||||
if let v = self._flags {
|
||||
} }()
|
||||
try { if let v = self._flags {
|
||||
try visitor.visitSingularUInt32Field(value: v, fieldNumber: 8)
|
||||
}
|
||||
if let v = self._width {
|
||||
} }()
|
||||
try { if let v = self._width {
|
||||
try visitor.visitSingularUInt32Field(value: v, fieldNumber: 9)
|
||||
}
|
||||
if let v = self._height {
|
||||
} }()
|
||||
try { if let v = self._height {
|
||||
try visitor.visitSingularUInt32Field(value: v, fieldNumber: 10)
|
||||
}
|
||||
if let v = self._caption {
|
||||
} }()
|
||||
try { if let v = self._caption {
|
||||
try visitor.visitSingularStringField(value: v, fieldNumber: 11)
|
||||
}
|
||||
if let v = self._url {
|
||||
} }()
|
||||
try { if let v = self._url {
|
||||
try visitor.visitSingularStringField(value: v, fieldNumber: 101)
|
||||
}
|
||||
} }()
|
||||
try unknownFields.traverse(visitor: &visitor)
|
||||
}
|
||||
|
||||
|
@ -2741,21 +2948,25 @@ extension SessionProtos_GroupContext: SwiftProtobuf.Message, SwiftProtobuf._Mess
|
|||
|
||||
func traverse<V: SwiftProtobuf.Visitor>(visitor: inout V) throws {
|
||||
try withExtendedLifetime(_storage) { (_storage: _StorageClass) in
|
||||
if let v = _storage._id {
|
||||
// The use of inline closures is to circumvent an issue where the compiler
|
||||
// allocates stack space for every if/case branch local when no optimizations
|
||||
// are enabled. https://github.com/apple/swift-protobuf/issues/1034 and
|
||||
// https://github.com/apple/swift-protobuf/issues/1182
|
||||
try { if let v = _storage._id {
|
||||
try visitor.visitSingularBytesField(value: v, fieldNumber: 1)
|
||||
}
|
||||
if let v = _storage._type {
|
||||
} }()
|
||||
try { if let v = _storage._type {
|
||||
try visitor.visitSingularEnumField(value: v, fieldNumber: 2)
|
||||
}
|
||||
if let v = _storage._name {
|
||||
} }()
|
||||
try { if let v = _storage._name {
|
||||
try visitor.visitSingularStringField(value: v, fieldNumber: 3)
|
||||
}
|
||||
} }()
|
||||
if !_storage._members.isEmpty {
|
||||
try visitor.visitRepeatedStringField(value: _storage._members, fieldNumber: 4)
|
||||
}
|
||||
if let v = _storage._avatar {
|
||||
try { if let v = _storage._avatar {
|
||||
try visitor.visitSingularMessageField(value: v, fieldNumber: 5)
|
||||
}
|
||||
} }()
|
||||
if !_storage._admins.isEmpty {
|
||||
try visitor.visitRepeatedStringField(value: _storage._admins, fieldNumber: 6)
|
||||
}
|
||||
|
|
|
@ -249,18 +249,22 @@ extension WebSocketProtos_WebSocketRequestMessage: SwiftProtobuf.Message, SwiftP
|
|||
}
|
||||
|
||||
func traverse<V: SwiftProtobuf.Visitor>(visitor: inout V) throws {
|
||||
if let v = self._verb {
|
||||
// The use of inline closures is to circumvent an issue where the compiler
|
||||
// allocates stack space for every if/case branch local when no optimizations
|
||||
// are enabled. https://github.com/apple/swift-protobuf/issues/1034 and
|
||||
// https://github.com/apple/swift-protobuf/issues/1182
|
||||
try { if let v = self._verb {
|
||||
try visitor.visitSingularStringField(value: v, fieldNumber: 1)
|
||||
}
|
||||
if let v = self._path {
|
||||
} }()
|
||||
try { if let v = self._path {
|
||||
try visitor.visitSingularStringField(value: v, fieldNumber: 2)
|
||||
}
|
||||
if let v = self._body {
|
||||
} }()
|
||||
try { if let v = self._body {
|
||||
try visitor.visitSingularBytesField(value: v, fieldNumber: 3)
|
||||
}
|
||||
if let v = self._requestID {
|
||||
} }()
|
||||
try { if let v = self._requestID {
|
||||
try visitor.visitSingularUInt64Field(value: v, fieldNumber: 4)
|
||||
}
|
||||
} }()
|
||||
if !self.headers.isEmpty {
|
||||
try visitor.visitRepeatedStringField(value: self.headers, fieldNumber: 5)
|
||||
}
|
||||
|
@ -305,18 +309,22 @@ extension WebSocketProtos_WebSocketResponseMessage: SwiftProtobuf.Message, Swift
|
|||
}
|
||||
|
||||
func traverse<V: SwiftProtobuf.Visitor>(visitor: inout V) throws {
|
||||
if let v = self._requestID {
|
||||
// The use of inline closures is to circumvent an issue where the compiler
|
||||
// allocates stack space for every if/case branch local when no optimizations
|
||||
// are enabled. https://github.com/apple/swift-protobuf/issues/1034 and
|
||||
// https://github.com/apple/swift-protobuf/issues/1182
|
||||
try { if let v = self._requestID {
|
||||
try visitor.visitSingularUInt64Field(value: v, fieldNumber: 1)
|
||||
}
|
||||
if let v = self._status {
|
||||
} }()
|
||||
try { if let v = self._status {
|
||||
try visitor.visitSingularUInt32Field(value: v, fieldNumber: 2)
|
||||
}
|
||||
if let v = self._message {
|
||||
} }()
|
||||
try { if let v = self._message {
|
||||
try visitor.visitSingularStringField(value: v, fieldNumber: 3)
|
||||
}
|
||||
if let v = self._body {
|
||||
} }()
|
||||
try { if let v = self._body {
|
||||
try visitor.visitSingularBytesField(value: v, fieldNumber: 4)
|
||||
}
|
||||
} }()
|
||||
if !self.headers.isEmpty {
|
||||
try visitor.visitRepeatedStringField(value: self.headers, fieldNumber: 5)
|
||||
}
|
||||
|
@ -357,15 +365,19 @@ extension WebSocketProtos_WebSocketMessage: SwiftProtobuf.Message, SwiftProtobuf
|
|||
}
|
||||
|
||||
func traverse<V: SwiftProtobuf.Visitor>(visitor: inout V) throws {
|
||||
if let v = self._type {
|
||||
// The use of inline closures is to circumvent an issue where the compiler
|
||||
// allocates stack space for every if/case branch local when no optimizations
|
||||
// are enabled. https://github.com/apple/swift-protobuf/issues/1034 and
|
||||
// https://github.com/apple/swift-protobuf/issues/1182
|
||||
try { if let v = self._type {
|
||||
try visitor.visitSingularEnumField(value: v, fieldNumber: 1)
|
||||
}
|
||||
if let v = self._request {
|
||||
} }()
|
||||
try { if let v = self._request {
|
||||
try visitor.visitSingularMessageField(value: v, fieldNumber: 2)
|
||||
}
|
||||
if let v = self._response {
|
||||
} }()
|
||||
try { if let v = self._response {
|
||||
try visitor.visitSingularMessageField(value: v, fieldNumber: 3)
|
||||
}
|
||||
} }()
|
||||
try unknownFields.traverse(visitor: &visitor)
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -12,6 +12,7 @@
|
|||
#import "TSGroupThread.h"
|
||||
#import "YapDatabaseConnection+OWS.h"
|
||||
#import <SessionMessagingKit/SessionMessagingKit-Swift.h>
|
||||
#import <SessionUtilitiesKit/SessionUtilitiesKit-Swift.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
|
@ -250,6 +251,26 @@ NSString *const kOWSBlockingManager_SyncedBlockedGroupIdsKey = @"kOWSBlockingMan
|
|||
forKey:kOWSBlockingManager_BlockedGroupMapKey
|
||||
inCollection:kOWSBlockingManager_BlockListCollection];
|
||||
|
||||
// Update the contact blocked state (so sync'ing won't be busted)
|
||||
NSMutableArray<SNContact *> *contactsToUpdate = [[NSMutableArray alloc] init];
|
||||
|
||||
[[[LKStorage shared] getAllContacts] enumerateObjectsUsingBlock:^(SNContact * _Nonnull obj, BOOL * _Nonnull stop) {
|
||||
// If the blocked flag doesn't match then add it to the array to be saved
|
||||
BOOL contactInBlockedList = [blockedPhoneNumbers containsObject:obj.sessionID];
|
||||
|
||||
if (obj.isBlocked != contactInBlockedList) {
|
||||
obj.isBlocked = contactInBlockedList;
|
||||
[contactsToUpdate addObject:obj];
|
||||
}
|
||||
}];
|
||||
|
||||
if ([contactsToUpdate count] > 0) {
|
||||
[LKStorage writeWithBlock:^(YapDatabaseReadWriteTransaction * _Nonnull transaction) {
|
||||
[contactsToUpdate enumerateObjectsUsingBlock:^(SNContact * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
|
||||
[[LKStorage shared] setContact:obj usingTransaction:transaction];
|
||||
}];
|
||||
}];
|
||||
}
|
||||
|
||||
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
||||
if (sendSyncMessage) {
|
||||
|
|
|
@ -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] || 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)
|
||||
|
||||
// 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 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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}()
|
||||
|
|
|
@ -86,7 +86,7 @@ public class TypingIndicatorsImpl : NSObject, TypingIndicators {
|
|||
|
||||
@objc
|
||||
public func didStartTypingOutgoingInput(inThread thread: TSThread) {
|
||||
guard let outgoingIndicators = ensureOutgoingIndicators(forThread: thread) else {
|
||||
guard let outgoingIndicators = ensureOutgoingIndicators(forThread: thread), !thread.isMessageRequest() else {
|
||||
return
|
||||
}
|
||||
outgoingIndicators.didStartTypingOutgoingInput()
|
||||
|
@ -94,7 +94,7 @@ public class TypingIndicatorsImpl : NSObject, TypingIndicators {
|
|||
|
||||
@objc
|
||||
public func didStopTypingOutgoingInput(inThread thread: TSThread) {
|
||||
guard let outgoingIndicators = ensureOutgoingIndicators(forThread: thread) else {
|
||||
guard let outgoingIndicators = ensureOutgoingIndicators(forThread: thread), !thread.isMessageRequest() else {
|
||||
return
|
||||
}
|
||||
outgoingIndicators.didStopTypingOutgoingInput()
|
||||
|
|
|
@ -18,6 +18,7 @@ public protocol SessionMessagingKitStorageProtocol {
|
|||
func getUserED25519KeyPair() -> Box.KeyPair?
|
||||
func getUser() -> Contact?
|
||||
func getAllContacts() -> Set<Contact>
|
||||
func getAllContacts(with transaction: YapDatabaseReadTransaction) -> Set<Contact>
|
||||
|
||||
// MARK: - Closed Groups
|
||||
|
||||
|
|
|
@ -57,6 +57,32 @@ NSString *const TSContactThreadPrefix = @"c";
|
|||
return @[ self.contactSessionID ];
|
||||
}
|
||||
|
||||
- (BOOL)isMessageRequest {
|
||||
NSString *sessionID = self.contactSessionID;
|
||||
SNContact *contact = [LKStorage.shared getContactWithSessionID:sessionID];
|
||||
|
||||
return (
|
||||
self.shouldBeVisible &&
|
||||
!self.isNoteToSelf && (
|
||||
contact == nil ||
|
||||
!contact.isApproved
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
- (BOOL)isMessageRequestUsingTransaction:(YapDatabaseReadTransaction *)transaction {
|
||||
NSString *sessionID = self.contactSessionID;
|
||||
SNContact *contact = [LKStorage.shared getContactWithSessionID:sessionID using:transaction];
|
||||
|
||||
return (
|
||||
self.shouldBeVisible &&
|
||||
!self.isNoteToSelf && (
|
||||
contact == nil ||
|
||||
!contact.isApproved
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
- (BOOL)isGroupThread
|
||||
{
|
||||
return NO;
|
||||
|
|
|
@ -48,6 +48,14 @@ BOOL IsNoteToSelfEnabled(void);
|
|||
|
||||
- (BOOL)isNoteToSelf;
|
||||
|
||||
/**
|
||||
* Whether the thread is a message request.
|
||||
*
|
||||
* @return YES if the combination of thread and contact approval means this thread should appear in the message requests section, NO otherwise.
|
||||
*/
|
||||
- (BOOL)isMessageRequest;
|
||||
- (BOOL)isMessageRequestUsingTransaction:(YapDatabaseReadTransaction *)transaction;
|
||||
|
||||
#pragma mark Interactions
|
||||
|
||||
- (void)enumerateInteractionsWithTransaction:(YapDatabaseReadTransaction *)transaction usingBlock:(void (^)(TSInteraction *interaction, BOOL *stop))block;
|
||||
|
|
|
@ -132,6 +132,16 @@ BOOL IsNoteToSelfEnabled(void)
|
|||
return [self.contactSessionID isEqual:[SNGeneralUtilities getUserPublicKey]];
|
||||
}
|
||||
|
||||
// Override in ContactThread
|
||||
- (BOOL)isMessageRequest {
|
||||
return NO;
|
||||
}
|
||||
|
||||
// Override in ContactThread
|
||||
- (BOOL)isMessageRequestUsingTransaction:(YapDatabaseReadTransaction *)transaction {
|
||||
return NO;
|
||||
}
|
||||
|
||||
#pragma mark To be subclassed.
|
||||
|
||||
- (BOOL)isGroupThread {
|
||||
|
|
|
@ -6,9 +6,25 @@ import UserNotifications
|
|||
public class NSENotificationPresenter: NSObject, NotificationsProtocol {
|
||||
|
||||
public func notifyUser(for incomingMessage: TSIncomingMessage, in thread: TSThread, transaction: YapDatabaseReadTransaction) {
|
||||
guard !thread.isMuted else {
|
||||
// Ignore PNs if the thread is muted
|
||||
return
|
||||
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 threads = transaction.ext(TSThreadDatabaseViewExtensionName) as! YapDatabaseViewTransaction
|
||||
let numMessageRequests = threads.numberOfItems(inGroup: TSMessageRequestGroup)
|
||||
|
||||
// Allow this to show a notification if there are no message requests (ie. this is the first one)
|
||||
guard numMessageRequests <= 1 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 senderPublicKey = incomingMessage.authorId
|
||||
|
@ -37,7 +53,6 @@ public class NSENotificationPresenter: NSObject, NotificationsProtocol {
|
|||
notificationTitle = String(format: NotificationStrings.incomingGroupMessageTitleFormat, senderName, groupName)
|
||||
}
|
||||
|
||||
let threadID = thread.uniqueId!
|
||||
let snippet = incomingMessage.previewText(with: transaction).filterForDisplay?.replacingMentions(for: threadID, using: transaction)
|
||||
?? "APN_Message".localized()
|
||||
|
||||
|
@ -47,12 +62,13 @@ public class NSENotificationPresenter: NSObject, NotificationsProtocol {
|
|||
let notificationContent = UNMutableNotificationContent()
|
||||
notificationContent.userInfo = userInfo
|
||||
notificationContent.sound = OWSSounds.notificationSound(for: thread).notificationSound(isQuiet: false)
|
||||
if let sharedUserDefaults = UserDefaults(suiteName: "group.com.loki-project.loki-messenger") {
|
||||
let newBadgeNumber = sharedUserDefaults.integer(forKey: "currentBadgeNumber") + 1
|
||||
notificationContent.badge = NSNumber(value: newBadgeNumber)
|
||||
sharedUserDefaults.set(newBadgeNumber, forKey: "currentBadgeNumber")
|
||||
}
|
||||
|
||||
// Badge Number
|
||||
let newBadgeNumber = CurrentAppContext().appUserDefaults().integer(forKey: "currentBadgeNumber") + 1
|
||||
notificationContent.badge = NSNumber(value: newBadgeNumber)
|
||||
CurrentAppContext().appUserDefaults().set(newBadgeNumber, forKey: "currentBadgeNumber")
|
||||
|
||||
// Title & body
|
||||
let notificationsPreference = Environment.shared.preferences!.notificationPreviewType()
|
||||
switch notificationsPreference {
|
||||
case .namePreview:
|
||||
|
@ -67,6 +83,13 @@ public class NSENotificationPresenter: NSObject, NotificationsProtocol {
|
|||
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 thread.isMessageRequest() {
|
||||
notificationContent.body = "MESSAGE_REQUESTS_NOTIFICATION".localized()
|
||||
}
|
||||
|
||||
// Add request
|
||||
let identifier = incomingMessage.notificationIdentifier ?? UUID().uuidString
|
||||
let request = UNNotificationRequest(identifier: identifier, content: notificationContent, trigger: nil)
|
||||
SNLog("Add remote notification request")
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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")! }
|
||||
}
|
||||
|
|
|
@ -0,0 +1,38 @@
|
|||
{
|
||||
"colors" : [
|
||||
{
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0x58",
|
||||
"green" : "0x58",
|
||||
"red" : "0x58"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "dark"
|
||||
}
|
||||
],
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0x43",
|
||||
"green" : "0x43",
|
||||
"red" : "0x43"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
{
|
||||
"colors" : [
|
||||
{
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0xFF",
|
||||
"green" : "0xFF",
|
||||
"red" : "0xFF"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "dark"
|
||||
}
|
||||
],
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0xAD",
|
||||
"green" : "0xAD",
|
||||
"red" : "0xAD"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
{
|
||||
"colors" : [
|
||||
{
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0x9F",
|
||||
"green" : "0x9F",
|
||||
"red" : "0x9F"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
|
|
|
@ -96,6 +96,25 @@ public extension UIView {
|
|||
return constraint
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func set(_ dimension: Dimension, to otherDimension: Dimension, of view: UIView, withOffset offset: CGFloat = 0) -> NSLayoutConstraint {
|
||||
translatesAutoresizingMaskIntoConstraints = false
|
||||
let otherAnchor: NSLayoutAnchor<NSLayoutDimension> = {
|
||||
switch otherDimension {
|
||||
case .width: return view.widthAnchor
|
||||
case .height: return view.heightAnchor
|
||||
}
|
||||
}()
|
||||
let constraint: NSLayoutConstraint = {
|
||||
switch dimension {
|
||||
case .width: return widthAnchor.constraint(equalTo: otherAnchor, constant: offset)
|
||||
case .height: return heightAnchor.constraint(equalTo: otherAnchor, constant: offset)
|
||||
}
|
||||
}()
|
||||
constraint.isActive = true
|
||||
return constraint
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func set(_ dimension: Dimension, greaterThanOrEqualTo size: CGFloat) -> NSLayoutConstraint {
|
||||
translatesAutoresizingMaskIntoConstraints = false
|
||||
|
|
|
@ -7,6 +7,7 @@ public enum SNUserDefaults {
|
|||
case hasViewedSeed
|
||||
case hasSeenLinkPreviewSuggestion
|
||||
case isUsingFullAPNs
|
||||
case hasHiddenMessageRequests
|
||||
}
|
||||
|
||||
public enum Date : Swift.String {
|
||||
|
|
|
@ -0,0 +1,56 @@
|
|||
@objc(SNMessageRequestsMigration)
|
||||
public class MessageRequestsMigration : OWSDatabaseMigration {
|
||||
|
||||
@objc
|
||||
class func migrationId() -> String {
|
||||
return "002"
|
||||
}
|
||||
|
||||
override public func runUp(completion: @escaping OWSDatabaseMigrationCompletion) {
|
||||
self.doMigrationAsync(completion: completion)
|
||||
}
|
||||
|
||||
private func doMigrationAsync(completion: @escaping OWSDatabaseMigrationCompletion) {
|
||||
var contacts: Set<Contact> = Set()
|
||||
var threads: [TSThread] = []
|
||||
|
||||
TSThread.enumerateCollectionObjects { object, _ in
|
||||
guard let thread: TSThread = object as? TSThread else { return }
|
||||
|
||||
if let contactThread: TSContactThread = thread as? TSContactThread {
|
||||
let sessionId: String = contactThread.contactSessionID()
|
||||
|
||||
if let contact: Contact = Storage.shared.getContact(with: sessionId) {
|
||||
contact.isApproved = true
|
||||
contact.didApproveMe = true
|
||||
contacts.insert(contact)
|
||||
}
|
||||
}
|
||||
else if let groupThread: TSGroupThread = thread as? TSGroupThread, groupThread.isClosedGroup {
|
||||
let groupAdmins: [String] = groupThread.groupModel.groupAdminIds
|
||||
|
||||
groupAdmins.forEach { sessionId in
|
||||
if let contact: Contact = Storage.shared.getContact(with: sessionId) {
|
||||
contact.isApproved = true
|
||||
contact.didApproveMe = true
|
||||
contacts.insert(contact)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
threads.append(thread)
|
||||
}
|
||||
|
||||
Storage.write(with: { transaction in
|
||||
contacts.forEach { contact in
|
||||
Storage.shared.setContact(contact, using: transaction)
|
||||
}
|
||||
threads.forEach { thread in
|
||||
thread.save(with: transaction)
|
||||
}
|
||||
self.save(with: transaction) // Intentionally capture self
|
||||
}, completion: {
|
||||
completion()
|
||||
})
|
||||
}
|
||||
}
|
|
@ -26,8 +26,9 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
- (NSArray<OWSDatabaseMigration *> *)allMigrations
|
||||
{
|
||||
return @[
|
||||
[SNContactsMigration new],
|
||||
[SNUnreadMentionMigration new]
|
||||
[SNUnreadMentionMigration new],
|
||||
[SNMessageRequestsMigration new],
|
||||
[SNContactsMigration new]
|
||||
];
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -1,52 +0,0 @@
|
|||
|
||||
extension ConfigurationMessage {
|
||||
|
||||
public static func getCurrent() -> ConfigurationMessage? {
|
||||
let storage = Storage.shared
|
||||
guard let user = storage.getUser() else { return nil }
|
||||
let displayName = user.name
|
||||
let profilePictureURL = user.profilePictureURL
|
||||
let profileKey = user.profileEncryptionKey?.keyData
|
||||
var closedGroups: Set<ClosedGroup> = []
|
||||
var openGroups: Set<String> = []
|
||||
var contacts: Set<Contact> = []
|
||||
var contactCount = 0
|
||||
Storage.read { transaction in
|
||||
TSGroupThread.enumerateCollectionObjects(with: transaction) { object, _ in
|
||||
guard let thread = object as? TSGroupThread else { return }
|
||||
switch thread.groupModel.groupType {
|
||||
case .closedGroup:
|
||||
guard thread.isCurrentUserMemberInGroup() else { return }
|
||||
let groupID = thread.groupModel.groupId
|
||||
let groupPublicKey = LKGroupUtilities.getDecodedGroupID(groupID)
|
||||
guard storage.isClosedGroup(groupPublicKey),
|
||||
let encryptionKeyPair = storage.getLatestClosedGroupEncryptionKeyPair(for: groupPublicKey) else { return }
|
||||
let closedGroup = ClosedGroup(publicKey: groupPublicKey, name: thread.groupModel.groupName!, encryptionKeyPair: encryptionKeyPair,
|
||||
members: Set(thread.groupModel.groupMemberIds), admins: Set(thread.groupModel.groupAdminIds), expirationTimer: thread.disappearingMessagesDuration(with: transaction))
|
||||
closedGroups.insert(closedGroup)
|
||||
case .openGroup:
|
||||
if let v2OpenGroup = storage.getV2OpenGroup(for: thread.uniqueId!) {
|
||||
openGroups.insert("\(v2OpenGroup.server)/\(v2OpenGroup.room)?public_key=\(v2OpenGroup.publicKey)")
|
||||
}
|
||||
default: break
|
||||
}
|
||||
}
|
||||
var truncatedContacts = storage.getAllContacts()
|
||||
if truncatedContacts.count > 200 { truncatedContacts = Set(Array(truncatedContacts)[0..<200]) }
|
||||
truncatedContacts.forEach { contact in
|
||||
let publicKey = contact.sessionID
|
||||
let threadID = TSContactThread.threadID(fromContactSessionID: publicKey)
|
||||
guard let thread = TSContactThread.fetch(uniqueId: threadID, transaction: transaction), thread.shouldBeVisible
|
||||
&& !SSKEnvironment.shared.blockingManager.isRecipientIdBlocked(publicKey) else { return }
|
||||
let profilePictureURL = contact.profilePictureURL
|
||||
let profileKey = contact.profileEncryptionKey?.keyData
|
||||
let contact = ConfigurationMessage.Contact(publicKey: publicKey, displayName: contact.name ?? publicKey,
|
||||
profilePictureURL: profilePictureURL, profileKey: profileKey)
|
||||
contacts.insert(contact)
|
||||
contactCount += 1
|
||||
}
|
||||
}
|
||||
return ConfigurationMessage(displayName: displayName, profilePictureURL: profilePictureURL, profileKey: profileKey,
|
||||
closedGroups: closedGroups, openGroups: openGroups, contacts: contacts)
|
||||
}
|
||||
}
|
|
@ -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)]) {
|
||||
|
|
|
@ -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
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue