diff --git a/Session.xcodeproj/project.pbxproj b/Session.xcodeproj/project.pbxproj index 8d93cf779..3ee37ed60 100644 --- a/Session.xcodeproj/project.pbxproj +++ b/Session.xcodeproj/project.pbxproj @@ -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 = ""; }; B8566C62256F55930045A0B9 /* OWSLinkPreview+Conversion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OWSLinkPreview+Conversion.swift"; sourceTree = ""; }; B8569AC225CB5D2900DBA3DB /* ConversationVC+Interaction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ConversationVC+Interaction.swift"; sourceTree = ""; }; - B8569AD225CBA13D00DBA3DB /* MediaTextOverlayView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaTextOverlayView.swift; sourceTree = ""; }; B8569AE225CBB19A00DBA3DB /* DocumentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DocumentView.swift; sourceTree = ""; }; B86BD08323399ACF000F5AE3 /* Modal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Modal.swift; sourceTree = ""; }; B86BD08523399CEF000F5AE3 /* SeedModal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SeedModal.swift; sourceTree = ""; }; @@ -1363,9 +1362,7 @@ B893063E2383961A005EAA8E /* ScanQRCodeWrapperVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScanQRCodeWrapperVC.swift; sourceTree = ""; }; B894D0742339EDCF00B4D94D /* NukeDataModal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NukeDataModal.swift; sourceTree = ""; }; B897621B25D201F7004F83B2 /* ScrollToBottomButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScrollToBottomButton.swift; sourceTree = ""; }; - B8AE75A325A6C6A6001A84D2 /* Data+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Data+Utilities.swift"; sourceTree = ""; }; - B8AE760925ABFB00001A84D2 /* GeneralUtilities.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneralUtilities.h; sourceTree = ""; }; - B8AE760A25ABFB5A001A84D2 /* GeneralUtilities.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = GeneralUtilities.m; sourceTree = ""; }; + B8AE75A325A6C6A6001A84D2 /* Data+Trimming.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Data+Trimming.swift"; sourceTree = ""; }; B8AF4BB326A5204600583500 /* SendSeedModal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendSeedModal.swift; sourceTree = ""; }; B8B32020258B1A650020074B /* Contact.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Contact.swift; sourceTree = ""; }; B8B32032258B235D0020074B /* Storage+Contacts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Storage+Contacts.swift"; sourceTree = ""; }; @@ -1956,6 +1953,8 @@ FD3C906C27E43C4B00CD579F /* MessageSenderEncryptionSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageSenderEncryptionSpec.swift; sourceTree = ""; }; FD3C906E27E43E8700CD579F /* MockBox.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockBox.swift; sourceTree = ""; }; FD3C907027E445E500CD579F /* MessageReceiverDecryptionSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageReceiverDecryptionSpec.swift; sourceTree = ""; }; + FD3C907227E8387300CD579F /* OpenGroupServerIdLookupMigration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenGroupServerIdLookupMigration.swift; sourceTree = ""; }; + FD3C907427E83AC200CD579F /* OpenGroupServerIdLookup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenGroupServerIdLookup.swift; sourceTree = ""; }; FD5D200E27AA2B6000FEA984 /* MessageRequestResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageRequestResponse.swift; sourceTree = ""; }; FD5D201027AA331F00FEA984 /* ConfigurationMessage+Convenience.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ConfigurationMessage+Convenience.swift"; sourceTree = ""; }; FD5D201D27B0D87C00FEA984 /* SessionId.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionId.swift; sourceTree = ""; }; @@ -2051,6 +2050,7 @@ FDC438CA27BB7DB100C60D73 /* UpdateMessageRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateMessageRequest.swift; sourceTree = ""; }; FDC438CC27BC641200C60D73 /* Set+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Set+Utilities.swift"; sourceTree = ""; }; FDC438CE27BCA45400C60D73 /* Server.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Server.swift; sourceTree = ""; }; + FDFD645727EC1F4000808CA1 /* Atomic.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Atomic.swift; sourceTree = ""; }; FEDBAE1B98C49BBE8C87F575 /* Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionUtilitiesKit.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionUtilitiesKit.debug.xcconfig"; path = "Pods/Target Support Files/Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionUtilitiesKit/Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionUtilitiesKit.debug.xcconfig"; sourceTree = ""; }; FF9BA33D021B115B1F5B4E46 /* Pods-SessionMessagingKit.app store release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SessionMessagingKit.app store release.xcconfig"; path = "Pods/Target Support Files/Pods-SessionMessagingKit/Pods-SessionMessagingKit.app store release.xcconfig"; sourceTree = ""; }; /* 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; diff --git a/Session/Conversations/ConversationVC+Interaction.swift b/Session/Conversations/ConversationVC+Interaction.swift index 89fc64fcd..75bebd8ef 100644 --- a/Session/Conversations/ConversationVC+Interaction.swift +++ b/Session/Conversations/ConversationVC+Interaction.swift @@ -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 = 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 = 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 = self.approveMessageRequestIfNeeded( - for: self.thread, - with: transaction, - isNewThread: !oldThreadShouldBeVisible, - timestamp: (sentTimestamp - 1) // Set 1ms earlier as this is used for sorting + let promise: Promise = 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 { + fileprivate func approveMessageRequestIfNeeded(for thread: TSThread?, isNewThread: Bool, timestamp: UInt64) -> Promise { 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 in + let (promise, seal) = Promise.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 = 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 = 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() { diff --git a/Session/Conversations/Message Cells/Content Views/LinkPreviewView.swift b/Session/Conversations/Message Cells/Content Views/LinkPreviewView.swift index a9bd6f3c5..2132d417b 100644 --- a/Session/Conversations/Message Cells/Content Views/LinkPreviewView.swift +++ b/Session/Conversations/Message Cells/Content Views/LinkPreviewView.swift @@ -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) } diff --git a/Session/Conversations/Message Cells/Content Views/MediaTextOverlayView.swift b/Session/Conversations/Message Cells/Content Views/MediaTextOverlayView.swift deleted file mode 100644 index a0c655a5f..000000000 --- a/Session/Conversations/Message Cells/Content Views/MediaTextOverlayView.swift +++ /dev/null @@ -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) - } -} diff --git a/Session/Conversations/Message Cells/VisibleMessageCell.swift b/Session/Conversations/Message Cells/VisibleMessageCell.swift index f20fd7fb4..8731377c6 100644 --- a/Session/Conversations/Message Cells/VisibleMessageCell.swift +++ b/Session/Conversations/Message Cells/VisibleMessageCell.swift @@ -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 { diff --git a/Session/Home/HomeVC.swift b/Session/Home/HomeVC.swift index f77fc8cff..30d58f363 100644 --- a/Session/Home/HomeVC.swift +++ b/Session/Home/HomeVC.swift @@ -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 diff --git a/Session/Home/NewConversationButtonSet.swift b/Session/Home/NewConversationButtonSet.swift index d0a82def5..b1f933479 100644 --- a/Session/Home/NewConversationButtonSet.swift +++ b/Session/Home/NewConversationButtonSet.swift @@ -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 diff --git a/Session/Meta/AppDelegate.swift b/Session/Meta/AppDelegate.swift index 30525183d..6f1182807 100644 --- a/Session/Meta/AppDelegate.swift +++ b/Session/Meta/AppDelegate.swift @@ -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 { - guard Storage.shared.getUser()?.name != nil, let configurationMessage = ConfigurationMessage.getCurrent(with: transaction) else { - return Promise.value(()) - } - + func forceSyncConfigurationNowIfNeeded() -> Promise { let destination = Message.Destination.contact(publicKey: getUserHexEncodedPublicKey()) let (promise, seal) = Promise.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 diff --git a/Session/Meta/MainAppContext.m b/Session/Meta/MainAppContext.m index 66948daf6..2fad58d41 100644 --- a/Session/Meta/MainAppContext.m +++ b/Session/Meta/MainAppContext.m @@ -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, ^{ diff --git a/Session/Settings/NukeDataModal.swift b/Session/Settings/NukeDataModal.swift index 7c86d629c..dba878a22 100644 --- a/Session/Settings/NukeDataModal.swift +++ b/Session/Settings/NukeDataModal.swift @@ -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 { diff --git a/Session/Shared/BaseVC.swift b/Session/Shared/BaseVC.swift index b2953ed36..0bccafb10 100644 --- a/Session/Shared/BaseVC.swift +++ b/Session/Shared/BaseVC.swift @@ -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) } diff --git a/Session/Shared/ConversationCell.swift b/Session/Shared/ConversationCell.swift index 760b2326f..152a442c3 100644 --- a/Session/Shared/ConversationCell.swift +++ b/Session/Shared/ConversationCell.swift @@ -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 } } diff --git a/Session/Shared/UserCell.swift b/Session/Shared/UserCell.swift index fb493fc5d..8fd199a28 100644 --- a/Session/Shared/UserCell.swift +++ b/Session/Shared/UserCell.swift @@ -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 } } diff --git a/Session/Utilities/BackgroundPoller.swift b/Session/Utilities/BackgroundPoller.swift index 903301354..cbfdf8147 100644 --- a/Session/Utilities/BackgroundPoller.swift +++ b/Session/Utilities/BackgroundPoller.swift @@ -34,7 +34,7 @@ public final class BackgroundPoller: NSObject { } private static func pollForMessages() -> Promise { - 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 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? 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 } } diff --git a/SessionMessagingKit/Database/Storage+ClosedGroups.swift b/SessionMessagingKit/Database/Storage+ClosedGroups.swift index 05e9fc1a5..a21d6d551 100644 --- a/SessionMessagingKit/Database/Storage+ClosedGroups.swift +++ b/SessionMessagingKit/Database/Storage+ClosedGroups.swift @@ -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 { var result: Set = [] 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 { + 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) + } } diff --git a/SessionMessagingKit/Database/Storage+OpenGroups.swift b/SessionMessagingKit/Database/Storage+OpenGroups.swift index 06143d0ba..ffe3b6042 100644 --- a/SessionMessagingKit/Database/Storage+OpenGroups.swift +++ b/SessionMessagingKit/Database/Storage+OpenGroups.swift @@ -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" diff --git a/SessionMessagingKit/Database/Storage+Shared.swift b/SessionMessagingKit/Database/Storage+Shared.swift index b2752d29d..d8f5de95d 100644 --- a/SessionMessagingKit/Database/Storage+Shared.swift +++ b/SessionMessagingKit/Database/Storage+Shared.swift @@ -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 } } diff --git a/SessionMessagingKit/Messages/Control Messages/ConfigurationMessage+Convenience.swift b/SessionMessagingKit/Messages/Control Messages/ConfigurationMessage+Convenience.swift index bd13ad885..1f37f57ac 100644 --- a/SessionMessagingKit/Messages/Control Messages/ConfigurationMessage+Convenience.swift +++ b/SessionMessagingKit/Messages/Control Messages/ConfigurationMessage+Convenience.swift @@ -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 = [] var contacts: Set = [] - 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, diff --git a/SessionMessagingKit/Meta/SessionMessagingKit.h b/SessionMessagingKit/Meta/SessionMessagingKit.h index f989c52e6..498e820cb 100644 --- a/SessionMessagingKit/Meta/SessionMessagingKit.h +++ b/SessionMessagingKit/Meta/SessionMessagingKit.h @@ -5,7 +5,6 @@ FOUNDATION_EXPORT const unsigned char SessionMessagingKitVersionString[]; #import #import -#import #import #import #import diff --git a/SessionMessagingKit/Open Groups/OpenGroupManager.swift b/SessionMessagingKit/Open Groups/OpenGroupManager.swift index ff1c4cf70..3bca3c22e 100644 --- a/SessionMessagingKit/Open Groups/OpenGroupManager.swift +++ b/SessionMessagingKit/Open Groups/OpenGroupManager.swift @@ -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( diff --git a/SessionMessagingKit/Open Groups/OpenGroupServerIdLookup.swift b/SessionMessagingKit/Open Groups/OpenGroupServerIdLookup.swift new file mode 100644 index 000000000..ee79614af --- /dev/null +++ b/SessionMessagingKit/Open Groups/OpenGroupServerIdLookup.swift @@ -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)" + } +} diff --git a/SessionMessagingKit/Sending & Receiving/MessageReceiver+Handling.swift b/SessionMessagingKit/Sending & Receiving/MessageReceiver+Handling.swift index a55c8df5d..5ffc42924 100644 --- a/SessionMessagingKit/Sending & Receiving/MessageReceiver+Handling.swift +++ b/SessionMessagingKit/Sending & Receiving/MessageReceiver+Handling.swift @@ -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() } } diff --git a/SessionMessagingKit/Sending & Receiving/MessageSender.swift b/SessionMessagingKit/Sending & Receiving/MessageSender.swift index 8f37fe51c..b0c153ea0 100644 --- a/SessionMessagingKit/Sending & Receiving/MessageSender.swift +++ b/SessionMessagingKit/Sending & Receiving/MessageSender.swift @@ -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! ] diff --git a/SessionMessagingKit/Sending & Receiving/Pollers/ClosedGroupPoller.swift b/SessionMessagingKit/Sending & Receiving/Pollers/ClosedGroupPoller.swift index eb5966cd5..1644488e6 100644 --- a/SessionMessagingKit/Sending & Receiving/Pollers/ClosedGroupPoller.swift +++ b/SessionMessagingKit/Sending & Receiving/Pollers/ClosedGroupPoller.swift @@ -100,15 +100,17 @@ public final class ClosedGroupPoller : NSObject { private func poll(_ groupPublicKey: String) -> Promise { 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).") diff --git a/SessionMessagingKit/Sending & Receiving/Pollers/Poller.swift b/SessionMessagingKit/Sending & Receiving/Pollers/Poller.swift index a80d428a7..91e48bfbb 100644 --- a/SessionMessagingKit/Sending & Receiving/Pollers/Poller.swift +++ b/SessionMessagingKit/Sending & Receiving/Pollers/Poller.swift @@ -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 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 diff --git a/SessionMessagingKit/Storage.swift b/SessionMessagingKit/Storage.swift index ed64c2ada..f5391f221 100644 --- a/SessionMessagingKit/Storage.swift +++ b/SessionMessagingKit/Storage.swift @@ -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 + func getUserClosedGroupPublicKeys(using transaction: YapDatabaseReadTransaction) -> Set func getZombieMembers(for groupPublicKey: String) -> Set func setZombieMembers(for groupPublicKey: String, to zombies: Set, 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? diff --git a/SessionMessagingKit/Utilities/General.swift b/SessionMessagingKit/Utilities/General.swift index b7bee1ea3..61d164333 100644 --- a/SessionMessagingKit/Utilities/General.swift +++ b/SessionMessagingKit/Utilities/General.swift @@ -1,7 +1,23 @@ import Foundation +public enum General { + public enum Cache { + public static var cachedEncodedPublicKey: Atomic = 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 } diff --git a/SessionMessagingKit/Utilities/GeneralUtilities.h b/SessionMessagingKit/Utilities/GeneralUtilities.h deleted file mode 100644 index cb120b993..000000000 --- a/SessionMessagingKit/Utilities/GeneralUtilities.h +++ /dev/null @@ -1,11 +0,0 @@ -#import - -NS_ASSUME_NONNULL_BEGIN - -@interface SNGeneralUtilities : NSObject - -+ (NSString *)getUserPublicKey; - -@end - -NS_ASSUME_NONNULL_END diff --git a/SessionMessagingKit/Utilities/GeneralUtilities.m b/SessionMessagingKit/Utilities/GeneralUtilities.m deleted file mode 100644 index 777a6c3fd..000000000 --- a/SessionMessagingKit/Utilities/GeneralUtilities.m +++ /dev/null @@ -1,16 +0,0 @@ -#import -#import "GeneralUtilities.h" -#import "OWSIdentityManager.h" - -NS_ASSUME_NONNULL_BEGIN - -@implementation SNGeneralUtilities - -+ (NSString *)getUserPublicKey -{ - return OWSIdentityManager.sharedManager.identityKeyPair.hexEncodedPublicKey; -} - -@end - -NS_ASSUME_NONNULL_END diff --git a/SessionNotificationServiceExtension/NSENotificationPresenter.swift b/SessionNotificationServiceExtension/NSENotificationPresenter.swift index 6e330b94a..148876540 100644 --- a/SessionNotificationServiceExtension/NSENotificationPresenter.swift +++ b/SessionNotificationServiceExtension/NSENotificationPresenter.swift @@ -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) { diff --git a/SessionNotificationServiceExtension/NotificationServiceExtension.swift b/SessionNotificationServiceExtension/NotificationServiceExtension.swift index 51c179333..2da1ee44e 100644 --- a/SessionNotificationServiceExtension/NotificationServiceExtension.swift +++ b/SessionNotificationServiceExtension/NotificationServiceExtension.swift @@ -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 { diff --git a/SessionShareExtension/ThreadPickerVC.swift b/SessionShareExtension/ThreadPickerVC.swift index 0e0ff5db3..91a193a7e 100644 --- a/SessionShareExtension/ThreadPickerVC.swift +++ b/SessionShareExtension/ThreadPickerVC.swift @@ -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 ?? "")" ) diff --git a/SessionSnodeKit/OnionRequestAPI.swift b/SessionSnodeKit/OnionRequestAPI.swift index acceac62d..3cf0fbf92 100644 --- a/SessionSnodeKit/OnionRequestAPI.swift +++ b/SessionSnodeKit/OnionRequestAPI.swift @@ -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()! } diff --git a/SessionSnodeKit/SnodeAPI.swift b/SessionSnodeKit/SnodeAPI.swift index 6244f0e5b..e22b92aef 100644 --- a/SessionSnodeKit/SnodeAPI.swift +++ b/SessionSnodeKit/SnodeAPI.swift @@ -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)).") } } diff --git a/SessionUtilitiesKit/General/Atomic.swift b/SessionUtilitiesKit/General/Atomic.swift index e3c2bbf9b..db9f97acb 100644 --- a/SessionUtilitiesKit/General/Atomic.swift +++ b/SessionUtilitiesKit/General/Atomic.swift @@ -14,12 +14,12 @@ import Foundation public class Atomic { 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 { return self diff --git a/SignalUtilitiesKit/Database/Migrations/OWSDatabaseMigrationRunner.m b/SignalUtilitiesKit/Database/Migrations/OWSDatabaseMigrationRunner.m index a7d38515a..91b370f4e 100644 --- a/SignalUtilitiesKit/Database/Migrations/OWSDatabaseMigrationRunner.m +++ b/SignalUtilitiesKit/Database/Migrations/OWSDatabaseMigrationRunner.m @@ -26,6 +26,7 @@ NS_ASSUME_NONNULL_BEGIN - (NSArray *)allMigrations { return @[ + [SNOpenGroupServerIdLookupMigration new], [SNMessageRequestsMigration new], [SNContactsMigration new] ]; diff --git a/SignalUtilitiesKit/Database/Migrations/OpenGroupServerIdLookupMigration.swift b/SignalUtilitiesKit/Database/Migrations/OpenGroupServerIdLookupMigration.swift new file mode 100644 index 000000000..a98a0da07 --- /dev/null +++ b/SignalUtilitiesKit/Database/Migrations/OpenGroupServerIdLookupMigration.swift @@ -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() + }) + } +} diff --git a/SignalUtilitiesKit/Messaging/OWSMessageUtils.h b/SignalUtilitiesKit/Messaging/OWSMessageUtils.h index 3069360bf..ef693b8dc 100644 --- a/SignalUtilitiesKit/Messaging/OWSMessageUtils.h +++ b/SignalUtilitiesKit/Messaging/OWSMessageUtils.h @@ -16,7 +16,6 @@ NS_ASSUME_NONNULL_BEGIN + (instancetype)sharedManager; - (NSUInteger)unreadMessagesCount; -- (NSUInteger)unreadMessageRequestCount; - (NSUInteger)unreadMessagesCountExcept:(TSThread *)thread; - (void)updateApplicationBadgeCount; diff --git a/SignalUtilitiesKit/Messaging/OWSMessageUtils.m b/SignalUtilitiesKit/Messaging/OWSMessageUtils.m index cbc703c91..543971dbc 100644 --- a/SignalUtilitiesKit/Messaging/OWSMessageUtils.m +++ b/SignalUtilitiesKit/Messaging/OWSMessageUtils.m @@ -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 *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;