Merge branch 'dev' into feature/session-id-blinding-part-2

# Conflicts:
#	Session.xcodeproj/project.pbxproj
#	Session/Utilities/BackgroundPoller.swift
#	SessionMessagingKit/Database/Storage+OpenGroups.swift
#	SessionMessagingKit/Messages/Control Messages/ConfigurationMessage+Convenience.swift
#	SessionMessagingKit/Open Groups/OpenGroupAPIV2.swift
#	SessionMessagingKit/Open Groups/OpenGroupManagerV2.swift
#	SessionMessagingKit/Sending & Receiving/MessageSender.swift
#	SessionMessagingKit/Sending & Receiving/Pollers/OpenGroupPollerV2.swift
#	SessionMessagingKit/Sending & Receiving/Pollers/Poller.swift
#	SessionMessagingKit/Storage.swift
#	SessionMessagingKit/Utilities/General.swift
#	SessionSnodeKit/SnodeAPI.swift
#	SessionUtilitiesKit/General/Atomic.swift
pull/592/head
Morgan Pretty 2 years ago
commit 5ca227434b

@ -198,7 +198,6 @@
B8566C6C256F60F50045A0B9 /* OWSUserProfile.m in Sources */ = {isa = PBXBuildFile; fileRef = C38EF2D1255B6DAF007E1867 /* OWSUserProfile.m */; };
B8566C7D256F62030045A0B9 /* OWSUserProfile.h in Headers */ = {isa = PBXBuildFile; fileRef = C38EF2D3255B6DAF007E1867 /* OWSUserProfile.h */; settings = {ATTRIBUTES = (Public, ); }; };
B8569AC325CB5D2900DBA3DB /* ConversationVC+Interaction.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8569AC225CB5D2900DBA3DB /* ConversationVC+Interaction.swift */; };
B8569AD325CBA13D00DBA3DB /* MediaTextOverlayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8569AD225CBA13D00DBA3DB /* MediaTextOverlayView.swift */; };
B8569AE325CBB19A00DBA3DB /* DocumentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8569AE225CBB19A00DBA3DB /* DocumentView.swift */; };
B866CE112581C1A900535CC4 /* Sodium+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3E7134E251C867C009649BB /* Sodium+Utilities.swift */; };
B86BD08423399ACF000F5AE3 /* Modal.swift in Sources */ = {isa = PBXBuildFile; fileRef = B86BD08323399ACF000F5AE3 /* Modal.swift */; };
@ -242,9 +241,7 @@
B893063F2383961A005EAA8E /* ScanQRCodeWrapperVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = B893063E2383961A005EAA8E /* ScanQRCodeWrapperVC.swift */; };
B894D0752339EDCF00B4D94D /* NukeDataModal.swift in Sources */ = {isa = PBXBuildFile; fileRef = B894D0742339EDCF00B4D94D /* NukeDataModal.swift */; };
B897621C25D201F7004F83B2 /* ScrollToBottomButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = B897621B25D201F7004F83B2 /* ScrollToBottomButton.swift */; };
B8AE75A425A6C6A6001A84D2 /* Data+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8AE75A325A6C6A6001A84D2 /* Data+Utilities.swift */; };
B8AE760B25ABFB5A001A84D2 /* GeneralUtilities.m in Sources */ = {isa = PBXBuildFile; fileRef = B8AE760A25ABFB5A001A84D2 /* GeneralUtilities.m */; };
B8AE761425ABFBB9001A84D2 /* GeneralUtilities.h in Headers */ = {isa = PBXBuildFile; fileRef = B8AE760925ABFB00001A84D2 /* GeneralUtilities.h */; settings = {ATTRIBUTES = (Public, ); }; };
B8AE75A425A6C6A6001A84D2 /* Data+Trimming.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8AE75A325A6C6A6001A84D2 /* Data+Trimming.swift */; };
B8AF4BB426A5204600583500 /* SendSeedModal.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8AF4BB326A5204600583500 /* SendSeedModal.swift */; };
B8B32021258B1A650020074B /* Contact.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8B32020258B1A650020074B /* Contact.swift */; };
B8B32033258B235D0020074B /* Storage+Contacts.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8B32032258B235D0020074B /* Storage+Contacts.swift */; };
@ -797,6 +794,8 @@
FD3C906D27E43C4B00CD579F /* MessageSenderEncryptionSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD3C906C27E43C4B00CD579F /* MessageSenderEncryptionSpec.swift */; };
FD3C906F27E43E8700CD579F /* MockBox.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD3C906E27E43E8700CD579F /* MockBox.swift */; };
FD3C907127E445E500CD579F /* MessageReceiverDecryptionSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD3C907027E445E500CD579F /* MessageReceiverDecryptionSpec.swift */; };
FD3C907327E8387300CD579F /* OpenGroupServerIdLookupMigration.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD3C907227E8387300CD579F /* OpenGroupServerIdLookupMigration.swift */; };
FD3C907527E83AC200CD579F /* OpenGroupServerIdLookup.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD3C907427E83AC200CD579F /* OpenGroupServerIdLookup.swift */; };
FD5D200F27AA2B6000FEA984 /* MessageRequestResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD5D200E27AA2B6000FEA984 /* MessageRequestResponse.swift */; };
FD5D201127AA331F00FEA984 /* ConfigurationMessage+Convenience.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD5D201027AA331F00FEA984 /* ConfigurationMessage+Convenience.swift */; };
FD5D201E27B0D87C00FEA984 /* SessionId.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD5D201D27B0D87C00FEA984 /* SessionId.swift */; };
@ -891,6 +890,7 @@
FDC438CB27BB7DB100C60D73 /* UpdateMessageRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC438CA27BB7DB100C60D73 /* UpdateMessageRequest.swift */; };
FDC438CD27BC641200C60D73 /* Set+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC438CC27BC641200C60D73 /* Set+Utilities.swift */; };
FDC438CF27BCA45400C60D73 /* Server.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC438CE27BCA45400C60D73 /* Server.swift */; };
FDFD645827EC1F4000808CA1 /* Atomic.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDFD645727EC1F4000808CA1 /* Atomic.swift */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
@ -1341,7 +1341,6 @@
B8544E3223D50E4900299F14 /* SNAppearance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SNAppearance.swift; sourceTree = "<group>"; };
B8566C62256F55930045A0B9 /* OWSLinkPreview+Conversion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OWSLinkPreview+Conversion.swift"; sourceTree = "<group>"; };
B8569AC225CB5D2900DBA3DB /* ConversationVC+Interaction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ConversationVC+Interaction.swift"; sourceTree = "<group>"; };
B8569AD225CBA13D00DBA3DB /* MediaTextOverlayView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaTextOverlayView.swift; sourceTree = "<group>"; };
B8569AE225CBB19A00DBA3DB /* DocumentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DocumentView.swift; sourceTree = "<group>"; };
B86BD08323399ACF000F5AE3 /* Modal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Modal.swift; sourceTree = "<group>"; };
B86BD08523399CEF000F5AE3 /* SeedModal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SeedModal.swift; sourceTree = "<group>"; };
@ -1363,9 +1362,7 @@
B893063E2383961A005EAA8E /* ScanQRCodeWrapperVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScanQRCodeWrapperVC.swift; sourceTree = "<group>"; };
B894D0742339EDCF00B4D94D /* NukeDataModal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NukeDataModal.swift; sourceTree = "<group>"; };
B897621B25D201F7004F83B2 /* ScrollToBottomButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScrollToBottomButton.swift; sourceTree = "<group>"; };
B8AE75A325A6C6A6001A84D2 /* Data+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Data+Utilities.swift"; sourceTree = "<group>"; };
B8AE760925ABFB00001A84D2 /* GeneralUtilities.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneralUtilities.h; sourceTree = "<group>"; };
B8AE760A25ABFB5A001A84D2 /* GeneralUtilities.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = GeneralUtilities.m; sourceTree = "<group>"; };
B8AE75A325A6C6A6001A84D2 /* Data+Trimming.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Data+Trimming.swift"; sourceTree = "<group>"; };
B8AF4BB326A5204600583500 /* SendSeedModal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendSeedModal.swift; sourceTree = "<group>"; };
B8B32020258B1A650020074B /* Contact.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Contact.swift; sourceTree = "<group>"; };
B8B32032258B235D0020074B /* Storage+Contacts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Storage+Contacts.swift"; sourceTree = "<group>"; };
@ -1956,6 +1953,8 @@
FD3C906C27E43C4B00CD579F /* MessageSenderEncryptionSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageSenderEncryptionSpec.swift; sourceTree = "<group>"; };
FD3C906E27E43E8700CD579F /* MockBox.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockBox.swift; sourceTree = "<group>"; };
FD3C907027E445E500CD579F /* MessageReceiverDecryptionSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageReceiverDecryptionSpec.swift; sourceTree = "<group>"; };
FD3C907227E8387300CD579F /* OpenGroupServerIdLookupMigration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenGroupServerIdLookupMigration.swift; sourceTree = "<group>"; };
FD3C907427E83AC200CD579F /* OpenGroupServerIdLookup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenGroupServerIdLookup.swift; sourceTree = "<group>"; };
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>"; };
FD5D201D27B0D87C00FEA984 /* SessionId.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionId.swift; sourceTree = "<group>"; };
@ -2051,6 +2050,7 @@
FDC438CA27BB7DB100C60D73 /* UpdateMessageRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateMessageRequest.swift; sourceTree = "<group>"; };
FDC438CC27BC641200C60D73 /* Set+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Set+Utilities.swift"; sourceTree = "<group>"; };
FDC438CE27BCA45400C60D73 /* Server.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Server.swift; sourceTree = "<group>"; };
FDFD645727EC1F4000808CA1 /* Atomic.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Atomic.swift; 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>"; };
/* End PBXFileReference section */
@ -2416,7 +2416,6 @@
isa = PBXGroup;
children = (
34A8B3502190A40E00218A25 /* MediaAlbumView.swift */,
B8569AD225CBA13D00DBA3DB /* MediaTextOverlayView.swift */,
3488F9352191CC4000E524CC /* MediaView.swift */,
B8041A9425C8FA1D003C2166 /* MediaLoaderView.swift */,
B8F5F71925F1B35C003BF8D4 /* MediaPlaceholderView.swift */,
@ -2588,9 +2587,10 @@
C3C2A5D12553860800C340D1 /* Array+Utilities.swift */,
FDC4383D27B4708600C60D73 /* Atomic.swift */,
FDC438CC27BC641200C60D73 /* Set+Utilities.swift */,
FDFD645727EC1F4000808CA1 /* Atomic.swift */,
C33FDAA8255A57FF00E217F9 /* BuildConfiguration.swift */,
B8F5F58225EC94A6003BF8D4 /* Collection+Subscripting.swift */,
B8AE75A325A6C6A6001A84D2 /* Data+Utilities.swift */,
B8AE75A325A6C6A6001A84D2 /* Data+Trimming.swift */,
C3C2A5D52553860A00C340D1 /* Dictionary+Utilities.swift */,
B87EF18026377A1D00124B3C /* Features.swift */,
B8BC00BF257D90E30032E807 /* General.swift */,
@ -3315,6 +3315,7 @@
children = (
B8B32044258C117C0020074B /* ContactsMigration.swift */,
FD88BADA27A750F200BBC442 /* MessageRequestsMigration.swift */,
FD3C907227E8387300CD579F /* OpenGroupServerIdLookupMigration.swift */,
FD0BA51C27CDC34600CC6805 /* SOGSV4Migration.swift */,
C38EF271255B6D79007E1867 /* OWSDatabaseMigration.h */,
C38EF270255B6D79007E1867 /* OWSDatabaseMigration.m */,
@ -3446,6 +3447,7 @@
children = (
FDC4381827B34EAD00C60D73 /* Models */,
FDC4380727B31D3A00C60D73 /* Types */,
FD3C907427E83AC200CD579F /* OpenGroupServerIdLookup.swift */,
B88FA7B726045D100049422F /* OpenGroupAPI.swift */,
C3DB66AB260ACA42001EFC55 /* OpenGroupManager.swift */,
);
@ -3477,8 +3479,6 @@
C3BBE0C62554F1570050F1E3 /* FixedWidthInteger+BigEndian.swift */,
C33FDB7F255A581100E217F9 /* FullTextSearchFinder.swift */,
C33FDBC1255A581700E217F9 /* General.swift */,
B8AE760925ABFB00001A84D2 /* GeneralUtilities.h */,
B8AE760A25ABFB5A001A84D2 /* GeneralUtilities.m */,
B82A0C3726B9098200C1BCE3 /* MessageInvalidator.swift */,
C3A71D0A2558989C0043A11F /* MessageWrapper.swift */,
C3A71D4E25589FF30043A11F /* NSData+messagePadding.h */,
@ -4298,7 +4298,6 @@
C32C5BF8256DC8F6003C73A2 /* OWSDisappearingMessagesJob.h in Headers */,
C32C5AAA256DBE8F003C73A2 /* TSIncomingMessage.h in Headers */,
B8856D72256F1421001CE70E /* OWSWindowManager.h in Headers */,
B8AE761425ABFBB9001A84D2 /* GeneralUtilities.h in Headers */,
C32C5B6B256DC357003C73A2 /* OWSDisappearingConfigurationUpdateInfoMessage.h in Headers */,
C32C5BBA256DC7E3003C73A2 /* ProfileManagerProtocol.h in Headers */,
C3A3A193256E20D4004D228D /* SignalRecipient.h in Headers */,
@ -5188,6 +5187,7 @@
C38EF407255B6DF7007E1867 /* Toast.swift in Sources */,
C38EF38C255B6DD2007E1867 /* ApprovalRailCellView.swift in Sources */,
C38EF409255B6DF7007E1867 /* ContactTableViewCell.m in Sources */,
FD3C907327E8387300CD579F /* OpenGroupServerIdLookupMigration.swift in Sources */,
C38EF32A255B6DBF007E1867 /* UIUtil.m in Sources */,
C38EF335255B6DBF007E1867 /* BlockListCache.swift in Sources */,
C38EF2A6255B6D93007E1867 /* PlaceholderIcon.swift in Sources */,
@ -5339,12 +5339,13 @@
FDC438CD27BC641200C60D73 /* Set+Utilities.swift in Sources */,
C3D9E3C925676AF30040E4F3 /* TSYapDatabaseObject.m in Sources */,
FD705A92278D051200F16121 /* ReusableView.swift in Sources */,
B8AE75A425A6C6A6001A84D2 /* Data+Utilities.swift in Sources */,
B8AE75A425A6C6A6001A84D2 /* Data+Trimming.swift in Sources */,
B8856DE6256F15F2001CE70E /* String+SSK.swift in Sources */,
C3471ED42555386B00297E91 /* AESGCM.swift in Sources */,
C32C5DDB256DD9FF003C73A2 /* ContentProxy.swift in Sources */,
C3A71F892558BA9F0043A11F /* Mnemonic.swift in Sources */,
B8F5F58325EC94A6003BF8D4 /* Collection+Subscripting.swift in Sources */,
FDFD645827EC1F4000808CA1 /* Atomic.swift in Sources */,
C33FDEF8255A656D00E217F9 /* Promise+Delaying.swift in Sources */,
B8BC00C0257D90E30032E807 /* General.swift in Sources */,
FD078E4627E02406000769AF /* Atomic.swift in Sources */,
@ -5445,6 +5446,7 @@
FDC4386327B4D94E00C60D73 /* SOGSMessage.swift in Sources */,
C3C2A7712553A41E00C340D1 /* ControlMessage.swift in Sources */,
C32C5D19256DD493003C73A2 /* OWSLinkPreview.swift in Sources */,
FD3C907527E83AC200CD579F /* OpenGroupServerIdLookup.swift in Sources */,
C32C5CF0256DD3E4003C73A2 /* Storage+Shared.swift in Sources */,
C300A5BD2554B00D00555489 /* ReadReceipt.swift in Sources */,
FD83B9CC27D179BC005E1583 /* FSEndpoint.swift in Sources */,
@ -5453,7 +5455,6 @@
C32C5E15256DDC78003C73A2 /* SSKPreferences.swift in Sources */,
FDC438C727BB6DF000C60D73 /* DirectMessage.swift in Sources */,
C32C5D9C256DD6DC003C73A2 /* OWSOutgoingReceiptManager.m in Sources */,
B8AE760B25ABFB5A001A84D2 /* GeneralUtilities.m in Sources */,
C32C5C4F256DCC36003C73A2 /* Storage+OpenGroups.swift in Sources */,
FDC4384F27B4804F00C60D73 /* Header.swift in Sources */,
C3DA9C0725AE7396008F7C7E /* ConfigurationMessage.swift in Sources */,
@ -5640,7 +5641,6 @@
341341EF2187467A00192D59 /* ConversationViewModel.m in Sources */,
4C21D5D8223AC60F00EF8A77 /* PhotoCapture.swift in Sources */,
C331FFF32558FF0300070591 /* PathStatusView.swift in Sources */,
B8569AD325CBA13D00DBA3DB /* MediaTextOverlayView.swift in Sources */,
4CC1ECFB211A553000CC13BE /* AppUpdateNag.swift in Sources */,
B848A4C5269EAAA200617031 /* UserDetailsSheet.swift in Sources */,
34B6A903218B3F63007C4606 /* TypingIndicatorView.swift in Sources */,
@ -5951,7 +5951,7 @@
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CODE_SIGN_STYLE = Automatic;
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 327;
CURRENT_PROJECT_VERSION = 332;
DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = SUQ8J2PCT7;
FRAMEWORK_SEARCH_PATHS = "$(inherited)";
@ -5976,7 +5976,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 1.11.22;
MARKETING_VERSION = 1.11.24;
MTL_ENABLE_DEBUG_INFO = YES;
PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger.ShareExtension";
PRODUCT_NAME = "$(TARGET_NAME)";
@ -6024,7 +6024,7 @@
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CODE_SIGN_STYLE = Automatic;
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 327;
CURRENT_PROJECT_VERSION = 332;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = SUQ8J2PCT7;
ENABLE_NS_ASSERTIONS = NO;
@ -6054,7 +6054,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 1.11.22;
MARKETING_VERSION = 1.11.24;
MTL_ENABLE_DEBUG_INFO = NO;
PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger.ShareExtension";
PRODUCT_NAME = "$(TARGET_NAME)";
@ -6090,7 +6090,7 @@
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CODE_SIGN_STYLE = Automatic;
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 327;
CURRENT_PROJECT_VERSION = 332;
DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = SUQ8J2PCT7;
FRAMEWORK_SEARCH_PATHS = "$(inherited)";
@ -6113,7 +6113,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 1.11.22;
MARKETING_VERSION = 1.11.24;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger.NotificationServiceExtension";
@ -6164,7 +6164,7 @@
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CODE_SIGN_STYLE = Automatic;
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 327;
CURRENT_PROJECT_VERSION = 332;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = SUQ8J2PCT7;
ENABLE_NS_ASSERTIONS = NO;
@ -6192,7 +6192,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 1.11.22;
MARKETING_VERSION = 1.11.24;
MTL_ENABLE_DEBUG_INFO = NO;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger.NotificationServiceExtension";
@ -7100,7 +7100,7 @@
CODE_SIGN_ENTITLEMENTS = Session/Meta/Signal.entitlements;
CODE_SIGN_IDENTITY = "iPhone Developer";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CURRENT_PROJECT_VERSION = 327;
CURRENT_PROJECT_VERSION = 332;
DEVELOPMENT_TEAM = SUQ8J2PCT7;
FRAMEWORK_SEARCH_PATHS = (
"$(inherited)",
@ -7139,7 +7139,7 @@
"$(SRCROOT)",
);
LLVM_LTO = NO;
MARKETING_VERSION = 1.11.22;
MARKETING_VERSION = 1.11.24;
OTHER_LDFLAGS = "$(inherited)";
OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\"";
PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger";
@ -7171,7 +7171,7 @@
CODE_SIGN_ENTITLEMENTS = Session/Meta/Signal.entitlements;
CODE_SIGN_IDENTITY = "iPhone Developer";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CURRENT_PROJECT_VERSION = 327;
CURRENT_PROJECT_VERSION = 332;
DEVELOPMENT_TEAM = SUQ8J2PCT7;
FRAMEWORK_SEARCH_PATHS = (
"$(inherited)",
@ -7210,7 +7210,7 @@
"$(SRCROOT)",
);
LLVM_LTO = NO;
MARKETING_VERSION = 1.11.22;
MARKETING_VERSION = 1.11.24;
OTHER_LDFLAGS = "$(inherited)";
PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger";
PRODUCT_NAME = Session;

@ -265,49 +265,46 @@ extension ConversationVC : InputViewDelegate, MessageCellDelegate, ContextMenuAc
let linkPreviewDraft = snInputView.linkPreviewInfo?.draft
let tsMessage = TSOutgoingMessage.from(message, associatedWith: thread)
Storage.write(with: { transaction in
let promise: Promise<Void> = self.approveMessageRequestIfNeeded(
for: self.thread,
with: transaction,
isNewThread: !oldThreadShouldBeVisible,
timestamp: (sentTimestamp - 1) // Set 1ms earlier as this is used for sorting
)
.map { [weak self] _ in
self?.viewModel.appendUnsavedOutgoingTextMessage(tsMessage)
Storage.write(with: { transaction in
message.linkPreview = VisibleMessage.LinkPreview.from(linkPreviewDraft, using: transaction)
}, completion: { [weak self] in
tsMessage.linkPreview = OWSLinkPreview.from(message.linkPreview)
Storage.shared.write(
with: { transaction in
tsMessage.save(with: transaction as! YapDatabaseReadWriteTransaction)
},
completion: { [weak self] in
// At this point the TSOutgoingMessage should have its link preview set, so we can scroll to the bottom knowing
// the height of the new message cell
self?.scrollToBottom(isAnimated: false)
}
)
Storage.shared.write { transaction in
MessageSender.send(message, with: [], in: thread, using: transaction as! YapDatabaseReadWriteTransaction)
let promise: Promise<Void> = self.approveMessageRequestIfNeeded(
for: self.thread,
isNewThread: !oldThreadShouldBeVisible,
timestamp: (sentTimestamp - 1) // Set 1ms earlier as this is used for sorting
)
.map { [weak self] _ in
self?.viewModel.appendUnsavedOutgoingTextMessage(tsMessage)
Storage.write(with: { transaction in
message.linkPreview = VisibleMessage.LinkPreview.from(linkPreviewDraft, using: transaction)
}, completion: { [weak self] in
tsMessage.linkPreview = OWSLinkPreview.from(message.linkPreview)
Storage.shared.write(
with: { transaction in
tsMessage.save(with: transaction as! YapDatabaseReadWriteTransaction)
},
completion: { [weak self] in
// At this point the TSOutgoingMessage should have its link preview set, so we can scroll to the bottom knowing
// the height of the new message cell
self?.scrollToBottom(isAnimated: false)
}
self?.handleMessageSent()
})
}
// Show an error indicating that approving the thread failed
promise.catch(on: DispatchQueue.main) { [weak self] _ in
let alert = UIAlertController(title: "Session", message: "An error occurred when trying to accept this message request", preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "OK", style: .default, handler: nil))
self?.present(alert, animated: true, completion: nil)
}
promise.retainUntilComplete()
})
)
Storage.shared.write { transaction in
MessageSender.send(message, with: [], in: thread, using: transaction as! YapDatabaseReadWriteTransaction)
}
self?.handleMessageSent()
})
}
// Show an error indicating that approving the thread failed
promise.catch(on: DispatchQueue.main) { [weak self] _ in
let alert = UIAlertController(title: "Session", message: "An error occurred when trying to accept this message request", preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "OK", style: .default, handler: nil))
self?.present(alert, animated: true, completion: nil)
}
promise.retainUntilComplete()
}
func sendAttachments(_ attachments: [SignalAttachment], with text: String, onComplete: (() -> ())? = nil) {
@ -331,44 +328,41 @@ extension ConversationVC : InputViewDelegate, MessageCellDelegate, ContextMenuAc
let oldThreadShouldBeVisible: Bool = thread.shouldBeVisible
let tsMessage = TSOutgoingMessage.from(message, associatedWith: thread)
Storage.write(with: { transaction in
let promise: Promise<Void> = self.approveMessageRequestIfNeeded(
for: self.thread,
with: transaction,
isNewThread: !oldThreadShouldBeVisible,
timestamp: (sentTimestamp - 1) // Set 1ms earlier as this is used for sorting
let promise: Promise<Void> = self.approveMessageRequestIfNeeded(
for: self.thread,
isNewThread: !oldThreadShouldBeVisible,
timestamp: (sentTimestamp - 1) // Set 1ms earlier as this is used for sorting
)
.map { [weak self] _ in
Storage.write(
with: { transaction in
tsMessage.save(with: transaction)
// The new message cell is inserted at this point, but the TSOutgoingMessage doesn't have its attachment yet
},
completion: { [weak self] in
Storage.write(with: { transaction in
MessageSender.send(message, with: attachments, in: thread, using: transaction)
}, completion: { [weak self] in
// At this point the TSOutgoingMessage should have its attachments set, so we can scroll to the bottom knowing
// the height of the new message cell
self?.scrollToBottom(isAnimated: false)
})
self?.handleMessageSent()
// Attachment successfully sent - dismiss the screen
onComplete?()
}
)
.map { [weak self] _ in
Storage.write(
with: { transaction in
tsMessage.save(with: transaction)
// The new message cell is inserted at this point, but the TSOutgoingMessage doesn't have its attachment yet
},
completion: { [weak self] in
Storage.write(with: { transaction in
MessageSender.send(message, with: attachments, in: thread, using: transaction)
}, completion: { [weak self] in
// At this point the TSOutgoingMessage should have its attachments set, so we can scroll to the bottom knowing
// the height of the new message cell
self?.scrollToBottom(isAnimated: false)
})
self?.handleMessageSent()
// Attachment successfully sent - dismiss the screen
onComplete?()
}
)
}
}
// Show an error indicating that approving the thread failed
promise.catch(on: DispatchQueue.main) { [weak self] _ in
let alert = UIAlertController(title: "Session", message: "An error occurred when trying to accept this message request", preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "OK", style: .default, handler: nil))
self?.present(alert, animated: true, completion: nil)
}
// Show an error indicating that approving the thread failed
promise.catch(on: DispatchQueue.main) { [weak self] _ in
let alert = UIAlertController(title: "Session", message: "An error occurred when trying to accept this message request", preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "OK", style: .default, handler: nil))
self?.present(alert, animated: true, completion: nil)
}
promise.retainUntilComplete()
})
promise.retainUntilComplete()
}
func handleMessageSent() {
@ -558,14 +552,7 @@ extension ConversationVC : InputViewDelegate, MessageCellDelegate, ContextMenuAc
} else {
guard let albumView = cell.albumView else { return }
let locationInCell = gestureRecognizer.location(in: cell)
// Figure out whether the "read more" button was tapped
if let overlayView = cell.mediaTextOverlayView {
let locationInOverlayView = cell.convert(locationInCell, to: overlayView)
if let readMoreButton = overlayView.readMoreButton, readMoreButton.frame.contains(locationInOverlayView) {
return showFullText(viewItem) // HACK: This is a dirty way to do this
}
}
// Otherwise, figure out which of the media views was tapped
// Figure out which of the media views was tapped
let locationInAlbumView = cell.convert(locationInCell, to: albumView)
guard let mediaView = albumView.mediaView(forLocation: locationInAlbumView) else { return }
if albumView.isMoreItemsView(mediaView: mediaView) && viewItem.mediaAlbumHasFailedAttachment() {
@ -604,10 +591,7 @@ extension ConversationVC : InputViewDelegate, MessageCellDelegate, ContextMenuAc
navigationController!.present(shareVC, animated: true, completion: nil)
}
case .textOnlyMessage:
if let preview = viewItem.linkPreview, let urlAsString = preview.urlString, let url = URL(string: urlAsString) {
// Open the link preview URL
openURL(url)
} else if let reply = viewItem.quotedReply {
if let reply = viewItem.quotedReply {
// Scroll to the source of the reply
guard let indexPath = viewModel.ensureLoadWindowContainsQuotedReply(reply) else { return }
messagesTableView.scrollToRow(at: indexPath, at: UITableView.ScrollPosition.middle, animated: true)
@ -1164,7 +1148,7 @@ extension ConversationVC: UIDocumentInteractionControllerDelegate {
extension ConversationVC {
fileprivate func approveMessageRequestIfNeeded(for thread: TSThread?, with transaction: YapDatabaseReadWriteTransaction, isNewThread: Bool, timestamp: UInt64) -> Promise<Void> {
fileprivate func approveMessageRequestIfNeeded(for thread: TSThread?, isNewThread: Bool, timestamp: UInt64) -> Promise<Void> {
guard let contactThread: TSContactThread = thread as? TSContactThread else { return Promise.value(()) }
// If the contact doesn't exist then we should create it so we can store the 'isApproved' state
@ -1195,7 +1179,17 @@ extension ConversationVC {
}
return promise
.then { MessageSender.sendNonDurably(messageRequestResponse, in: contactThread, using: transaction) }
.then { _ -> Promise<Void> in
let (promise, seal) = Promise<Void>.pending()
Storage.writeSync { transaction in
MessageSender.sendNonDurably(messageRequestResponse, in: contactThread, using: transaction)
.done { seal.fulfill(()) }
.catch { _ in seal.fulfill(()) } // Fulfill even if this failed; the configuration in the swarm should be at most 2 days old
.retainUntilComplete()
}
return promise
}
.map { _ in
if self?.presentedViewController is ModalActivityIndicatorViewController {
self?.dismiss(animated: true, completion: nil) // Dismiss the loader
@ -1204,9 +1198,11 @@ extension ConversationVC {
}
.map { _ in
// Default 'didApproveMe' to true for the person approving the message request
contact.isApproved = true
contact.didApproveMe = (contact.didApproveMe || !isNewThread)
Storage.shared.setContact(contact, using: transaction)
Storage.write { transaction in
contact.isApproved = true
contact.didApproveMe = (contact.didApproveMe || !isNewThread)
Storage.shared.setContact(contact, using: transaction)
}
// Hide the 'messageRequestView' since the request has been approved and force a config
// sync to propagate the contact approval state (both must run on the main thread)
@ -1244,30 +1240,27 @@ extension ConversationVC {
// Send a sync message with the details of the contact
if let appDelegate = UIApplication.shared.delegate as? AppDelegate {
appDelegate.forceSyncConfigurationNowIfNeeded(with: transaction).retainUntilComplete()
appDelegate.forceSyncConfigurationNowIfNeeded().retainUntilComplete()
}
}
}
}
@objc func acceptMessageRequest() {
Storage.write { transaction in
let promise: Promise<Void> = self.approveMessageRequestIfNeeded(
for: self.thread,
with: transaction,
isNewThread: false,
timestamp: NSDate.millisecondTimestamp()
)
// Show an error indicating that approving the thread failed
promise.catch(on: DispatchQueue.main) { [weak self] _ in
let alert = UIAlertController(title: "Session", message: NSLocalizedString("MESSAGE_REQUESTS_APPROVAL_ERROR_MESSAGE", comment: ""), preferredStyle: .alert)
alert.addAction(UIAlertAction(title: NSLocalizedString("BUTTON_OK", comment: ""), style: .default, handler: nil))
self?.present(alert, animated: true, completion: nil)
}
promise.retainUntilComplete()
let promise: Promise<Void> = self.approveMessageRequestIfNeeded(
for: self.thread,
isNewThread: false,
timestamp: NSDate.millisecondTimestamp()
)
// Show an error indicating that approving the thread failed
promise.catch(on: DispatchQueue.main) { [weak self] _ in
let alert = UIAlertController(title: "Session", message: NSLocalizedString("MESSAGE_REQUESTS_APPROVAL_ERROR_MESSAGE", comment: ""), preferredStyle: .alert)
alert.addAction(UIAlertAction(title: NSLocalizedString("BUTTON_OK", comment: ""), style: .default, handler: nil))
self?.present(alert, animated: true, completion: nil)
}
promise.retainUntilComplete()
}
@objc func deleteMessageRequest() {

@ -9,9 +9,10 @@ final class LinkPreviewView : UIView {
private lazy var imageViewContainerHeightConstraint = imageView.set(.height, to: 100)
private lazy var sentLinkPreviewTextColor: UIColor = {
let isOutgoing = (viewItem!.interaction.interactionType() == .outgoingMessage)
let isOutgoing = (viewItem?.interaction.interactionType() == .outgoingMessage)
switch (isOutgoing, AppModeManager.shared.currentAppMode) {
case (true, .dark), (false, .light): return .black
case (true, .light): return Colors.grey
default: return .white
}
}()
@ -57,6 +58,8 @@ final class LinkPreviewView : UIView {
result.addTarget(self, action: #selector(cancel), for: UIControl.Event.touchUpInside)
return result
}()
var bodyTextView: UITextView?
// MARK: Settings
private static let loaderSize: CGFloat = 24
@ -133,15 +136,7 @@ final class LinkPreviewView : UIView {
loader.alpha = (image != nil) ? 0 : 1
if image != nil { loader.stopAnimating() } else { loader.startAnimating() }
// Title
let isSent = (linkPreviewState is LinkPreviewSent)
let isOutgoing = (viewItem?.interaction.interactionType() == .outgoingMessage)
let textColor: UIColor
if isSent && isOutgoing && isLightMode {
textColor = .white
} else {
textColor = isDarkMode ? .white : .black
}
titleLabel.textColor = textColor
titleLabel.textColor = sentLinkPreviewTextColor
titleLabel.text = linkPreviewState.title()
// Horizontal stack view
switch linkPreviewState {
@ -152,6 +147,7 @@ final class LinkPreviewView : UIView {
bodyTextViewContainer.subviews.forEach { $0.removeFromSuperview() }
if let viewItem = viewItem {
let bodyTextView = VisibleMessageCell.getBodyTextView(for: viewItem, with: maxWidth, textColor: sentLinkPreviewTextColor, searchText: delegate.lastSearchedText, delegate: delegate)
self.bodyTextView = bodyTextView
bodyTextViewContainer.addSubview(bodyTextView)
bodyTextView.pin(to: bodyTextViewContainer, withInset: 12)
}

@ -1,74 +0,0 @@
import UIKit
/// Shown over a media message if it has a message body.
final class MediaTextOverlayView : UIView {
private let viewItem: ConversationViewItem
private let albumViewWidth: CGFloat
private let delegate: MessageCellDelegate
private let textColor: UIColor
var readMoreButton: UIButton?
// MARK: Settings
private static let maxHeight: CGFloat = 88;
// MARK: Lifecycle
init(viewItem: ConversationViewItem, albumViewWidth: CGFloat, textColor: UIColor, delegate: MessageCellDelegate) {
self.viewItem = viewItem
self.albumViewWidth = albumViewWidth
self.delegate = delegate
self.textColor = textColor
super.init(frame: CGRect.zero)
setUpViewHierarchy()
}
override init(frame: CGRect) {
preconditionFailure("Use init(text:) instead.")
}
required init?(coder: NSCoder) {
preconditionFailure("Use init(text:) instead.")
}
private func setUpViewHierarchy() {
guard let message = viewItem.interaction as? TSMessage, let body = message.body, body.count > 0 else { return }
// Body label
let bodyLabel = UILabel()
bodyLabel.numberOfLines = 0
bodyLabel.lineBreakMode = .byTruncatingTail
bodyLabel.text = given(body) { MentionUtilities.highlightMentions(in: $0, threadID: viewItem.interaction.uniqueThreadId) }
bodyLabel.textColor = self.textColor
bodyLabel.font = .systemFont(ofSize: Values.mediumFontSize)
// Content stack view
let contentStackView = UIStackView(arrangedSubviews: [ bodyLabel ])
contentStackView.axis = .horizontal
contentStackView.spacing = Values.smallSpacing
addSubview(contentStackView)
let inset: CGFloat = 12
contentStackView.pin(.left, to: .left, of: self, withInset: inset)
contentStackView.pin(.top, to: .top, of: self)
contentStackView.pin(.right, to: .right, of: self, withInset: -inset)
// Max height
bodyLabel.heightAnchor.constraint(lessThanOrEqualToConstant: MediaTextOverlayView.maxHeight).isActive = true
// Overflow button
let bodyLabelTargetSize = bodyLabel.sizeThatFits(CGSize(width: albumViewWidth - 2 * inset, height: .greatestFiniteMagnitude))
if bodyLabelTargetSize.height > MediaTextOverlayView.maxHeight {
let readMoreButton = UIButton()
self.readMoreButton = readMoreButton
readMoreButton.setTitle("Read More", for: UIControl.State.normal)
readMoreButton.titleLabel!.font = .boldSystemFont(ofSize: Values.smallFontSize)
readMoreButton.setTitleColor(self.textColor, for: UIControl.State.normal)
readMoreButton.addTarget(self, action: #selector(readMore), for: UIControl.Event.touchUpInside)
addSubview(readMoreButton)
readMoreButton.pin(.left, to: .left, of: self, withInset: inset)
readMoreButton.pin(.top, to: .bottom, of: contentStackView, withInset: Values.smallSpacing)
readMoreButton.pin(.bottom, to: .bottom, of: self, withInset: -Values.smallSpacing)
} else {
contentStackView.pin(.bottom, to: .bottom, of: self, withInset: -inset)
}
}
// MARK: Interaction
@objc private func readMore() {
delegate.showFullText(viewItem)
}
}

@ -4,7 +4,6 @@ final class VisibleMessageCell : MessageCell, LinkPreviewViewDelegate {
private var previousX: CGFloat = 0
var albumView: MediaAlbumView?
var bodyTextView: UITextView?
var mediaTextOverlayView: MediaTextOverlayView?
// Constraints
private lazy var headerViewTopConstraint = headerView.pin(.top, to: .top, of: self, withInset: 1)
private lazy var authorLabelHeightConstraint = authorLabel.set(.height, to: 0)
@ -254,8 +253,9 @@ final class VisibleMessageCell : MessageCell, LinkPreviewViewDelegate {
let authorLabelSize = authorLabel.sizeThatFits(authorLabelAvailableSpace)
authorLabelHeightConstraint.constant = (viewItem.senderName != nil) ? authorLabelSize.height : 0
// Message status image view
let (image, backgroundColor) = getMessageStatusImage(for: message)
let (image, tintColor, backgroundColor) = getMessageStatusImage(for: message)
messageStatusImageView.image = image
messageStatusImageView.tintColor = tintColor
messageStatusImageView.backgroundColor = backgroundColor
if let message = message as? TSOutgoingMessage {
messageStatusImageView.isHidden = (message.messageState == .sent && thread?.lastInteraction != message)
@ -312,7 +312,6 @@ final class VisibleMessageCell : MessageCell, LinkPreviewViewDelegate {
}
albumView = nil
bodyTextView = nil
mediaTextOverlayView = nil
let isOutgoing = (viewItem.interaction.interactionType() == .outgoingMessage)
switch viewItem.messageCellType {
case .textOnlyMessage:
@ -324,6 +323,7 @@ final class VisibleMessageCell : MessageCell, LinkPreviewViewDelegate {
snContentView.addSubview(linkPreviewView)
linkPreviewView.pin(to: snContentView)
linkPreviewView.layer.mask = bubbleViewMaskLayer
self.bodyTextView = linkPreviewView.bodyTextView
} else if let openGroupInvitationName = message.openGroupInvitationName, let openGroupInvitationURL = message.openGroupInvitationURL {
let openGroupInvitationView = OpenGroupInvitationView(name: openGroupInvitationName, url: openGroupInvitationURL, textColor: bodyLabelTextColor, isOutgoing: isOutgoing)
snContentView.addSubview(openGroupInvitationView)
@ -372,11 +372,12 @@ final class VisibleMessageCell : MessageCell, LinkPreviewViewDelegate {
albumView.layer.mask = bubbleViewMaskLayer
stackView.addArrangedSubview(albumView)
// Body text view
if let message = viewItem.interaction as? TSMessage, let body = message.body, body.count > 0,
let delegate = delegate { // delegate should always be set at this point
let overlayView = MediaTextOverlayView(viewItem: viewItem, albumViewWidth: size.width, textColor: bodyLabelTextColor, delegate: delegate)
self.mediaTextOverlayView = overlayView
stackView.addArrangedSubview(overlayView)
if let message = viewItem.interaction as? TSMessage, let body = message.body, body.count > 0 {
let inset: CGFloat = 12
let maxWidth = size.width - 2 * inset
let bodyTextView = VisibleMessageCell.getBodyTextView(for: viewItem, with: maxWidth, textColor: bodyLabelTextColor, searchText: delegate?.lastSearchedText, delegate: self)
self.bodyTextView = bodyTextView
stackView.addArrangedSubview(UIView(wrapping: bodyTextView, withInsets: UIEdgeInsets(top: 0, left: inset, bottom: inset, right: inset)))
}
unloadContent = { albumView.unloadMedia() }
// Constraints
@ -627,20 +628,33 @@ final class VisibleMessageCell : MessageCell, LinkPreviewViewDelegate {
}
}
private func getMessageStatusImage(for message: TSMessage) -> (image: UIImage?, backgroundColor: UIColor?) {
guard let message = message as? TSOutgoingMessage else { return (nil, nil) }
private func getMessageStatusImage(for message: TSMessage) -> (image: UIImage?, tintColor: UIColor?, backgroundColor: UIColor?) {
guard let message = message as? TSOutgoingMessage else { return (nil, nil, nil) }
let image: UIImage
var tintColor: UIColor? = nil
var backgroundColor: UIColor? = nil
let status = MessageRecipientStatusUtils.recipientStatus(outgoingMessage: message)
switch status {
case .uploading, .sending: image = #imageLiteral(resourceName: "CircleDotDotDot").asTintedImage(color: Colors.text)!
case .sent, .skipped, .delivered: image = #imageLiteral(resourceName: "CircleCheck").asTintedImage(color: Colors.text)!
case .read:
backgroundColor = isLightMode ? .black : .white
image = isLightMode ? #imageLiteral(resourceName: "FilledCircleCheckLightMode") : #imageLiteral(resourceName: "FilledCircleCheckDarkMode")
case .failed: image = #imageLiteral(resourceName: "message_status_failed").asTintedImage(color: Colors.destructive)!
case .uploading, .sending:
image = #imageLiteral(resourceName: "CircleDotDotDot").withRenderingMode(.alwaysTemplate)
tintColor = Colors.text
case .sent, .skipped, .delivered:
image = #imageLiteral(resourceName: "CircleCheck").withRenderingMode(.alwaysTemplate)
tintColor = Colors.text
case .read:
image = isLightMode ? #imageLiteral(resourceName: "FilledCircleCheckLightMode") : #imageLiteral(resourceName: "FilledCircleCheckDarkMode")
backgroundColor = isLightMode ? .black : .white
case .failed:
image = #imageLiteral(resourceName: "message_status_failed").withRenderingMode(.alwaysTemplate)
tintColor = Colors.destructive
}
return (image, backgroundColor)
return (image, tintColor, backgroundColor)
}
private func getSize(for viewItem: ConversationViewItem) -> CGSize {

@ -7,7 +7,18 @@ final class HomeVC : BaseVC, UITableViewDataSource, UITableViewDelegate, NewConv
private var threadViewModelCache: [String:ThreadViewModel] = [:] // Thread ID to ThreadViewModel
private var tableViewTopConstraint: NSLayoutConstraint!
private var unreadMessageRequestCount: UInt {
OWSMessageUtils.sharedManager().unreadMessageRequestCount()
var count: UInt = 0
dbConnection.read { transaction in
let ext = transaction.ext(TSThreadDatabaseViewExtensionName) as! YapDatabaseViewTransaction
ext.enumerateRows(inGroup: TSMessageRequestGroup) { _, _, object, _, _, _ in
if ((object as? TSThread)?.unreadMessageCount(transaction: transaction) ?? 0) > 0 {
count += 1
}
}
}
return count
}
private var threadCount: UInt {
@ -84,6 +95,12 @@ final class HomeVC : BaseVC, UITableViewDataSource, UITableViewDelegate, NewConv
// MARK: Lifecycle
override func viewDidLoad() {
super.viewDidLoad()
// Note: This is a hack to ensure `isRTL` is initially gets run on the main thread so the value is cached (it gets
// called on background threads and if it hasn't cached the value then it can cause odd performance issues since
// it accesses UIKit)
_ = CurrentAppContext().isRTL
// Threads (part 1)
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)
// Preparation

@ -25,7 +25,7 @@ final class NewConversationButtonSet : UIView {
private lazy var newDMLabel: UILabel = {
let result: UILabel = UILabel()
result.translatesAutoresizingMaskIntoConstraints = false
result.font = UIFont.systemFont(ofSize: Values.verySmallFontSize)
result.font = UIFont.systemFont(ofSize: Values.verySmallFontSize, weight: .bold)
result.text = NSLocalizedString("NEW_CONVERSATION_MENU_DIRECT_MESSAGE", comment: "").uppercased()
result.textColor = Colors.grey
result.textAlignment = .center
@ -36,7 +36,7 @@ final class NewConversationButtonSet : UIView {
private lazy var createClosedGroupLabel: UILabel = {
let result: UILabel = UILabel()
result.translatesAutoresizingMaskIntoConstraints = false
result.font = UIFont.systemFont(ofSize: Values.verySmallFontSize)
result.font = UIFont.systemFont(ofSize: Values.verySmallFontSize, weight: .bold)
result.text = NSLocalizedString("NEW_CONVERSATION_MENU_CLOSED_GROUP", comment: "").uppercased()
result.textColor = Colors.grey
result.textAlignment = .center
@ -47,7 +47,7 @@ final class NewConversationButtonSet : UIView {
private lazy var joinOpenGroupLabel: UILabel = {
let result: UILabel = UILabel()
result.translatesAutoresizingMaskIntoConstraints = false
result.font = UIFont.systemFont(ofSize: Values.verySmallFontSize)
result.font = UIFont.systemFont(ofSize: Values.verySmallFontSize, weight: .bold)
result.text = NSLocalizedString("NEW_CONVERSATION_MENU_OPEN_GROUP", comment: "").uppercased()
result.textColor = Colors.grey
result.textAlignment = .center

@ -7,10 +7,11 @@ extension AppDelegate {
guard Storage.shared.getUser()?.name != nil else { return }
let userDefaults = UserDefaults.standard
let lastSync = userDefaults[.lastConfigurationSync] ?? .distantPast
guard Date().timeIntervalSince(lastSync) > 7 * 24 * 60 * 60,
let configurationMessage = ConfigurationMessage.getCurrent() else { return } // Sync every 2 days
guard Date().timeIntervalSince(lastSync) > 7 * 24 * 60 * 60 else { return } // Sync every 2 days
let destination = Message.Destination.contact(publicKey: getUserHexEncodedPublicKey())
Storage.shared.write { transaction in
Storage.write { transaction in
guard let configurationMessage = ConfigurationMessage.getCurrent(with: transaction) else { return }
let job = MessageSendJob(message: configurationMessage, destination: destination)
JobQueue.shared.add(job, using: transaction)
}
@ -22,14 +23,17 @@ extension AppDelegate {
}
}
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> {
let destination = Message.Destination.contact(publicKey: getUserHexEncodedPublicKey())
let (promise, seal) = Promise<Void>.pending()
// Note: SQLite only supports a single write thread so we can be sure this will retrieve the most up-to-date data
Storage.writeSync { transaction in
guard Storage.shared.getUser(using: transaction)?.name != nil, let configurationMessage = ConfigurationMessage.getCurrent(with: transaction) else {
seal.fulfill(())
return
}
MessageSender.send(configurationMessage, to: destination, using: transaction).done {
seal.fulfill(())
}.catch { _ in

@ -166,6 +166,7 @@ NSString *const ReportedApplicationStateDidChangeNotification = @"ReportedApplic
- (BOOL)isRTL
{
// FIXME: We should try to remove this as we've had to add a hack to ensure the first call to this runs on the main thread
static BOOL isRTL = NO;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{

@ -1,4 +1,7 @@
import UIKit
import SessionUIKit
import SessionSnodeKit
import SessionMessagingKit
@objc(LKNukeDataModal)
final class NukeDataModal : Modal {
@ -125,6 +128,7 @@ final class NukeDataModal : Modal {
appDelegate.forceSyncConfigurationNowIfNeeded().ensure(on: DispatchQueue.main) {
self?.dismiss(animated: true, completion: nil) // Dismiss the loader
UserDefaults.removeAll() // Not done in the nuke data implementation as unlinking requires this to happen later
General.Cache.cachedEncodedPublicKey.mutate { $0 = nil } // Remove the cached key so it gets re-cached on next access
NotificationCenter.default.post(name: .dataNukeRequested, object: nil)
}.retainUntilComplete()
}
@ -136,6 +140,7 @@ final class NukeDataModal : Modal {
self?.dismiss(animated: true, completion: nil) // Dismiss the loader
let potentiallyMaliciousSnodes = confirmations.compactMap { $0.value == false ? $0.key : nil }
if potentiallyMaliciousSnodes.isEmpty {
General.Cache.cachedEncodedPublicKey.mutate { $0 = nil } // Remove the cached key so it gets re-cached on next access
UserDefaults.removeAll() // Not done in the nuke data implementation as unlinking requires this to happen later
NotificationCenter.default.post(name: .dataNukeRequested, object: nil)
} else {

@ -106,6 +106,9 @@ class BaseVC : UIViewController {
}
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
if #available(iOS 13.0, *) {
SNLog("Current trait collection: \(UITraitCollection.current), previous trait collection: \(previousTraitCollection)")
}
if LKAppModeUtilities.isSystemDefault {
NotificationCenter.default.post(name: .appModeChanged, object: nil)
}

@ -321,19 +321,30 @@ final class ConversationCell : UITableViewCell {
statusIndicatorView.backgroundColor = nil
let lastMessage = threadViewModel.lastMessageForInbox
if let lastMessage = lastMessage as? TSOutgoingMessage {
let image: UIImage
let status = MessageRecipientStatusUtils.recipientStatus(outgoingMessage: lastMessage)
switch status {
case .uploading, .sending: image = #imageLiteral(resourceName: "CircleDotDotDot").asTintedImage(color: Colors.text)!
case .sent, .skipped, .delivered: image = #imageLiteral(resourceName: "CircleCheck").asTintedImage(color: Colors.text)!
case .read:
statusIndicatorView.backgroundColor = isLightMode ? .black : .white
image = isLightMode ? #imageLiteral(resourceName: "FilledCircleCheckLightMode") : #imageLiteral(resourceName: "FilledCircleCheckDarkMode")
case .failed: image = #imageLiteral(resourceName: "message_status_failed").asTintedImage(color: Colors.text)!
case .uploading, .sending:
statusIndicatorView.image = #imageLiteral(resourceName: "CircleDotDotDot").withRenderingMode(.alwaysTemplate)
statusIndicatorView.tintColor = Colors.text
case .sent, .skipped, .delivered:
statusIndicatorView.image = #imageLiteral(resourceName: "CircleCheck").withRenderingMode(.alwaysTemplate)
statusIndicatorView.tintColor = Colors.text
case .read:
statusIndicatorView.image = isLightMode ? #imageLiteral(resourceName: "FilledCircleCheckLightMode") : #imageLiteral(resourceName: "FilledCircleCheckDarkMode")
statusIndicatorView.tintColor = nil
statusIndicatorView.backgroundColor = (isLightMode ? .black : .white)
case .failed:
statusIndicatorView.image = #imageLiteral(resourceName: "message_status_failed").withRenderingMode(.alwaysTemplate)
statusIndicatorView.tintColor = Colors.destructive
}
statusIndicatorView.image = image
statusIndicatorView.isHidden = false
} else {
}
else {
statusIndicatorView.isHidden = true
}
}

@ -83,16 +83,22 @@ final class UserCell : UITableViewCell {
profilePictureView.publicKey = publicKey
profilePictureView.update()
displayNameLabel.text = Storage.shared.getContact(with: publicKey)?.displayName(for: .regular) ?? publicKey
switch accessory {
case .none: accessoryImageView.isHidden = true
case .lock:
accessoryImageView.isHidden = false
accessoryImageView.image = #imageLiteral(resourceName: "ic_lock_outline").asTintedImage(color: Colors.text.withAlphaComponent(Values.mediumOpacity))!
case .tick(let isSelected):
accessoryImageView.isHidden = false
let icon = isSelected ? #imageLiteral(resourceName: "CircleCheck") : #imageLiteral(resourceName: "Circle")
accessoryImageView.image = isDarkMode ? icon : icon.asTintedImage(color: Colors.text)!
case .none: accessoryImageView.isHidden = true
case .lock:
accessoryImageView.isHidden = false
accessoryImageView.image = #imageLiteral(resourceName: "ic_lock_outline").withRenderingMode(.alwaysTemplate)
accessoryImageView.tintColor = Colors.text.withAlphaComponent(Values.mediumOpacity)
case .tick(let isSelected):
let icon: UIImage = (isSelected ? #imageLiteral(resourceName: "CircleCheck") : #imageLiteral(resourceName: "Circle"))
accessoryImageView.isHidden = false
accessoryImageView.image = icon.withRenderingMode(.alwaysTemplate)
accessoryImageView.tintColor = Colors.text
}
let alpha: CGFloat = isZombie ? 0.5 : 1
[ profilePictureView, displayNameLabel, accessoryImageView ].forEach { $0.alpha = alpha }
}

@ -34,7 +34,7 @@ public final class BackgroundPoller: NSObject {
}
private static func pollForMessages() -> Promise<Void> {
let userPublicKey = getUserHexEncodedPublicKey()
let userPublicKey: String = getUserHexEncodedPublicKey()
return getMessages(for: userPublicKey)
}
@ -51,7 +51,7 @@ public final class BackgroundPoller: NSObject {
return attempt(maxRetryCount: 4, recoveringOn: DispatchQueue.main) {
return SnodeAPI.getRawMessages(from: snode, associatedWith: publicKey)
.then(on: DispatchQueue.main) { responseData -> Promise<Void> in
let messages = SnodeAPI.parseRawMessagesResponse(responseData, from: snode, associatedWith: publicKey)
let (messages, lastRawMessage) = SnodeAPI.parseRawMessagesResponse(responseData, from: snode, associatedWith: publicKey)
let promises = messages
.compactMap { json -> Promise<Void>? in
// Use a best attempt approach here; we don't want to fail
@ -65,6 +65,9 @@ public final class BackgroundPoller: NSObject {
return job.execute()
}
// Now that the MessageReceiveJob's have been created we can update the `lastMessageHash` value
SnodeAPI.updateLastMessageHashValueIfPossible(for: snode, associatedWith: publicKey, from: lastRawMessage)
return when(fulfilled: promises) // The promise returned by MessageReceiveJob never rejects
}
}

@ -10,13 +10,19 @@ extension Storage {
private static let closedGroupZombieMembersCollection = "SNClosedGroupZombieMembersCollection"
public func getClosedGroupEncryptionKeyPairs(for groupPublicKey: String) -> [ECKeyPair] {
var result: [ECKeyPair] = []
Storage.read { transaction in
result = self.getClosedGroupEncryptionKeyPairs(for: groupPublicKey, using: transaction)
}
return result
}
public func getClosedGroupEncryptionKeyPairs(for groupPublicKey: String, using transaction: YapDatabaseReadTransaction) -> [ECKeyPair] {
let collection = Storage.getClosedGroupEncryptionKeyPairCollection(for: groupPublicKey)
var timestampsAndKeyPairs: [(timestamp: Double, keyPair: ECKeyPair)] = []
Storage.read { transaction in
transaction.enumerateKeysAndObjects(inCollection: collection) { key, object, _ in
guard let timestamp = Double(key), let keyPair = object as? ECKeyPair else { return }
timestampsAndKeyPairs.append((timestamp, keyPair))
}
transaction.enumerateKeysAndObjects(inCollection: collection) { key, object, _ in
guard let timestamp = Double(key), let keyPair = object as? ECKeyPair else { return }
timestampsAndKeyPairs.append((timestamp, keyPair))
}
return timestampsAndKeyPairs.sorted { $0.timestamp < $1.timestamp }.map { $0.keyPair }
}
@ -24,6 +30,10 @@ extension Storage {
public func getLatestClosedGroupEncryptionKeyPair(for groupPublicKey: String) -> ECKeyPair? {
return getClosedGroupEncryptionKeyPairs(for: groupPublicKey).last
}
public func getLatestClosedGroupEncryptionKeyPair(for groupPublicKey: String, using transaction: YapDatabaseReadTransaction) -> ECKeyPair? {
return getClosedGroupEncryptionKeyPairs(for: groupPublicKey, using: transaction).last
}
public func addClosedGroupEncryptionKeyPair(_ keyPair: ECKeyPair, for groupPublicKey: String, using transaction: Any) {
let collection = Storage.getClosedGroupEncryptionKeyPairCollection(for: groupPublicKey)
@ -39,10 +49,14 @@ extension Storage {
public func getUserClosedGroupPublicKeys() -> Set<String> {
var result: Set<String> = []
Storage.read { transaction in
result = Set(transaction.allKeys(inCollection: Storage.closedGroupPublicKeyCollection))
result = self.getUserClosedGroupPublicKeys(using: transaction)
}
return result
}
public func getUserClosedGroupPublicKeys(using transaction: YapDatabaseReadTransaction) -> Set<String> {
return Set(transaction.allKeys(inCollection: Storage.closedGroupPublicKeyCollection))
}
public func addClosedGroupPublicKey(_ groupPublicKey: String, using transaction: Any) {
(transaction as! YapDatabaseReadWriteTransaction).setObject(groupPublicKey, forKey: groupPublicKey, inCollection: Storage.closedGroupPublicKeyCollection)
@ -81,4 +95,8 @@ extension Storage {
public func isClosedGroup(_ publicKey: String) -> Bool {
getUserClosedGroupPublicKeys().contains(publicKey)
}
public func isClosedGroup(_ publicKey: String, using transaction: YapDatabaseReadTransaction) -> Bool {
getUserClosedGroupPublicKeys(using: transaction).contains(publicKey)
}
}

@ -159,6 +159,31 @@ extension Storage {
(transaction as! YapDatabaseReadWriteTransaction).removeObject(forKey: server, inCollection: collection)
}
// MARK: - OpenGroupServerIdToUniqueIdLookup
public static let openGroupServerIdToUniqueIdLookupCollection = "SNOpenGroupServerIdToUniqueIdLookup"
public func getOpenGroupServerIdLookup(_ serverId: UInt64, in room: String, on server: String, using transaction: YapDatabaseReadTransaction) -> OpenGroupServerIdLookup? {
let key: String = OpenGroupServerIdLookup.id(serverId: serverId, in: room, on: server)
return transaction.object(forKey: key, inCollection: Storage.openGroupServerIdToUniqueIdLookupCollection) as? OpenGroupServerIdLookup
}
public func addOpenGroupServerIdLookup(_ serverId: UInt64?, tsMessageId: String?, in room: String, on server: String, using transaction: YapDatabaseReadWriteTransaction) {
guard let serverId: UInt64 = serverId, let tsMessageId: String = tsMessageId else { return }
let lookup: OpenGroupServerIdLookup = OpenGroupServerIdLookup(server: server, room: room, serverId: serverId, tsMessageId: tsMessageId)
addOpenGroupServerIdLookup(lookup, using: transaction)
}
public func addOpenGroupServerIdLookup(_ lookup: OpenGroupServerIdLookup, using transaction: YapDatabaseReadWriteTransaction) {
transaction.setObject(lookup, forKey: lookup.id, inCollection: Storage.openGroupServerIdToUniqueIdLookupCollection)
}
public func removeOpenGroupServerIdLookup(_ serverId: UInt64, in room: String, on server: String, using transaction: YapDatabaseReadWriteTransaction) {
let key: String = OpenGroupServerIdLookup.id(serverId: serverId, in: room, on: server)
transaction.removeObject(forKey: key, inCollection: Storage.openGroupServerIdToUniqueIdLookupCollection)
}
// MARK: - Metadata
private static let openGroupUserCountCollection = "SNOpenGroupUserCountCollection"

@ -36,11 +36,21 @@ extension Storage {
}
@objc public func getUser() -> Contact? {
guard let userPublicKey = getUserPublicKey() else { return nil }
return getUser(using: nil)
}
public func getUser(using transaction: YapDatabaseReadTransaction?) -> Contact? {
let userPublicKey = getUserHexEncodedPublicKey()
var result: Contact?
Storage.read { transaction in
if let transaction = transaction {
result = Storage.shared.getContact(with: userPublicKey, using: transaction)
}
else {
Storage.read { transaction in
result = Storage.shared.getContact(with: userPublicKey, using: transaction)
}
}
return result
}
}

@ -2,9 +2,9 @@ import SessionUtilitiesKit
extension ConfigurationMessage {
public static func getCurrent(with transaction: YapDatabaseReadWriteTransaction? = nil) -> ConfigurationMessage? {
public static func getCurrent(with transaction: YapDatabaseReadTransaction) -> ConfigurationMessage? {
let storage = Storage.shared
guard let user = storage.getUser() else { return nil }
guard let user = storage.getUser(using: transaction) else { return nil }
let displayName = user.name
let profilePictureURL = user.profilePictureURL
@ -13,91 +13,84 @@ extension ConfigurationMessage {
var openGroups: Set<String> = []
var contacts: Set<ConfigurationMessage.Contact> = []
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 openGroup = storage.getOpenGroup(for: thread.uniqueId!) {
openGroups.insert("\(openGroup.server)/\(openGroup.room)?public_key=\(openGroup.publicKey)")
}
default: break
}
}
let currentUserPublicKey: String = getUserHexEncodedPublicKey()
TSGroupThread.enumerateCollectionObjects(with: transaction) { object, _ in
guard let thread = object as? TSGroupThread else { return }
contacts = storage.getAllContacts(with: transaction)
.filter { contact -> Bool in
let threadID = TSContactThread.threadID(fromContactSessionID: contact.sessionID)
switch thread.groupModel.groupType {
case .closedGroup:
guard thread.isCurrentUserMemberInGroup() else { return }
let groupID = thread.groupModel.groupId
let groupPublicKey = LKGroupUtilities.getDecodedGroupID(groupID)
return (
// Skip the current user
contact.sessionID != currentUserPublicKey &&
// Contacts which have visible threads
TSContactThread.fetch(uniqueId: threadID, transaction: transaction)?.shouldBeVisible == true && (
// Include already approved contacts
contact.isApproved ||
contact.didApproveMe ||
// Sync blocked contacts
SSKEnvironment.shared.blockingManager.isRecipientIdBlocked(contact.sessionID)
)
guard
storage.isClosedGroup(groupPublicKey, using: transaction),
let encryptionKeyPair = storage.getLatestClosedGroupEncryptionKeyPair(for: groupPublicKey, using: transaction)
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)
)
}
.map { contact -> ConfigurationMessage.Contact in
// 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
closedGroups.insert(closedGroup)
case .openGroup:
if let threadId: String = thread.uniqueId, let openGroup = storage.getOpenGroup(for: threadId) {
openGroups.insert("\(openGroup.server)/\(openGroup.room)?public_key=\(openGroup.publicKey)")
}
return ConfigurationMessage.Contact(
publicKey: contact.sessionID,
displayName: (contact.name ?? contact.sessionID),
profilePictureURL: profilePictureURL,
profileKey: profileKey,
hasIsApproved: true,
isApproved: contact.isApproved,
hasIsBlocked: true,
isBlocked: contact.isBlocked,
hasDidApproveMe: true,
didApproveMe: contact.didApproveMe
default: break
}
}
let currentUserPublicKey: String = getUserHexEncodedPublicKey()
contacts = storage.getAllContacts(with: transaction)
.compactMap { contact -> ConfigurationMessage.Contact? in
let threadID = TSContactThread.threadID(fromContactSessionID: contact.sessionID)
guard
// Skip the current user
contact.sessionID != currentUserPublicKey &&
// Contacts which have visible threads
TSContactThread.fetch(uniqueId: threadID, transaction: transaction)?.shouldBeVisible == true && (
// Include already approved contacts
contact.isApproved ||
contact.didApproveMe ||
// Sync blocked contacts
SSKEnvironment.shared.blockingManager.isRecipientIdBlocked(contact.sessionID)
)
else {
return nil
}
.asSet()
// 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
return ConfigurationMessage.Contact(
publicKey: contact.sessionID,
displayName: (contact.name ?? contact.sessionID),
profilePictureURL: profilePictureURL,
profileKey: profileKey,
hasIsApproved: true,
isApproved: contact.isApproved,
hasIsBlocked: true,
isBlocked: SSKEnvironment.shared.blockingManager.isRecipientIdBlocked(contact.sessionID),
hasDidApproveMe: true,
didApproveMe: contact.didApproveMe
)
}
// 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) }
}
.asSet()
return ConfigurationMessage(
displayName: displayName,

@ -5,7 +5,6 @@ FOUNDATION_EXPORT const unsigned char SessionMessagingKitVersionString[];
#import <SessionMessagingKit/AppReadiness.h>
#import <SessionMessagingKit/Environment.h>
#import <SessionMessagingKit/GeneralUtilities.h>
#import <SessionMessagingKit/NotificationsProtocol.h>
#import <SessionMessagingKit/NSData+messagePadding.h>
#import <SessionMessagingKit/OWSAudioPlayer.h>

@ -395,18 +395,24 @@ public final class OpenGroupManager: NSObject {
// Handle any deletions that are needed
guard !messageServerIDsToRemove.isEmpty else { return }
guard let thread = TSGroupThread.fetch(groupId: openGroupIdData, transaction: transaction) else { return }
var messagesToRemove: [TSMessage] = []
thread.enumerateInteractions(with: transaction) { interaction, stop in
guard let message: TSMessage = interaction as? TSMessage, messageServerIDsToRemove.contains(message.openGroupServerMessageID) else {
dependencies.storage.write { transaction in
guard let transaction: YapDatabaseReadWriteTransaction = transaction as? YapDatabaseReadWriteTransaction else {
return
}
messagesToRemove.append(message)
messageServerIDsToRemove.forEach { openGroupServerMessageId in
guard let messageLookup: OpenGroupServerIdLookup = dependencies.storage.getOpenGroupServerIdLookup(openGroupServerMessageId, in: roomToken, on: server, using: transaction) else {
return
}
guard let tsMessage: TSMessage = TSMessage.fetch(uniqueId: messageLookup.tsMessageId, transaction: transaction) else {
return
}
tsMessage.remove(with: transaction)
dependencies.storage.removeOpenGroupServerIdLookup(openGroupServerMessageId, in: roomToken, on: server, using: transaction)
}
}
messagesToRemove.forEach { $0.remove(with: transaction) }
}
internal static func handleDirectMessages(

@ -0,0 +1,46 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import Foundation
@objc(SNOpenGroupServerIdLookup)
public final class OpenGroupServerIdLookup: NSObject, NSCoding { // NSObject/NSCoding conformance is needed for YapDatabase compatibility
@objc public let id: String
@objc public let serverId: UInt64
@objc public let tsMessageId: String
// MARK: - Initialization
@objc public init(server: String, room: String, serverId: UInt64, tsMessageId: String) {
self.id = OpenGroupServerIdLookup.id(serverId: serverId, in: room, on: server)
self.serverId = serverId
self.tsMessageId = tsMessageId
super.init()
}
private override init() { preconditionFailure("Use init(blindedId:sessionId:) instead.") }
// MARK: - Coding
public required init?(coder: NSCoder) {
guard let id: String = coder.decodeObject(forKey: "id") as! String? else { return nil }
guard let serverId: UInt64 = coder.decodeObject(forKey: "serverId") as! UInt64? else { return nil }
guard let tsMessageId: String = coder.decodeObject(forKey: "tsMessageId") as! String? else { return nil }
self.id = id
self.serverId = serverId
self.tsMessageId = tsMessageId
}
public func encode(with coder: NSCoder) {
coder.encode(id, forKey: "id")
coder.encode(serverId, forKey: "serverId")
coder.encode(tsMessageId, forKey: "tsMessageId")
}
// MARK: - Convenience
static func id(serverId: UInt64, in room: String, on server: String) -> String {
return "\(server).\(room).\(serverId)"
}
}

@ -384,7 +384,15 @@ extension MessageReceiver {
}
if let tsMessage = TSMessage.fetch(uniqueId: tsMessageID, transaction: transaction) {
// Keep track of the open group server message ID message ID relationship
if let serverID = message.openGroupServerMessageID { tsMessage.openGroupServerMessageID = serverID }
if let serverID = message.openGroupServerMessageID {
tsMessage.openGroupServerMessageID = serverID
// Create a lookup between the openGroupServerMessageId and the tsMessage id for easy lookup
if let openGroup: OpenGroupV2 = storage.getV2OpenGroup(for: threadID) {
storage.addOpenGroupServerIdLookup(serverID, tsMessageId: tsMessageID, in: openGroup.room, on: openGroup.server, using: transaction)
}
}
// Keep track of server hash
if let serverHash = message.serverHash { tsMessage.serverHash = serverHash }
tsMessage.save(with: transaction)
@ -830,12 +838,14 @@ extension MessageReceiver {
// 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
Storage.write { transaction in
guard Storage.shared.getUser()?.name != nil, let configurationMessage = ConfigurationMessage.getCurrent(with: transaction) else {
return
}
let destination: Message.Destination = Message.Destination.contact(publicKey: userPublicKey)
MessageSender.send(configurationMessage, to: destination, using: transaction).retainUntilComplete()
}
let destination: Message.Destination = Message.Destination.contact(publicKey: userPublicKey)
MessageSender.send(configurationMessage, to: destination, using: transaction).retainUntilComplete()
}
}

@ -231,12 +231,12 @@ public final class MessageSender : NSObject {
let promiseCount = promises.count
var errorCount = 0
promises.forEach {
let _ = $0.done(on: DispatchQueue.global(qos: .userInitiated)) { rawResponse in
let _ = $0.done(on: DispatchQueue.global(qos: .userInitiated)) { responseData in
guard !isSuccess else { return } // Succeed as soon as the first promise succeeds
isSuccess = true
storage.write(with: { transaction in
let json = rawResponse as? JSON
let hash = json?["hash"] as? String
let responseJson: JSON? = try? JSONSerialization.jsonObject(with: responseData, options: [ .fragmentsAllowed ]) as? JSON
let hash = responseJson?["hash"] as? String
message.serverHash = hash
MessageSender.handleSuccessfulMessageSend(message, to: destination, isSyncMessage: isSyncMessage, using: transaction)
var shouldNotify = ((message is VisibleMessage || message is UnsendRequest) && !isSyncMessage)
@ -520,6 +520,20 @@ public final class MessageSender : NSObject {
// Otherwise the quote messages may not be able
// to be found by the timestamp on other devices
tsMessage.updateOpenGroupServerID(openGroupServerMessageID, serverTimeStamp: timestamp)
// Create a lookup between the openGroupServerMessageId and the tsMessage id for easy lookup
switch destination {
case .openGroup(let room, let server, _, _, _):
Storage.shared.addOpenGroupServerIdLookup(
openGroupServerMessageID,
tsMessageId: tsMessage.uniqueId,
in: room,
on: server,
using: transaction
)
default: break
}
}
// Mark the message as sent
var recipients = [ message.recipient! ]

@ -100,15 +100,17 @@ public final class ClosedGroupPoller : NSObject {
private func poll(_ groupPublicKey: String) -> Promise<Void> {
guard isPolling(for: groupPublicKey) else { return Promise.value(()) }
let promise = SnodeAPI.getSwarm(for: groupPublicKey).then2 { [weak self] swarm -> Promise<[JSON]> in
let promise = SnodeAPI.getSwarm(for: groupPublicKey).then2 { [weak self] swarm -> Promise<(Snode, [JSON], JSON?)> in
// randomElement() uses the system's default random generator, which is cryptographically secure
guard let snode = swarm.randomElement() else { return Promise(error: Error.insufficientSnodes) }
guard let self = self, self.isPolling(for: groupPublicKey) else { return Promise(error: Error.pollingCanceled) }
return SnodeAPI.getRawMessages(from: snode, associatedWith: groupPublicKey).map2 {
SnodeAPI.parseRawMessagesResponse($0, from: snode, associatedWith: groupPublicKey)
let (rawMessages, lastRawMessage) = SnodeAPI.parseRawMessagesResponse($0, from: snode, associatedWith: groupPublicKey)
return (snode, rawMessages, lastRawMessage)
}
}
promise.done2 { [weak self] rawMessages in
promise.done2 { [weak self] snode, rawMessages, lastRawMessage in
guard let self = self, self.isPolling(for: groupPublicKey) else { return }
if !rawMessages.isEmpty {
SNLog("Received \(rawMessages.count) new message(s) in closed group with public key: \(groupPublicKey).")
@ -125,6 +127,9 @@ public final class ClosedGroupPoller : NSObject {
SNLog("Failed to deserialize envelope due to error: \(error).")
}
}
// Now that the MessageReceiveJob's have been created we can update the `lastMessageHash` value
SnodeAPI.updateLastMessageHashValueIfPossible(for: snode, associatedWith: groupPublicKey, from: lastRawMessage)
}
promise.catch2 { error in
SNLog("Polling failed for closed group with public key: \(groupPublicKey) due to error: \(error).")

@ -94,7 +94,9 @@ public final class Poller : NSObject {
let userPublicKey = getUserHexEncodedPublicKey()
return SnodeAPI.getRawMessages(from: snode, associatedWith: userPublicKey).then(on: Threading.pollerQueue) { [weak self] responseData -> Promise<Void> in
guard let strongSelf = self, strongSelf.isPolling else { return Promise { $0.fulfill(()) } }
let messages = SnodeAPI.parseRawMessagesResponse(responseData, from: snode, associatedWith: userPublicKey)
let (messages, lastRawMessage) = SnodeAPI.parseRawMessagesResponse(responseData, from: snode, associatedWith: userPublicKey)
if !messages.isEmpty {
SNLog("Received \(messages.count) new message(s).")
}
@ -110,6 +112,10 @@ public final class Poller : NSObject {
SNLog("Failed to deserialize envelope due to error: \(error).")
}
}
// Now that the MessageReceiveJob's have been created we can update the `lastMessageHash` value
SnodeAPI.updateLastMessageHashValueIfPossible(for: snode, associatedWith: userPublicKey, from: lastRawMessage)
strongSelf.pollCount += 1
if strongSelf.pollCount == Poller.maxPollCount {
throw Error.pollLimitReached

@ -18,7 +18,7 @@ public protocol SessionMessagingKitStorageProtocol {
func getUserPublicKey() -> String?
func getUserKeyPair() -> ECKeyPair?
func getUserED25519KeyPair() -> Box.KeyPair?
func getUser() -> Contact?
func getUser(using transaction: YapDatabaseReadTransaction?) -> Contact?
// MARK: - Contacts
@ -44,9 +44,11 @@ public protocol SessionMessagingKitStorageProtocol {
func addClosedGroupEncryptionKeyPair(_ keyPair: ECKeyPair, for groupPublicKey: String, using transaction: Any)
func removeAllClosedGroupEncryptionKeyPairs(for groupPublicKey: String, using transaction: Any)
func getUserClosedGroupPublicKeys() -> Set<String>
func getUserClosedGroupPublicKeys(using transaction: YapDatabaseReadTransaction) -> Set<String>
func getZombieMembers(for groupPublicKey: String) -> Set<String>
func setZombieMembers(for groupPublicKey: String, to zombies: Set<String>, using transaction: Any)
func isClosedGroup(_ publicKey: String) -> Bool
func isClosedGroup(_ publicKey: String, using transaction: YapDatabaseReadTransaction) -> Bool
// MARK: - Jobs
@ -92,6 +94,13 @@ public protocol SessionMessagingKitStorageProtocol {
func setOpenGroupSequenceNumber(for room: String, on server: String, to newValue: Int64, using transaction: Any)
func removeOpenGroupSequenceNumber(for room: String, on server: String, using transaction: Any)
// MARK: - OpenGroupServerIdToUniqueIdLookup
func getOpenGroupServerIdLookup(_ serverId: UInt64, in room: String, on server: String, using transaction: YapDatabaseReadTransaction) -> OpenGroupServerIdLookup?
func addOpenGroupServerIdLookup(_ serverId: UInt64?, tsMessageId: String?, in room: String, on server: String, using transaction: YapDatabaseReadWriteTransaction)
func addOpenGroupServerIdLookup(_ lookup: OpenGroupServerIdLookup, using transaction: YapDatabaseReadWriteTransaction)
func removeOpenGroupServerIdLookup(_ serverId: UInt64, in room: String, on server: String, using transaction: YapDatabaseReadWriteTransaction)
// MARK: - -- Open Group Inbox Latest Message Id
func getOpenGroupInboxLatestMessageId(for server: String) -> Int64?

@ -1,7 +1,23 @@
import Foundation
public enum General {
public enum Cache {
public static var cachedEncodedPublicKey: Atomic<String?> = Atomic(nil)
}
}
@objc(SNGeneralUtilities)
public class GeneralUtilities: NSObject {
@objc public static func getUserPublicKey() -> String {
return getUserHexEncodedPublicKey()
}
}
public func getUserHexEncodedPublicKey(using dependencies: Dependencies = Dependencies()) -> String {
if let cachedKey: String = General.Cache.cachedEncodedPublicKey.wrappedValue { return cachedKey }
if let keyPair = dependencies.identityManager.identityKeyPair() { // Can be nil under some circumstances
General.Cache.cachedEncodedPublicKey.mutate { $0 = keyPair.hexEncodedPublicKey }
return keyPair.hexEncodedPublicKey
}

@ -1,11 +0,0 @@
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface SNGeneralUtilities : NSObject
+ (NSString *)getUserPublicKey;
@end
NS_ASSUME_NONNULL_END

@ -1,16 +0,0 @@
#import <SessionUtilitiesKit/SessionUtilitiesKit.h>
#import "GeneralUtilities.h"
#import "OWSIdentityManager.h"
NS_ASSUME_NONNULL_BEGIN
@implementation SNGeneralUtilities
+ (NSString *)getUserPublicKey
{
return OWSIdentityManager.sharedManager.identityKeyPair.hexEncodedPublicKey;
}
@end
NS_ASSUME_NONNULL_END

@ -13,14 +13,14 @@ public class NSENotificationPresenter: NSObject, NotificationsProtocol {
// 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] {
if !thread.isGroupThread() && thread.isMessageRequest(using: transaction) && !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 == 0 else { return }
}
else if thread.isMessageRequest() && CurrentAppContext().appUserDefaults()[.hasHiddenMessageRequests] {
else if thread.isMessageRequest(using: transaction) && CurrentAppContext().appUserDefaults()[.hasHiddenMessageRequests] {
// If there are other interactions on this thread already then don't show the notification
if thread.numberOfInteractions(with: transaction) > 1 { return }
@ -28,7 +28,7 @@ public class NSENotificationPresenter: NSObject, NotificationsProtocol {
}
let senderPublicKey = incomingMessage.authorId
let userPublicKey = SNGeneralUtilities.getUserPublicKey()
let userPublicKey = GeneralUtilities.getUserPublicKey()
guard senderPublicKey != userPublicKey else {
// Ignore PNs for messages sent by the current user
// after handling the message. Otherwise the closed
@ -37,7 +37,7 @@ public class NSENotificationPresenter: NSObject, NotificationsProtocol {
}
let context = Contact.context(for: thread)
let senderName = Storage.shared.getContact(with: senderPublicKey)?.displayName(for: context) ?? senderPublicKey
let senderName = Storage.shared.getContact(with: senderPublicKey, using: transaction)?.displayName(for: context) ?? senderPublicKey
var notificationTitle = senderName
if let group = thread as? TSGroupThread {
@ -85,7 +85,7 @@ public class NSENotificationPresenter: NSObject, NotificationsProtocol {
// 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() {
if thread.isMessageRequest(using: transaction) {
notificationContent.title = "Session"
notificationContent.body = "MESSAGE_REQUESTS_NOTIFICATION".localized()
}
@ -93,8 +93,16 @@ public class NSENotificationPresenter: NSObject, NotificationsProtocol {
// Add request
let identifier = incomingMessage.notificationIdentifier ?? UUID().uuidString
let request = UNNotificationRequest(identifier: identifier, content: notificationContent, trigger: nil)
SNLog("Add remote notification request")
UNUserNotificationCenter.current().add(request)
SNLog("Add remote notification request: \(notificationContent.body)")
let semaphore = DispatchSemaphore(value: 0)
UNUserNotificationCenter.current().add(request) { error in
if let error = error {
SNLog("Failed to add notification request due to error:\(error)")
}
semaphore.signal()
}
semaphore.wait()
SNLog("Finish adding remote notification request")
}
public func cancelNotification(_ identifier: String) {

@ -41,7 +41,10 @@ public final class NotificationServiceExtension : UNNotificationServiceExtension
let envelope = try? MessageWrapper.unwrap(data: data), let envelopeAsData = try? envelope.serializedData() else {
return self.handleFailure(for: notificationContent)
}
Storage.write { transaction in // Intentionally capture self
// HACK: It is important to use writeSync() here to avoid a race condition
// where the completeSilenty() is called before the local notification request
// is added to notification center.
Storage.writeSync { transaction in // Intentionally capture self
do {
let (message, proto) = try MessageReceiver.parse(envelopeAsData, openGroupMessageServerID: nil, using: transaction)
switch message {

@ -162,7 +162,7 @@ final class ThreadPickerVC: UIViewController, UITableViewDataSource, UITableView
message.sentTimestamp = NSDate.millisecondTimestamp()
message.text = (isSharingUrl && (messageText?.isEmpty == true || attachments[0].linkPreviewDraft == nil) ?
(
(messageText?.isEmpty == true ?
(messageText?.isEmpty == true || (attachments[0].text() == messageText) ?
attachments[0].text() :
"\(attachments[0].text() ?? "")\n\n\(messageText ?? "")"
)

@ -186,7 +186,11 @@ public enum OnionRequestAPI: OnionRequestAPIType {
} else {
return buildPaths(reusing: []).map2 { paths in
if let snode = snode {
return paths.filter { !$0.contains(snode) }.randomElement()!
if let path = paths.filter({ !$0.contains(snode) }).randomElement() {
return path
} else {
throw Error.insufficientSnodes
}
} else {
return paths.randomElement()!
}

@ -590,23 +590,26 @@ public final class SnodeAPI : NSObject {
})
}
public static func parseRawMessagesResponse(_ responseData: Data, from snode: Snode, associatedWith publicKey: String) -> [JSON] {
public static func parseRawMessagesResponse(_ responseData: Data, from snode: Snode, associatedWith publicKey: String) -> (messages: [JSON], lastRawMessage: JSON?) {
guard let responseJson: JSON = try? JSONSerialization.jsonObject(with: responseData, options: [ .fragmentsAllowed ]) as? JSON else {
return []
return ([], nil)
}
guard let rawMessages = responseJson["messages"] as? [JSON] else { return [] }
updateLastMessageHashValueIfPossible(for: snode, associatedWith: publicKey, from: rawMessages)
return removeDuplicates(from: rawMessages, associatedWith: publicKey)
guard let rawMessages = responseJson["messages"] as? [JSON] else { return ([], nil) }
return (
removeDuplicates(from: rawMessages, associatedWith: publicKey),
rawMessages.last
)
}
private static func updateLastMessageHashValueIfPossible(for snode: Snode, associatedWith publicKey: String, from rawMessages: [JSON]) {
if let lastMessage = rawMessages.last, let lastHash = lastMessage["hash"] as? String, let expirationDate = lastMessage["expiration"] as? UInt64 {
public static func updateLastMessageHashValueIfPossible(for snode: Snode, associatedWith publicKey: String, from lastRawMessage: JSON?) {
if let lastMessage = lastRawMessage, let lastHash = lastMessage["hash"] as? String, let expirationDate = lastMessage["expiration"] as? UInt64 {
SNSnodeKitConfiguration.shared.storage.writeSync { transaction in
SNSnodeKitConfiguration.shared.storage.setLastMessageHashInfo(for: snode, associatedWith: publicKey,
to: [ "hash" : lastHash, "expirationDate" : NSNumber(value: expirationDate) ], using: transaction)
}
} else if (!rawMessages.isEmpty) {
SNLog("Failed to update last message hash value from: \(rawMessages).")
} else if (lastRawMessage != nil) {
SNLog("Failed to update last message hash value from: \(String(describing: lastRawMessage)).")
}
}

@ -14,12 +14,12 @@ import Foundation
public class Atomic<Value> {
private let queue: DispatchQueue = DispatchQueue(label: "io.oxen.\(UUID().uuidString)")
private var value: Value
/// In order to change the value you **must** use the `mutate` function
public var wrappedValue: Value {
return queue.sync { return value }
}
/// For more information see https://github.com/apple/swift-evolution/blob/master/proposals/0258-property-wrappers.md#projections
public var projectedValue: Atomic<Value> {
return self

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

@ -0,0 +1,48 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import Foundation
@objc(SNOpenGroupServerIdLookupMigration)
public class OpenGroupServerIdLookupMigration: OWSDatabaseMigration {
@objc
class func migrationId() -> String {
return "003"
}
override public func runUp(completion: @escaping OWSDatabaseMigrationCompletion) {
self.doMigrationAsync(completion: completion)
}
private func doMigrationAsync(completion: @escaping OWSDatabaseMigrationCompletion) {
var lookups: [OpenGroupServerIdLookup] = []
Storage.write(with: { transaction in
TSGroupThread.enumerateCollectionObjects(with: transaction) { object, _ in
guard let thread: TSGroupThread = object as? TSGroupThread else { return }
guard let threadId: String = thread.uniqueId else { return }
guard let openGroup: OpenGroupV2 = Storage.shared.getV2OpenGroup(for: threadId) else { return }
thread.enumerateInteractions(with: transaction) { interaction, _ in
guard let tsMessage: TSMessage = interaction as? TSMessage else { return }
guard let tsMessageId: String = tsMessage.uniqueId else { return }
lookups.append(
OpenGroupServerIdLookup(
server: openGroup.server,
room: openGroup.room,
serverId: tsMessage.openGroupServerMessageID,
tsMessageId: tsMessageId
)
)
}
}
lookups.forEach { lookup in
Storage.shared.addOpenGroupServerIdLookup(lookup, using: transaction)
}
self.save(with: transaction) // Intentionally capture self
}, completion: {
completion()
})
}
}

@ -16,7 +16,6 @@ NS_ASSUME_NONNULL_BEGIN
+ (instancetype)sharedManager;
- (NSUInteger)unreadMessagesCount;
- (NSUInteger)unreadMessageRequestCount;
- (NSUInteger)unreadMessagesCountExcept:(TSThread *)thread;
- (void)updateApplicationBadgeCount;

@ -93,27 +93,6 @@ NS_ASSUME_NONNULL_BEGIN
return count;
}
- (NSUInteger)unreadMessageRequestCount {
__block NSUInteger count = 0;
[LKStorage readWithBlock:^(YapDatabaseReadTransaction *transaction) {
YapDatabaseViewTransaction *unreadMessages = [transaction ext:TSUnreadDatabaseViewExtensionName];
NSArray<NSString *> *allGroups = [unreadMessages allGroups];
// FIXME: Confusingly, `allGroups` includes contact threads as well
for (NSString *groupID in allGroups) {
TSThread *thread = [TSThread fetchObjectWithUniqueID:groupID transaction:transaction];
// Only increase the count for message requests
if (![thread isMessageRequestUsingTransaction:transaction]) { continue; }
if ([unreadMessages numberOfItemsInGroup:groupID] > 0) {
count += 1;
}
}
}];
return count;
}
- (NSUInteger)unreadMessagesCountExcept:(TSThread *)thread
{
__block NSUInteger numberOfItems;

Loading…
Cancel
Save