Fixed a number of issues found during internal testing

Added copy for an unrecoverable startup case
Added some additional logs to better debug ValueObservation query errors
Increased the pageSize to 20 on iPad devices (to prevent it immediately loading a second page)
Cleaned up a bunch of threading logic (try to avoid overriding subscribe/receive threads specified at subscription)
Consolidated the 'sendMessage' and 'sendAttachments' functions
Updated the various frameworks to use 'DAWRF with DSYM' to allow for better debugging during debug mode (at the cost of a longer build time)
Updated the logic to optimistically insert messages when sending to avoid any database write delays
Updated the logic to avoid sending notifications for messages which are already marked as read by the config
Fixed an issue where multiple paths could incorrectly get built at the same time in some cases
Fixed an issue where other job queues could be started before the blockingQueue finishes
Fixed a potential bug with the snode version comparison (was just a string comparison which would fail when getting to double-digit values)
Fixed a bug where you couldn't remove the last reaction on a message
Fixed the broken media message zoom animations
Fixed a bug where the last message read in a conversation wouldn't be correctly detected as already read
Fixed a bug where the QuoteView had no line limits (resulting in the '@You' mention background highlight being incorrectly positioned in the quote preview)
Fixed a bug where a large number of configSyncJobs could be scheduled (only one would run at a time but this could result in performance impacts)
This commit is contained in:
Morgan Pretty 2023-06-23 17:54:29 +10:00
parent 5db254303a
commit 53a5db0ea5
119 changed files with 2290 additions and 1376 deletions

@ -1 +1 @@
Subproject commit 49c78682a6f4546c8773113f3e201244f0b1e65a Subproject commit e0b994201a016cc5bf9065526a0ceb4291f60d5a

View File

@ -578,6 +578,10 @@
FD245C6B2850667400B966DD /* VisibleMessage+Profile.swift in Sources */ = {isa = PBXBuildFile; fileRef = C300A5B12554AF9800555489 /* VisibleMessage+Profile.swift */; }; FD245C6B2850667400B966DD /* VisibleMessage+Profile.swift in Sources */ = {isa = PBXBuildFile; fileRef = C300A5B12554AF9800555489 /* VisibleMessage+Profile.swift */; };
FD245C6C2850669200B966DD /* MessageReceiveJob.swift in Sources */ = {isa = PBXBuildFile; fileRef = C352A31225574F5200338F3E /* MessageReceiveJob.swift */; }; FD245C6C2850669200B966DD /* MessageReceiveJob.swift in Sources */ = {isa = PBXBuildFile; fileRef = C352A31225574F5200338F3E /* MessageReceiveJob.swift */; };
FD245C6D285066A400B966DD /* NotifyPushServerJob.swift in Sources */ = {isa = PBXBuildFile; fileRef = C352A32E2557549C00338F3E /* NotifyPushServerJob.swift */; }; FD245C6D285066A400B966DD /* NotifyPushServerJob.swift in Sources */ = {isa = PBXBuildFile; fileRef = C352A32E2557549C00338F3E /* NotifyPushServerJob.swift */; };
FD29598B2A43BB8100888A17 /* GetStatsResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD29598A2A43BB8100888A17 /* GetStatsResponse.swift */; };
FD29598D2A43BC0B00888A17 /* Version.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD29598C2A43BC0B00888A17 /* Version.swift */; };
FD2959902A43BE5F00888A17 /* VersionSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD29598F2A43BE5F00888A17 /* VersionSpec.swift */; };
FD2959922A4417A900888A17 /* PreparedSendData.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD2959912A4417A900888A17 /* PreparedSendData.swift */; };
FD2AAAED28ED3E1000A49611 /* MockGeneralCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDFD645C27F273F300808CA1 /* MockGeneralCache.swift */; }; FD2AAAED28ED3E1000A49611 /* MockGeneralCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDFD645C27F273F300808CA1 /* MockGeneralCache.swift */; };
FD2AAAEE28ED3E1100A49611 /* MockGeneralCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDFD645C27F273F300808CA1 /* MockGeneralCache.swift */; }; FD2AAAEE28ED3E1100A49611 /* MockGeneralCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDFD645C27F273F300808CA1 /* MockGeneralCache.swift */; };
FD2AAAF028ED57B500A49611 /* SynchronousStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD2AAAEF28ED57B500A49611 /* SynchronousStorage.swift */; }; FD2AAAF028ED57B500A49611 /* SynchronousStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD2AAAEF28ED57B500A49611 /* SynchronousStorage.swift */; };
@ -1281,7 +1285,6 @@
7BC707F127290ACB002817AD /* SessionCallManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionCallManager.swift; sourceTree = "<group>"; }; 7BC707F127290ACB002817AD /* SessionCallManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionCallManager.swift; sourceTree = "<group>"; };
7BCD116B27016062006330F1 /* WebRTCSession+DataChannel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "WebRTCSession+DataChannel.swift"; sourceTree = "<group>"; }; 7BCD116B27016062006330F1 /* WebRTCSession+DataChannel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "WebRTCSession+DataChannel.swift"; sourceTree = "<group>"; };
7BD477A727EC39F5004E2822 /* Atomic.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Atomic.swift; sourceTree = "<group>"; }; 7BD477A727EC39F5004E2822 /* Atomic.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Atomic.swift; sourceTree = "<group>"; };
7BD477A927F15F24004E2822 /* OpenGroupServerIdLookup.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OpenGroupServerIdLookup.swift; sourceTree = "<group>"; };
7BDCFC0424206E7300641C39 /* SessionNotificationServiceExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = SessionNotificationServiceExtension.entitlements; sourceTree = "<group>"; }; 7BDCFC0424206E7300641C39 /* SessionNotificationServiceExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = SessionNotificationServiceExtension.entitlements; sourceTree = "<group>"; };
7BDCFC07242186E700641C39 /* NotificationServiceExtensionContext.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NotificationServiceExtensionContext.swift; sourceTree = "<group>"; }; 7BDCFC07242186E700641C39 /* NotificationServiceExtensionContext.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NotificationServiceExtensionContext.swift; sourceTree = "<group>"; };
7BFA8AE22831D0D4001876F3 /* ContextMenuVC+EmojiReactsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ContextMenuVC+EmojiReactsView.swift"; sourceTree = "<group>"; }; 7BFA8AE22831D0D4001876F3 /* ContextMenuVC+EmojiReactsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ContextMenuVC+EmojiReactsView.swift"; sourceTree = "<group>"; };
@ -1725,6 +1728,10 @@
FD23EA6028ED0B260058676E /* CombineExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CombineExtensions.swift; sourceTree = "<group>"; }; FD23EA6028ED0B260058676E /* CombineExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CombineExtensions.swift; sourceTree = "<group>"; };
FD245C612850664300B966DD /* Configuration.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Configuration.swift; sourceTree = "<group>"; }; FD245C612850664300B966DD /* Configuration.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Configuration.swift; sourceTree = "<group>"; };
FD28A4F527EAD44C00FF65E7 /* Storage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Storage.swift; sourceTree = "<group>"; }; FD28A4F527EAD44C00FF65E7 /* Storage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Storage.swift; sourceTree = "<group>"; };
FD29598A2A43BB8100888A17 /* GetStatsResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GetStatsResponse.swift; sourceTree = "<group>"; };
FD29598C2A43BC0B00888A17 /* Version.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Version.swift; sourceTree = "<group>"; };
FD29598F2A43BE5F00888A17 /* VersionSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VersionSpec.swift; sourceTree = "<group>"; };
FD2959912A4417A900888A17 /* PreparedSendData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreparedSendData.swift; sourceTree = "<group>"; };
FD2AAAEF28ED57B500A49611 /* SynchronousStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SynchronousStorage.swift; sourceTree = "<group>"; }; FD2AAAEF28ED57B500A49611 /* SynchronousStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SynchronousStorage.swift; sourceTree = "<group>"; };
FD2B4AFA29429D1000AB4848 /* ConfigContactsSpec.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConfigContactsSpec.swift; sourceTree = "<group>"; }; FD2B4AFA29429D1000AB4848 /* ConfigContactsSpec.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConfigContactsSpec.swift; sourceTree = "<group>"; };
FD2B4AFC294688D000AB4848 /* SessionUtil+Contacts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SessionUtil+Contacts.swift"; sourceTree = "<group>"; }; FD2B4AFC294688D000AB4848 /* SessionUtil+Contacts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SessionUtil+Contacts.swift"; sourceTree = "<group>"; };
@ -3217,7 +3224,6 @@
FDC4381827B34EAD00C60D73 /* Models */, FDC4381827B34EAD00C60D73 /* Models */,
FDC4380727B31D3A00C60D73 /* Types */, FDC4380727B31D3A00C60D73 /* Types */,
FD3C907427E83AC200CD579F /* OpenGroupServerIdLookup.swift */, FD3C907427E83AC200CD579F /* OpenGroupServerIdLookup.swift */,
7BD477A927F15F24004E2822 /* OpenGroupServerIdLookup.swift */,
B88FA7B726045D100049422F /* OpenGroupAPI.swift */, B88FA7B726045D100049422F /* OpenGroupAPI.swift */,
C3DB66AB260ACA42001EFC55 /* OpenGroupManager.swift */, C3DB66AB260ACA42001EFC55 /* OpenGroupManager.swift */,
); );
@ -3609,6 +3615,7 @@
FD8ECF93293856AF00C0D1BB /* Randomness.swift */, FD8ECF93293856AF00C0D1BB /* Randomness.swift */,
C33FDB38255A580B00E217F9 /* OWSBackgroundTask.h */, C33FDB38255A580B00E217F9 /* OWSBackgroundTask.h */,
C33FDC1B255A581F00E217F9 /* OWSBackgroundTask.m */, C33FDC1B255A581F00E217F9 /* OWSBackgroundTask.m */,
FD29598C2A43BC0B00888A17 /* Version.swift */,
); );
path = Utilities; path = Utilities;
sourceTree = "<group>"; sourceTree = "<group>";
@ -3784,6 +3791,14 @@
path = Utilities; path = Utilities;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
FD29598E2A43BE5400888A17 /* Utilities */ = {
isa = PBXGroup;
children = (
FD29598F2A43BE5F00888A17 /* VersionSpec.swift */,
);
path = Utilities;
sourceTree = "<group>";
};
FD2B4B022949886900AB4848 /* Database */ = { FD2B4B022949886900AB4848 /* Database */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
@ -4033,6 +4048,7 @@
FD37EA1228AB3F60003AE748 /* Database */, FD37EA1228AB3F60003AE748 /* Database */,
FD83B9B927CF20A5005E1583 /* General */, FD83B9B927CF20A5005E1583 /* General */,
FD9B30F1293EA0AF008DEE3E /* Networking */, FD9B30F1293EA0AF008DEE3E /* Networking */,
FD29598E2A43BE5400888A17 /* Utilities */,
); );
path = SessionUtilitiesKitTests; path = SessionUtilitiesKitTests;
sourceTree = "<group>"; sourceTree = "<group>";
@ -4167,6 +4183,7 @@
FDC4381F27B36ADC00C60D73 /* SOGSEndpoint.swift */, FDC4381F27B36ADC00C60D73 /* SOGSEndpoint.swift */,
FDC4380827B31D4E00C60D73 /* OpenGroupAPIError.swift */, FDC4380827B31D4E00C60D73 /* OpenGroupAPIError.swift */,
FDC4381627B32EC700C60D73 /* Personalization.swift */, FDC4381627B32EC700C60D73 /* Personalization.swift */,
FD2959912A4417A900888A17 /* PreparedSendData.swift */,
FDC4381427B329CE00C60D73 /* NonceGenerator.swift */, FDC4381427B329CE00C60D73 /* NonceGenerator.swift */,
FDC438C227BB512200C60D73 /* SodiumProtocols.swift */, FDC438C227BB512200C60D73 /* SodiumProtocols.swift */,
); );
@ -4395,6 +4412,7 @@
FDF848A629405C5A007DCAE5 /* ONSResolveRequest.swift */, FDF848A629405C5A007DCAE5 /* ONSResolveRequest.swift */,
FDF8489E29405C5A007DCAE5 /* ONSResolveResponse.swift */, FDF8489E29405C5A007DCAE5 /* ONSResolveResponse.swift */,
FDF8489C29405C5A007DCAE5 /* GetServiceNodesRequest.swift */, FDF8489C29405C5A007DCAE5 /* GetServiceNodesRequest.swift */,
FD29598A2A43BB8100888A17 /* GetStatsResponse.swift */,
); );
path = Models; path = Models;
sourceTree = "<group>"; sourceTree = "<group>";
@ -5580,6 +5598,7 @@
FDF848CC29405C5B007DCAE5 /* SnodeReceivedMessage.swift in Sources */, FDF848CC29405C5B007DCAE5 /* SnodeReceivedMessage.swift in Sources */,
FDF848C129405C5A007DCAE5 /* UpdateExpiryRequest.swift in Sources */, FDF848C129405C5A007DCAE5 /* UpdateExpiryRequest.swift in Sources */,
FDF848C729405C5B007DCAE5 /* SendMessageResponse.swift in Sources */, FDF848C729405C5B007DCAE5 /* SendMessageResponse.swift in Sources */,
FD29598B2A43BB8100888A17 /* GetStatsResponse.swift in Sources */,
FDF848CA29405C5B007DCAE5 /* DeleteAllBeforeRequest.swift in Sources */, FDF848CA29405C5B007DCAE5 /* DeleteAllBeforeRequest.swift in Sources */,
FDF848D229405C5B007DCAE5 /* LegacyGetMessagesRequest.swift in Sources */, FDF848D229405C5B007DCAE5 /* LegacyGetMessagesRequest.swift in Sources */,
FDF848CB29405C5B007DCAE5 /* SnodePoolResponse.swift in Sources */, FDF848CB29405C5B007DCAE5 /* SnodePoolResponse.swift in Sources */,
@ -5696,6 +5715,7 @@
C3D9E4F4256778AF0040E4F3 /* NSData+Image.m in Sources */, C3D9E4F4256778AF0040E4F3 /* NSData+Image.m in Sources */,
FDF222092818D2B0000A4995 /* NSAttributedString+Utilities.swift in Sources */, FDF222092818D2B0000A4995 /* NSAttributedString+Utilities.swift in Sources */,
C32C5E0C256DDAFA003C73A2 /* NSRegularExpression+SSK.swift in Sources */, C32C5E0C256DDAFA003C73A2 /* NSRegularExpression+SSK.swift in Sources */,
FD29598D2A43BC0B00888A17 /* Version.swift in Sources */,
FDF8487C29405906007DCAE5 /* HTTPMethod.swift in Sources */, FDF8487C29405906007DCAE5 /* HTTPMethod.swift in Sources */,
FDF8488429405A2B007DCAE5 /* RequestInfo.swift in Sources */, FDF8488429405A2B007DCAE5 /* RequestInfo.swift in Sources */,
C3BBE0A92554D4DE0050F1E3 /* HTTP.swift in Sources */, C3BBE0A92554D4DE0050F1E3 /* HTTP.swift in Sources */,
@ -5892,6 +5912,7 @@
FD245C632850664600B966DD /* Configuration.swift in Sources */, FD245C632850664600B966DD /* Configuration.swift in Sources */,
C32C5DBF256DD743003C73A2 /* ClosedGroupPoller.swift in Sources */, C32C5DBF256DD743003C73A2 /* ClosedGroupPoller.swift in Sources */,
C352A35B2557824E00338F3E /* AttachmentUploadJob.swift in Sources */, C352A35B2557824E00338F3E /* AttachmentUploadJob.swift in Sources */,
FD2959922A4417A900888A17 /* PreparedSendData.swift in Sources */,
FD5C7305284F0FF30029977D /* MessageReceiver+VisibleMessages.swift in Sources */, FD5C7305284F0FF30029977D /* MessageReceiver+VisibleMessages.swift in Sources */,
FD432437299DEA38008A0213 /* TypeConversion+Utilities.swift in Sources */, FD432437299DEA38008A0213 /* TypeConversion+Utilities.swift in Sources */,
FD2B4B042949887A00AB4848 /* QueryInterfaceRequest+Utilities.swift in Sources */, FD2B4B042949887A00AB4848 /* QueryInterfaceRequest+Utilities.swift in Sources */,
@ -6156,6 +6177,7 @@
FD0B77B229B82B7A009169BA /* ArrayUtilitiesSpec.swift in Sources */, FD0B77B229B82B7A009169BA /* ArrayUtilitiesSpec.swift in Sources */,
FD1A94FE2900D2EA000D73D3 /* PersistableRecordUtilitiesSpec.swift in Sources */, FD1A94FE2900D2EA000D73D3 /* PersistableRecordUtilitiesSpec.swift in Sources */,
FDC290AA27D9B6FD005DAE71 /* Mock.swift in Sources */, FDC290AA27D9B6FD005DAE71 /* Mock.swift in Sources */,
FD2959902A43BE5F00888A17 /* VersionSpec.swift in Sources */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
@ -6395,7 +6417,7 @@
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
COPY_PHASE_STRIP = NO; COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 408; CURRENT_PROJECT_VERSION = 409;
DEBUG_INFORMATION_FORMAT = dwarf; DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = SUQ8J2PCT7; DEVELOPMENT_TEAM = SUQ8J2PCT7;
FRAMEWORK_SEARCH_PATHS = "$(inherited)"; FRAMEWORK_SEARCH_PATHS = "$(inherited)";
@ -6467,7 +6489,7 @@
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
COPY_PHASE_STRIP = NO; COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 408; CURRENT_PROJECT_VERSION = 409;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = SUQ8J2PCT7; DEVELOPMENT_TEAM = SUQ8J2PCT7;
ENABLE_NS_ASSERTIONS = NO; ENABLE_NS_ASSERTIONS = NO;
@ -6532,7 +6554,7 @@
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
COPY_PHASE_STRIP = NO; COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 408; CURRENT_PROJECT_VERSION = 409;
DEBUG_INFORMATION_FORMAT = dwarf; DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = SUQ8J2PCT7; DEVELOPMENT_TEAM = SUQ8J2PCT7;
FRAMEWORK_SEARCH_PATHS = "$(inherited)"; FRAMEWORK_SEARCH_PATHS = "$(inherited)";
@ -6606,7 +6628,7 @@
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
COPY_PHASE_STRIP = NO; COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 408; CURRENT_PROJECT_VERSION = 409;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = SUQ8J2PCT7; DEVELOPMENT_TEAM = SUQ8J2PCT7;
ENABLE_NS_ASSERTIONS = NO; ENABLE_NS_ASSERTIONS = NO;
@ -6671,7 +6693,7 @@
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
COPY_PHASE_STRIP = NO; COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 1; CURRENT_PROJECT_VERSION = 1;
DEBUG_INFORMATION_FORMAT = dwarf; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEFINES_MODULE = YES; DEFINES_MODULE = YES;
DEVELOPMENT_TEAM = SUQ8J2PCT7; DEVELOPMENT_TEAM = SUQ8J2PCT7;
DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_COMPATIBILITY_VERSION = 1;
@ -6808,7 +6830,7 @@
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
COPY_PHASE_STRIP = NO; COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 1; CURRENT_PROJECT_VERSION = 1;
DEBUG_INFORMATION_FORMAT = dwarf; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEFINES_MODULE = YES; DEFINES_MODULE = YES;
DEVELOPMENT_TEAM = SUQ8J2PCT7; DEVELOPMENT_TEAM = SUQ8J2PCT7;
DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_COMPATIBILITY_VERSION = 1;
@ -6959,7 +6981,7 @@
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
COPY_PHASE_STRIP = NO; COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 1; CURRENT_PROJECT_VERSION = 1;
DEBUG_INFORMATION_FORMAT = dwarf; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEFINES_MODULE = YES; DEFINES_MODULE = YES;
DEVELOPMENT_TEAM = SUQ8J2PCT7; DEVELOPMENT_TEAM = SUQ8J2PCT7;
DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_COMPATIBILITY_VERSION = 1;
@ -7096,7 +7118,7 @@
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
COPY_PHASE_STRIP = NO; COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 1; CURRENT_PROJECT_VERSION = 1;
DEBUG_INFORMATION_FORMAT = dwarf; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEFINES_MODULE = YES; DEFINES_MODULE = YES;
DEVELOPMENT_TEAM = SUQ8J2PCT7; DEVELOPMENT_TEAM = SUQ8J2PCT7;
DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_COMPATIBILITY_VERSION = 1;
@ -7235,7 +7257,7 @@
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
COPY_PHASE_STRIP = NO; COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 1; CURRENT_PROJECT_VERSION = 1;
DEBUG_INFORMATION_FORMAT = dwarf; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEFINES_MODULE = YES; DEFINES_MODULE = YES;
DEVELOPMENT_TEAM = SUQ8J2PCT7; DEVELOPMENT_TEAM = SUQ8J2PCT7;
DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_COMPATIBILITY_VERSION = 1;
@ -7514,7 +7536,7 @@
CODE_SIGN_ENTITLEMENTS = Session/Meta/Signal.entitlements; CODE_SIGN_ENTITLEMENTS = Session/Meta/Signal.entitlements;
CODE_SIGN_IDENTITY = "iPhone Developer"; CODE_SIGN_IDENTITY = "iPhone Developer";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CURRENT_PROJECT_VERSION = 408; CURRENT_PROJECT_VERSION = 409;
DEVELOPMENT_TEAM = SUQ8J2PCT7; DEVELOPMENT_TEAM = SUQ8J2PCT7;
FRAMEWORK_SEARCH_PATHS = ( FRAMEWORK_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
@ -7585,7 +7607,7 @@
CODE_SIGN_ENTITLEMENTS = Session/Meta/Signal.entitlements; CODE_SIGN_ENTITLEMENTS = Session/Meta/Signal.entitlements;
CODE_SIGN_IDENTITY = "iPhone Developer"; CODE_SIGN_IDENTITY = "iPhone Developer";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CURRENT_PROJECT_VERSION = 408; CURRENT_PROJECT_VERSION = 409;
DEVELOPMENT_TEAM = SUQ8J2PCT7; DEVELOPMENT_TEAM = SUQ8J2PCT7;
FRAMEWORK_SEARCH_PATHS = ( FRAMEWORK_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",

View File

@ -214,7 +214,10 @@ extension ContextMenuVC {
let shouldShowEmojiActions: Bool = { let shouldShowEmojiActions: Bool = {
if cellViewModel.threadVariant == .community { if cellViewModel.threadVariant == .community {
return OpenGroupManager.isOpenGroupSupport(.reactions, on: cellViewModel.threadOpenGroupServer) return OpenGroupManager.doesOpenGroupSupport(
capability: .reactions,
on: cellViewModel.threadOpenGroupServer
)
} }
return !currentThreadIsMessageRequest return !currentThreadIsMessageRequest
}() }()

View File

@ -150,10 +150,17 @@ extension ConversationVC:
} }
func sendMediaNav(_ sendMediaNavigationController: SendMediaNavigationController, didApproveAttachments attachments: [SignalAttachment], forThreadId threadId: String, messageText: String?) { func sendMediaNav(_ sendMediaNavigationController: SendMediaNavigationController, didApproveAttachments attachments: [SignalAttachment], forThreadId threadId: String, messageText: String?) {
sendAttachments(attachments, with: messageText ?? "") sendMessage(text: (messageText ?? ""), attachments: attachments)
self.snInputView.text = ""
resetMentions() resetMentions()
dismiss(animated: true) { }
dismiss(animated: true) { [weak self] in
if self?.isFirstResponder == false {
self?.becomeFirstResponder()
}
else {
self?.reloadInputViews()
}
}
} }
func sendMediaNavInitialMessageText(_ sendMediaNavigationController: SendMediaNavigationController) -> String? { func sendMediaNavInitialMessageText(_ sendMediaNavigationController: SendMediaNavigationController) -> String? {
@ -167,13 +174,17 @@ extension ConversationVC:
// MARK: - AttachmentApprovalViewControllerDelegate // MARK: - AttachmentApprovalViewControllerDelegate
func attachmentApproval(_ attachmentApproval: AttachmentApprovalViewController, didApproveAttachments attachments: [SignalAttachment], forThreadId threadId: String, messageText: String?) { func attachmentApproval(_ attachmentApproval: AttachmentApprovalViewController, didApproveAttachments attachments: [SignalAttachment], forThreadId threadId: String, messageText: String?) {
sendAttachments(attachments, with: messageText ?? "") { [weak self] in sendMessage(text: (messageText ?? ""), attachments: attachments)
self?.dismiss(animated: true, completion: nil)
}
scrollToBottom(isAnimated: false)
self.snInputView.text = ""
resetMentions() resetMentions()
dismiss(animated: true) { [weak self] in
if self?.isFirstResponder == false {
self?.becomeFirstResponder()
}
else {
self?.reloadInputViews()
}
}
} }
func attachmentApprovalDidCancel(_ attachmentApproval: AttachmentApprovalViewController) { func attachmentApprovalDidCancel(_ attachmentApproval: AttachmentApprovalViewController) {
@ -181,7 +192,7 @@ extension ConversationVC:
} }
func attachmentApproval(_ attachmentApproval: AttachmentApprovalViewController, didChangeMessageText newMessageText: String?) { func attachmentApproval(_ attachmentApproval: AttachmentApprovalViewController, didChangeMessageText newMessageText: String?) {
snInputView.text = newMessageText ?? "" snInputView.text = (newMessageText ?? "")
} }
func attachmentApproval(_ attachmentApproval: AttachmentApprovalViewController, didRemoveAttachment attachment: SignalAttachment) { func attachmentApproval(_ attachmentApproval: AttachmentApprovalViewController, didRemoveAttachment attachment: SignalAttachment) {
@ -348,6 +359,7 @@ extension ConversationVC:
attachments: attachments, attachments: attachments,
approvalDelegate: self approvalDelegate: self
) )
navController.modalPresentationStyle = .fullScreen
present(navController, animated: true, completion: nil) present(navController, animated: true, completion: nil)
} }
@ -369,7 +381,7 @@ extension ConversationVC:
modalActivityIndicator.dismiss { modalActivityIndicator.dismiss {
guard !attachment.hasError else { guard !attachment.hasError else {
self?.showErrorAlert(for: attachment, onDismiss: nil) self?.showErrorAlert(for: attachment)
return return
} }
@ -385,149 +397,33 @@ extension ConversationVC:
// MARK: --Message Sending // MARK: --Message Sending
func handleSendButtonTapped() { func handleSendButtonTapped() {
sendMessage() sendMessage(
} text: snInputView.text.trimmingCharacters(in: .whitespacesAndNewlines),
linkPreviewDraft: snInputView.linkPreviewInfo?.draft,
func sendMessage(hasPermissionToSendSeed: Bool = false) { quoteModel: snInputView.quoteDraftInfo?.model
guard !showBlockedModalIfNeeded() else { return }
let text = replaceMentions(in: snInputView.text.trimmingCharacters(in: .whitespacesAndNewlines))
guard !text.isEmpty else { return }
if text.contains(mnemonic) && !viewModel.threadData.threadIsNoteToSelf && !hasPermissionToSendSeed {
// Warn the user if they're about to send their seed to someone
let modal: ConfirmationModal = ConfirmationModal(
info: ConfirmationModal.Info(
title: "modal_send_seed_title".localized(),
body: .text("modal_send_seed_explanation".localized()),
confirmTitle: "modal_send_seed_send_button_title".localized(),
confirmStyle: .danger,
cancelStyle: .alert_text,
onConfirm: { [weak self] _ in self?.sendMessage(hasPermissionToSendSeed: true) }
)
)
return present(modal, animated: true, completion: nil)
}
// Clearing this out immediately to make this appear more snappy
DispatchQueue.main.async { [weak self] in
self?.snInputView.text = ""
self?.snInputView.quoteDraftInfo = nil
self?.resetMentions()
}
// Note: 'shouldBeVisible' is set to true the first time a thread is saved so we can
// use it to determine if the user is creating a new thread and update the 'isApproved'
// flags appropriately
let threadId: String = self.viewModel.threadData.threadId
let threadVariant: SessionThread.Variant = self.viewModel.threadData.threadVariant
let oldThreadShouldBeVisible: Bool = (self.viewModel.threadData.threadShouldBeVisible == true)
let sentTimestampMs: Int64 = SnodeAPI.currentOffsetTimestampMs()
let linkPreviewDraft: LinkPreviewDraft? = snInputView.linkPreviewInfo?.draft
let quoteModel: QuotedReplyModel? = snInputView.quoteDraftInfo?.model
// If this was a message request then approve it
approveMessageRequestIfNeeded(
for: threadId,
threadVariant: self.viewModel.threadData.threadVariant,
isNewThread: !oldThreadShouldBeVisible,
timestampMs: (sentTimestampMs - 1) // Set 1ms earlier as this is used for sorting
) )
// Send the message
Storage.shared
.writePublisher { [weak self] db in
// Let the viewModel know we are about to send a message
self?.viewModel.sentMessageBeforeUpdate = true
// Update the thread to be visible (if it isn't already)
if self?.viewModel.threadData.threadShouldBeVisible == false {
_ = try SessionThread
.filter(id: threadId)
.updateAllAndConfig(db, SessionThread.Columns.shouldBeVisible.set(to: true))
}
let authorId: String = {
if let blindedId = self?.viewModel.threadData.currentUserBlindedPublicKey {
return blindedId
}
return self?.viewModel.threadData.currentUserPublicKey ?? getUserHexEncodedPublicKey(db)
}()
// Create the interaction
let interaction: Interaction = try Interaction(
threadId: threadId,
authorId: authorId,
variant: .standardOutgoing,
body: text,
timestampMs: sentTimestampMs,
hasMention: Interaction.isUserMentioned(db, threadId: threadId, body: text),
expiresInSeconds: try? DisappearingMessagesConfiguration
.select(.durationSeconds)
.filter(id: threadId)
.filter(DisappearingMessagesConfiguration.Columns.isEnabled == true)
.asRequest(of: TimeInterval.self)
.fetchOne(db),
linkPreviewUrl: linkPreviewDraft?.urlString
).inserted(db)
// If there is a LinkPreview and it doesn't match an existing one then add it now
if
let linkPreviewDraft: LinkPreviewDraft = linkPreviewDraft,
(try? interaction.linkPreview.isEmpty(db)) == true
{
try LinkPreview(
url: linkPreviewDraft.urlString,
title: linkPreviewDraft.title,
attachmentId: LinkPreview.saveAttachmentIfPossible(
db,
imageData: linkPreviewDraft.jpegImageData,
mimeType: OWSMimeTypeImageJpeg
)
).insert(db)
}
// If there is a Quote the insert it now
if let interactionId: Int64 = interaction.id, let quoteModel: QuotedReplyModel = quoteModel {
try Quote(
interactionId: interactionId,
authorId: quoteModel.authorId,
timestampMs: quoteModel.timestampMs,
body: quoteModel.body,
attachmentId: quoteModel.generateAttachmentThumbnailIfNeeded(db)
).insert(db)
}
try MessageSender.send(
db,
interaction: interaction,
threadId: threadId,
threadVariant: threadVariant
)
}
.subscribe(on: DispatchQueue.global(qos: .userInitiated))
.sinkUntilComplete(
receiveCompletion: { [weak self] _ in
self?.handleMessageSent()
}
)
} }
func sendAttachments(_ attachments: [SignalAttachment], with text: String, hasPermissionToSendSeed: Bool = false, onComplete: (() -> ())? = nil) { func sendMessage(
text: String,
attachments: [SignalAttachment] = [],
linkPreviewDraft: LinkPreviewDraft? = nil,
quoteModel: QuotedReplyModel? = nil,
hasPermissionToSendSeed: Bool = false
) {
guard !showBlockedModalIfNeeded() else { return } guard !showBlockedModalIfNeeded() else { return }
for attachment in attachments { // Handle attachment errors if applicable
if attachment.hasError { if let failedAttachment: SignalAttachment = attachments.first(where: { $0.hasError }) {
return showErrorAlert(for: attachment, onDismiss: onComplete) return showErrorAlert(for: failedAttachment)
}
} }
let text = replaceMentions(in: snInputView.text.trimmingCharacters(in: .whitespacesAndNewlines)) let processedText: String = replaceMentions(in: text.trimmingCharacters(in: .whitespacesAndNewlines))
if text.contains(mnemonic) && !viewModel.threadData.threadIsNoteToSelf && !hasPermissionToSendSeed { // If we have no content then do nothing
guard !processedText.isEmpty || !attachments.isEmpty else { return }
if processedText.contains(mnemonic) && !viewModel.threadData.threadIsNoteToSelf && !hasPermissionToSendSeed {
// Warn the user if they're about to send their seed to someone // Warn the user if they're about to send their seed to someone
let modal: ConfirmationModal = ConfirmationModal( let modal: ConfirmationModal = ConfirmationModal(
info: ConfirmationModal.Info( info: ConfirmationModal.Info(
@ -537,7 +433,13 @@ extension ConversationVC:
confirmStyle: .danger, confirmStyle: .danger,
cancelStyle: .alert_text, cancelStyle: .alert_text,
onConfirm: { [weak self] _ in onConfirm: { [weak self] _ in
self?.sendAttachments(attachments, with: text, hasPermissionToSendSeed: true, onComplete: onComplete) self?.sendMessage(
text: text,
attachments: attachments,
linkPreviewDraft: linkPreviewDraft,
quoteModel: quoteModel,
hasPermissionToSendSeed: true
)
} }
) )
) )
@ -564,17 +466,24 @@ extension ConversationVC:
// If this was a message request then approve it // If this was a message request then approve it
approveMessageRequestIfNeeded( approveMessageRequestIfNeeded(
for: threadId, for: threadId,
threadVariant: self.viewModel.threadData.threadVariant, threadVariant: threadVariant,
isNewThread: !oldThreadShouldBeVisible, isNewThread: !oldThreadShouldBeVisible,
timestampMs: (sentTimestampMs - 1) // Set 1ms earlier as this is used for sorting timestampMs: (sentTimestampMs - 1) // Set 1ms earlier as this is used for sorting
) )
// Send the message // Optimistically insert the outgoing message (this will trigger a UI update)
self.viewModel.sentMessageBeforeUpdate = true
let optimisticData: ConversationViewModel.OptimisticMessageData = self.viewModel.optimisticallyAppendOutgoingMessage(
text: processedText,
sentTimestampMs: sentTimestampMs,
attachments: attachments,
linkPreviewDraft: linkPreviewDraft,
quoteModel: quoteModel
)
// Actually send the message
Storage.shared Storage.shared
.writePublisher { [weak self] db in .writePublisher { [weak self] db in
// Let the viewModel know we are about to send a message
self?.viewModel.sentMessageBeforeUpdate = true
// Update the thread to be visible (if it isn't already) // Update the thread to be visible (if it isn't already)
if self?.viewModel.threadData.threadShouldBeVisible == false { if self?.viewModel.threadData.threadShouldBeVisible == false {
_ = try SessionThread _ = try SessionThread
@ -582,35 +491,44 @@ extension ConversationVC:
.updateAllAndConfig(db, SessionThread.Columns.shouldBeVisible.set(to: true)) .updateAllAndConfig(db, SessionThread.Columns.shouldBeVisible.set(to: true))
} }
// Create the interaction // Insert the interaction and associated it with the optimistically inserted message so
let interaction: Interaction = try Interaction( // we can remove it once the database triggers a UI update
threadId: threadId, let insertedInteraction: Interaction = try optimisticData.interaction.inserted(db)
authorId: getUserHexEncodedPublicKey(db), self?.viewModel.associate(optimisticMessageId: optimisticData.id, to: insertedInteraction.id)
variant: .standardOutgoing,
body: text,
timestampMs: sentTimestampMs,
hasMention: Interaction.isUserMentioned(db, threadId: threadId, body: text),
expiresInSeconds: try? DisappearingMessagesConfiguration
.select(.durationSeconds)
.filter(id: threadId)
.filter(DisappearingMessagesConfiguration.Columns.isEnabled == true)
.asRequest(of: TimeInterval.self)
.fetchOne(db)
).inserted(db)
guard let interactionId: Int64 = interaction.id else { return } // If there is a LinkPreview and it doesn't match an existing one then add it now
if
let linkPreviewDraft: LinkPreviewDraft = linkPreviewDraft,
(try? insertedInteraction.linkPreview.isEmpty(db)) == true
{
try LinkPreview(
url: linkPreviewDraft.urlString,
title: linkPreviewDraft.title,
attachmentId: try optimisticData.linkPreviewAttachment?.inserted(db).id
).insert(db)
}
// Prepare any attachments // If there is a Quote the insert it now
try Attachment.prepare( if let interactionId: Int64 = insertedInteraction.id, let quoteModel: QuotedReplyModel = quoteModel {
try Quote(
interactionId: interactionId,
authorId: quoteModel.authorId,
timestampMs: quoteModel.timestampMs,
body: quoteModel.body,
attachmentId: quoteModel.generateAttachmentThumbnailIfNeeded(db)
).insert(db)
}
// Process any attachments
try Attachment.process(
db, db,
attachments: attachments, data: optimisticData.attachmentData,
for: interactionId for: insertedInteraction.id
) )
// Send the message
try MessageSender.send( try MessageSender.send(
db, db,
interaction: interaction, interaction: insertedInteraction,
threadId: threadId, threadId: threadId,
threadVariant: threadVariant threadVariant: threadVariant
) )
@ -619,11 +537,6 @@ extension ConversationVC:
.sinkUntilComplete( .sinkUntilComplete(
receiveCompletion: { [weak self] _ in receiveCompletion: { [weak self] _ in
self?.handleMessageSent() self?.handleMessageSent()
// Attachment successfully sent - dismiss the screen
DispatchQueue.main.async {
onComplete?()
}
} }
) )
} }
@ -1212,7 +1125,7 @@ extension ConversationVC:
guard cellViewModel.threadVariant == .community else { return } guard cellViewModel.threadVariant == .community else { return }
Storage.shared Storage.shared
.readPublisherFlatMap { db -> AnyPublisher<(OpenGroupAPI.ReactionRemoveAllResponse, OpenGroupAPI.PendingChange), Error> in .readPublisher { db -> (OpenGroupAPI.PreparedSendData<OpenGroupAPI.ReactionRemoveAllResponse>, OpenGroupAPI.PendingChange) in
guard guard
let openGroup: OpenGroup = try? OpenGroup let openGroup: OpenGroup = try? OpenGroup
.fetchOne(db, id: cellViewModel.threadId), .fetchOne(db, id: cellViewModel.threadId),
@ -1223,6 +1136,14 @@ extension ConversationVC:
.fetchOne(db) .fetchOne(db)
else { throw StorageError.objectNotFound } else { throw StorageError.objectNotFound }
let sendData: OpenGroupAPI.PreparedSendData<OpenGroupAPI.ReactionRemoveAllResponse> = try OpenGroupAPI
.preparedReactionDeleteAll(
db,
emoji: emoji,
id: openGroupServerMessageId,
in: openGroup.roomToken,
on: openGroup.server
)
let pendingChange: OpenGroupAPI.PendingChange = OpenGroupManager let pendingChange: OpenGroupAPI.PendingChange = OpenGroupManager
.addPendingReaction( .addPendingReaction(
emoji: emoji, emoji: emoji,
@ -1232,27 +1153,22 @@ extension ConversationVC:
type: .removeAll type: .removeAll
) )
return OpenGroupAPI return (sendData, pendingChange)
.reactionDeleteAll(
db,
emoji: emoji,
id: openGroupServerMessageId,
in: openGroup.roomToken,
on: openGroup.server
)
.map { _, response in (response, pendingChange) }
.eraseToAnyPublisher()
} }
.subscribe(on: DispatchQueue.global(qos: .userInitiated)) .subscribe(on: DispatchQueue.global(qos: .userInitiated))
.handleEvents( .flatMap { sendData, pendingChange in
receiveOutput: { response, pendingChange in OpenGroupAPI.send(data: sendData)
OpenGroupManager .handleEvents(
.updatePendingChange( receiveOutput: { _, response in
pendingChange, OpenGroupManager
seqNo: response.seqNo .updatePendingChange(
) pendingChange,
} seqNo: response.seqNo
) )
}
)
.eraseToAnyPublisher()
}
.sinkUntilComplete( .sinkUntilComplete(
receiveCompletion: { _ in receiveCompletion: { _ in
Storage.shared.writeAsync { db in Storage.shared.writeAsync { db in
@ -1266,14 +1182,16 @@ extension ConversationVC:
} }
func react(_ cellViewModel: MessageViewModel, with emoji: String, remove: Bool) { func react(_ cellViewModel: MessageViewModel, with emoji: String, remove: Bool) {
guard cellViewModel.variant == .standardIncoming || cellViewModel.variant == .standardOutgoing else { guard
return self.viewModel.threadData.threadIsMessageRequest != true && (
} cellViewModel.variant == .standardIncoming ||
cellViewModel.variant == .standardOutgoing
let threadIsMessageRequest: Bool = (self.viewModel.threadData.threadIsMessageRequest == true) )
guard !threadIsMessageRequest else { return } else { return }
// Perform local rate limiting (don't allow more than 20 reactions within 60 seconds) // Perform local rate limiting (don't allow more than 20 reactions within 60 seconds)
let threadVariant: SessionThread.Variant = self.viewModel.threadData.threadVariant
let openGroupRoom: String? = self.viewModel.threadData.openGroupRoomToken
let sentTimestamp: Int64 = SnodeAPI.currentOffsetTimestampMs() let sentTimestamp: Int64 = SnodeAPI.currentOffsetTimestampMs()
let recentReactionTimestamps: [Int64] = General.cache.wrappedValue.recentReactionTimestamps let recentReactionTimestamps: [Int64] = General.cache.wrappedValue.recentReactionTimestamps
@ -1299,9 +1217,38 @@ extension ConversationVC:
.appending(sentTimestamp) .appending(sentTimestamp)
} }
// Perform the sending logic typealias OpenGroupInfo = (
Storage.shared pendingReaction: Reaction?,
.writePublisherFlatMap { [weak self] db -> AnyPublisher<MessageSender.PreparedSendData?, Error> in pendingChange: OpenGroupAPI.PendingChange,
sendData: OpenGroupAPI.PreparedSendData<Int64?>
)
/// Perform the sending logic, we generate the pending reaction first in a deferred future closure to prevent the OpenGroup
/// cache from blocking either the main thread or the database write thread
Deferred {
Future<OpenGroupAPI.PendingChange?, Error> { resolver in
guard
threadVariant == .community,
let serverMessageId: Int64 = cellViewModel.openGroupServerMessageId,
let openGroupServer: String = cellViewModel.threadOpenGroupServer,
let openGroupPublicKey: String = cellViewModel.threadOpenGroupPublicKey
else { return resolver(Result.success(nil)) }
// Create the pending change if we have open group info
return resolver(Result.success(
OpenGroupManager.addPendingReaction(
emoji: emoji,
id: serverMessageId,
in: openGroupServer,
on: openGroupPublicKey,
type: (remove ? .remove : .add)
)
))
}
}
.subscribe(on: DispatchQueue.global(qos: .userInitiated))
.flatMap { pendingChange -> AnyPublisher<(MessageSender.PreparedSendData?, OpenGroupInfo?), Error> in
Storage.shared.writePublisher { [weak self] db -> (MessageSender.PreparedSendData?, OpenGroupInfo?) in
// Update the thread to be visible (if it isn't already) // Update the thread to be visible (if it isn't already)
if self?.viewModel.threadData.threadShouldBeVisible == false { if self?.viewModel.threadData.threadShouldBeVisible == false {
_ = try SessionThread _ = try SessionThread
@ -1310,29 +1257,29 @@ extension ConversationVC:
} }
let pendingReaction: Reaction? = { let pendingReaction: Reaction? = {
if remove { guard !remove else {
return try? Reaction return try? Reaction
.filter(Reaction.Columns.interactionId == cellViewModel.id) .filter(Reaction.Columns.interactionId == cellViewModel.id)
.filter(Reaction.Columns.authorId == cellViewModel.currentUserPublicKey) .filter(Reaction.Columns.authorId == cellViewModel.currentUserPublicKey)
.filter(Reaction.Columns.emoji == emoji) .filter(Reaction.Columns.emoji == emoji)
.fetchOne(db) .fetchOne(db)
} else {
let sortId = Reaction.getSortId(
db,
interactionId: cellViewModel.id,
emoji: emoji
)
return Reaction(
interactionId: cellViewModel.id,
serverHash: nil,
timestampMs: sentTimestamp,
authorId: cellViewModel.currentUserPublicKey,
emoji: emoji,
count: 1,
sortId: sortId
)
} }
let sortId: Int64 = Reaction.getSortId(
db,
interactionId: cellViewModel.id,
emoji: emoji
)
return Reaction(
interactionId: cellViewModel.id,
serverHash: nil,
timestampMs: sentTimestamp,
authorId: cellViewModel.currentUserPublicKey,
emoji: emoji,
count: 1,
sortId: sortId
)
}() }()
// Update the database // Update the database
@ -1350,125 +1297,108 @@ extension ConversationVC:
Emoji.addRecent(db, emoji: emoji) Emoji.addRecent(db, emoji: emoji)
} }
// If it's not an OpenGroup then send the message directly to the thread switch threadVariant {
guard case .community:
let openGroup: OpenGroup = try? OpenGroup.fetchOne(db, id: cellViewModel.threadId), guard
OpenGroupManager.isOpenGroupSupport(.reactions, on: openGroup.server) let serverMessageId: Int64 = cellViewModel.openGroupServerMessageId,
else { let openGroupServer: String = cellViewModel.threadOpenGroupServer,
let sendData: MessageSender.PreparedSendData = try MessageSender.preparedSendData( let openGroupRoom: String = openGroupRoom,
db, let pendingChange: OpenGroupAPI.PendingChange = pendingChange,
message: VisibleMessage( OpenGroupManager.doesOpenGroupSupport(db, capability: .reactions, on: openGroupServer)
sentTimestamp: UInt64(sentTimestamp), else { throw MessageSenderError.invalidMessage }
text: nil,
reaction: VisibleMessage.VMReaction( let sendData: OpenGroupAPI.PreparedSendData<Int64?> = try {
timestamp: UInt64(cellViewModel.timestampMs), guard !remove else {
publicKey: { return try OpenGroupAPI
guard cellViewModel.variant == .standardIncoming else { .preparedReactionDelete(
return cellViewModel.currentUserPublicKey db,
} emoji: emoji,
id: serverMessageId,
return cellViewModel.authorId in: openGroupRoom,
}(), on: openGroupServer
emoji: emoji,
kind: (remove ? .remove : .react)
)
),
to: try Message.Destination
.from(db, threadId: cellViewModel.threadId, threadVariant: cellViewModel.threadVariant),
namespace: try Message.Destination
.from(db, threadId: cellViewModel.threadId, threadVariant: cellViewModel.threadVariant)
.defaultNamespace,
interactionId: cellViewModel.id
)
return Just(sendData)
.setFailureType(to: Error.self)
.eraseToAnyPublisher()
}
// Otherwise we need to make an API call to the OpenGroup
// Send reaction to open groups
guard
let openGroupServerMessageId: Int64 = try? Interaction
.select(.openGroupServerMessageId)
.filter(id: cellViewModel.id)
.asRequest(of: Int64.self)
.fetchOne(db)
else { throw MessageSenderError.invalidMessage }
let pendingChange: OpenGroupAPI.PendingChange = OpenGroupManager
.addPendingReaction(
emoji: emoji,
id: openGroupServerMessageId,
in: openGroup.roomToken,
on: openGroup.server,
type: (remove ? .remove : .add)
)
let request: AnyPublisher<Int64?, Error> = {
switch remove {
case true:
return OpenGroupAPI
.reactionDelete(
db,
emoji: emoji,
id: openGroupServerMessageId,
in: openGroup.roomToken,
on: openGroup.server
)
.map { _, response in response.seqNo }
.eraseToAnyPublisher()
case false:
return OpenGroupAPI
.reactionAdd(
db,
emoji: emoji,
id: openGroupServerMessageId,
in: openGroup.roomToken,
on: openGroup.server
)
.map { _, response in response.seqNo }
.eraseToAnyPublisher()
}
}()
return request
.handleEvents(
receiveOutput: { seqNo in
OpenGroupManager
.updatePendingChange(
pendingChange,
seqNo: seqNo
)
},
receiveCompletion: { [weak self] result in
switch result {
case .finished: break
case .failure:
OpenGroupManager.removePendingChange(pendingChange)
self?.handleReactionSentFailure(
pendingReaction,
remove: remove
) )
.map { _, response in response.seqNo }
} }
}
) return try OpenGroupAPI
.map { _ in nil } .preparedReactionAdd(
.eraseToAnyPublisher() db,
} emoji: emoji,
.subscribe(on: DispatchQueue.global(qos: .userInitiated)) id: serverMessageId,
.flatMap { maybeSendData -> AnyPublisher<Void, Error> in in: openGroupRoom,
guard let sendData: MessageSender.PreparedSendData = maybeSendData else { on: openGroupServer
return Just(()) )
.setFailureType(to: Error.self) .map { _, response in response.seqNo }
.eraseToAnyPublisher() }()
return (nil, (pendingReaction, pendingChange, sendData))
default:
let sendData: MessageSender.PreparedSendData = try MessageSender.preparedSendData(
db,
message: VisibleMessage(
sentTimestamp: UInt64(sentTimestamp),
text: nil,
reaction: VisibleMessage.VMReaction(
timestamp: UInt64(cellViewModel.timestampMs),
publicKey: {
guard cellViewModel.variant == .standardIncoming else {
return cellViewModel.currentUserPublicKey
}
return cellViewModel.authorId
}(),
emoji: emoji,
kind: (remove ? .remove : .react)
)
),
to: try Message.Destination
.from(db, threadId: cellViewModel.threadId, threadVariant: cellViewModel.threadVariant),
namespace: try Message.Destination
.from(db, threadId: cellViewModel.threadId, threadVariant: cellViewModel.threadVariant)
.defaultNamespace,
interactionId: cellViewModel.id
)
return (sendData, nil)
} }
return MessageSender.sendImmediate(preparedSendData: sendData)
} }
.sinkUntilComplete() }
.tryFlatMap { messageSendData, openGroupInfo -> AnyPublisher<Void, Error> in
switch (messageSendData, openGroupInfo) {
case (.some(let sendData), _):
return MessageSender.sendImmediate(preparedSendData: sendData)
case (_, .some(let info)):
return OpenGroupAPI.send(data: info.sendData)
.handleEvents(
receiveOutput: { _, seqNo in
OpenGroupManager
.updatePendingChange(
info.pendingChange,
seqNo: seqNo
)
},
receiveCompletion: { [weak self] result in
switch result {
case .finished: break
case .failure:
OpenGroupManager.removePendingChange(info.pendingChange)
self?.handleReactionSentFailure(
info.pendingReaction,
remove: remove
)
}
}
)
.map { _ in () }
.eraseToAnyPublisher()
default: throw MessageSenderError.invalidMessage
}
}
.sinkUntilComplete()
} }
func handleReactionSentFailure(_ pendingReaction: Reaction?, remove: Bool) { func handleReactionSentFailure(_ pendingReaction: Reaction?, remove: Bool) {
@ -1891,16 +1821,18 @@ extension ConversationVC:
// Delete the message from the open group // Delete the message from the open group
deleteRemotely( deleteRemotely(
from: self, from: self,
request: Storage.shared.readPublisherFlatMap { db in request: Storage.shared
OpenGroupAPI.messageDelete( .readPublisher { db in
db, try OpenGroupAPI.preparedMessageDelete(
id: openGroupServerMessageId, db,
in: openGroup.roomToken, id: openGroupServerMessageId,
on: openGroup.server in: openGroup.roomToken,
) on: openGroup.server
)
}
.flatMap { OpenGroupAPI.send(data: $0) }
.map { _ in () } .map { _ in () }
.eraseToAnyPublisher() .eraseToAnyPublisher()
}
) { [weak self] in ) { [weak self] in
self?.showInputAccessoryView() self?.showInputAccessoryView()
} }
@ -2100,21 +2032,20 @@ extension ConversationVC:
cancelStyle: .alert_text, cancelStyle: .alert_text,
onConfirm: { [weak self] _ in onConfirm: { [weak self] _ in
Storage.shared Storage.shared
.readPublisherFlatMap { db -> AnyPublisher<Void, Error> in .readPublisher { db -> OpenGroupAPI.PreparedSendData<NoResponse> in
guard let openGroup: OpenGroup = try OpenGroup.fetchOne(db, id: threadId) else { guard let openGroup: OpenGroup = try OpenGroup.fetchOne(db, id: threadId) else {
throw StorageError.objectNotFound throw StorageError.objectNotFound
} }
return OpenGroupAPI return try OpenGroupAPI
.userBan( .preparedUserBan(
db, db,
sessionId: cellViewModel.authorId, sessionId: cellViewModel.authorId,
from: [openGroup.roomToken], from: [openGroup.roomToken],
on: openGroup.server on: openGroup.server
) )
.map { _ in () }
.eraseToAnyPublisher()
} }
.flatMap { OpenGroupAPI.send(data: $0) }
.subscribe(on: DispatchQueue.global(qos: .userInitiated)) .subscribe(on: DispatchQueue.global(qos: .userInitiated))
.receive(on: DispatchQueue.main) .receive(on: DispatchQueue.main)
.sinkUntilComplete( .sinkUntilComplete(
@ -2316,11 +2247,11 @@ extension ConversationVC:
let attachment = SignalAttachment.voiceMessageAttachment(dataSource: dataSource, dataUTI: kUTTypeMPEG4Audio as String) let attachment = SignalAttachment.voiceMessageAttachment(dataSource: dataSource, dataUTI: kUTTypeMPEG4Audio as String)
guard !attachment.hasError else { guard !attachment.hasError else {
return showErrorAlert(for: attachment, onDismiss: nil) return showErrorAlert(for: attachment)
} }
// Send attachment // Send attachment
sendAttachments([ attachment ], with: "") sendMessage(text: "", attachments: [attachment])
} }
func cancelVoiceMessageRecording() { func cancelVoiceMessageRecording() {
@ -2360,15 +2291,14 @@ extension ConversationVC:
// MARK: - Convenience // MARK: - Convenience
func showErrorAlert(for attachment: SignalAttachment, onDismiss: (() -> ())?) { func showErrorAlert(for attachment: SignalAttachment) {
let modal: ConfirmationModal = ConfirmationModal( let modal: ConfirmationModal = ConfirmationModal(
targetView: self.view, targetView: self.view,
info: ConfirmationModal.Info( info: ConfirmationModal.Info(
title: "ATTACHMENT_ERROR_ALERT_TITLE".localized(), title: "ATTACHMENT_ERROR_ALERT_TITLE".localized(),
body: .text(attachment.localizedErrorDescription ?? SignalAttachment.missingDataErrorMessage), body: .text(attachment.localizedErrorDescription ?? SignalAttachment.missingDataErrorMessage),
cancelTitle: "BUTTON_OK".localized(), cancelTitle: "BUTTON_OK".localized(),
cancelStyle: .alert_text, cancelStyle: .alert_text
afterClosed: onDismiss
) )
) )
self.present(modal, animated: true) self.present(modal, animated: true)

View File

@ -890,6 +890,7 @@ final class ConversationVC: BaseVC, SessionUtilRespondingViewController, Convers
guard !didSendMessageBeforeUpdate && !wasOnlyUpdates else { guard !didSendMessageBeforeUpdate && !wasOnlyUpdates else {
self.viewModel.updateInteractionData(updatedData) self.viewModel.updateInteractionData(updatedData)
self.tableView.reloadData() self.tableView.reloadData()
self.tableView.layoutIfNeeded()
// If we just sent a message then we want to jump to the bottom of the conversation instantly // If we just sent a message then we want to jump to the bottom of the conversation instantly
if didSendMessageBeforeUpdate { if didSendMessageBeforeUpdate {

View File

@ -176,9 +176,10 @@ public class ConversationViewModel: OWSAudioPlayerDelegate {
/// this is due to the behaviour of `ValueConcurrentObserver.asyncStartObservation` which triggers it's own /// this is due to the behaviour of `ValueConcurrentObserver.asyncStartObservation` which triggers it's own
/// fetch (after the ones in `ValueConcurrentObserver.asyncStart`/`ValueConcurrentObserver.syncStart`) /// fetch (after the ones in `ValueConcurrentObserver.asyncStart`/`ValueConcurrentObserver.syncStart`)
/// just in case the database has changed between the two reads - unfortunately it doesn't look like there is a way to prevent this /// just in case the database has changed between the two reads - unfortunately it doesn't look like there is a way to prevent this
public lazy var observableThreadData: ValueObservation<ValueReducers.RemoveDuplicates<ValueReducers.Fetch<SessionThreadViewModel?>>> = setupObservableThreadData(for: self.threadId) public typealias ThreadObservation = ValueObservation<ValueReducers.Trace<ValueReducers.RemoveDuplicates<ValueReducers.Fetch<SessionThreadViewModel?>>>>
public lazy var observableThreadData: ThreadObservation = setupObservableThreadData(for: self.threadId)
private func setupObservableThreadData(for threadId: String) -> ValueObservation<ValueReducers.RemoveDuplicates<ValueReducers.Fetch<SessionThreadViewModel?>>> { private func setupObservableThreadData(for threadId: String) -> ThreadObservation {
return ValueObservation return ValueObservation
.trackingConstantRegion { [weak self] db -> SessionThreadViewModel? in .trackingConstantRegion { [weak self] db -> SessionThreadViewModel? in
let userPublicKey: String = getUserHexEncodedPublicKey(db) let userPublicKey: String = getUserHexEncodedPublicKey(db)
@ -197,6 +198,7 @@ public class ConversationViewModel: OWSAudioPlayerDelegate {
} }
} }
.removeDuplicates() .removeDuplicates()
.handleEvents(didFail: { SNLog("[ConversationViewModel] Observation failed with error: \($0)") })
} }
public func updateThreadData(_ updatedData: SessionThreadViewModel) { public func updateThreadData(_ updatedData: SessionThreadViewModel) {
@ -314,8 +316,16 @@ public class ConversationViewModel: OWSAudioPlayerDelegate {
) )
], ],
onChangeUnsorted: { [weak self] updatedData, updatedPageInfo in onChangeUnsorted: { [weak self] updatedData, updatedPageInfo in
self?.resolveOptimisticUpdates(with: updatedData)
PagedData.processAndTriggerUpdates( PagedData.processAndTriggerUpdates(
updatedData: self?.process(data: updatedData, for: updatedPageInfo), updatedData: self?.process(
data: updatedData,
for: updatedPageInfo,
optimisticMessages: (self?.optimisticallyInsertedMessages.wrappedValue.values)
.map { Array($0) },
initialUnreadInteractionId: self?.initialUnreadInteractionId
),
currentDataRetriever: { self?.interactionData }, currentDataRetriever: { self?.interactionData },
onDataChange: self?.onInteractionChange, onDataChange: self?.onInteractionChange,
onUnobservedDataChange: { updatedData, changeset in onUnobservedDataChange: { updatedData, changeset in
@ -329,11 +339,16 @@ public class ConversationViewModel: OWSAudioPlayerDelegate {
) )
} }
private func process(data: [MessageViewModel], for pageInfo: PagedData.PageInfo) -> [SectionModel] { private func process(
let initialUnreadInteractionId: Int64? = self.initialUnreadInteractionId data: [MessageViewModel],
for pageInfo: PagedData.PageInfo,
optimisticMessages: [MessageViewModel]?,
initialUnreadInteractionId: Int64?
) -> [SectionModel] {
let typingIndicator: MessageViewModel? = data.first(where: { $0.isTypingIndicator == true }) let typingIndicator: MessageViewModel? = data.first(where: { $0.isTypingIndicator == true })
let sortedData: [MessageViewModel] = data let sortedData: [MessageViewModel] = data
.filter { $0.isTypingIndicator != true } .appending(contentsOf: (optimisticMessages ?? []))
.filter { !$0.cellType.isPostProcessed }
.sorted { lhs, rhs -> Bool in lhs.timestampMs < rhs.timestampMs } .sorted { lhs, rhs -> Bool in lhs.timestampMs < rhs.timestampMs }
// We load messages from newest to oldest so having a pageOffset larger than zero means // We load messages from newest to oldest so having a pageOffset larger than zero means
@ -408,12 +423,151 @@ public class ConversationViewModel: OWSAudioPlayerDelegate {
self.interactionData = updatedData self.interactionData = updatedData
} }
public func expandReactions(for interactionId: Int64) { // MARK: - Optimistic Message Handling
reactionExpandedInteractionIds.insert(interactionId)
public typealias OptimisticMessageData = (
id: UUID,
interaction: Interaction,
attachmentData: Attachment.PreparedData?,
linkPreviewAttachment: Attachment?
)
private var optimisticallyInsertedMessages: Atomic<[UUID: MessageViewModel]> = Atomic([:])
private var optimisticMessageAssociatedInteractionIds: Atomic<[Int64: UUID]> = Atomic([:])
public func optimisticallyAppendOutgoingMessage(
text: String?,
sentTimestampMs: Int64,
attachments: [SignalAttachment]?,
linkPreviewDraft: LinkPreviewDraft?,
quoteModel: QuotedReplyModel?
) -> OptimisticMessageData {
// Generate the optimistic data
let optimisticMessageId: UUID = UUID()
let currentUserProfile: Profile = Profile.fetchOrCreateCurrentUser()
let interaction: Interaction = Interaction(
threadId: threadData.threadId,
authorId: (threadData.currentUserBlindedPublicKey ?? threadData.currentUserPublicKey),
variant: .standardOutgoing,
body: text,
timestampMs: sentTimestampMs,
hasMention: Interaction.isUserMentioned(
publicKeysToCheck: [
threadData.currentUserPublicKey,
threadData.currentUserBlindedPublicKey
].compactMap { $0 },
body: text
),
expiresInSeconds: threadData.disappearingMessagesConfiguration
.map { disappearingConfig in
guard disappearingConfig.isEnabled else { return nil }
return disappearingConfig.durationSeconds
},
linkPreviewUrl: linkPreviewDraft?.urlString
)
let optimisticAttachments: Attachment.PreparedData? = attachments
.map { Attachment.prepare(attachments: $0) }
let linkPreviewAttachment: Attachment? = linkPreviewDraft.map { draft in
try? LinkPreview.generateAttachmentIfPossible(
imageData: draft.jpegImageData,
mimeType: OWSMimeTypeImageJpeg
)
}
let optimisticData: OptimisticMessageData = (
optimisticMessageId,
interaction,
optimisticAttachments,
linkPreviewAttachment
)
// Generate the actual 'MessageViewModel'
let messageViewModel: MessageViewModel = MessageViewModel(
threadId: threadData.threadId,
threadVariant: threadData.threadVariant,
threadHasDisappearingMessagesEnabled: (threadData.disappearingMessagesConfiguration?.isEnabled ?? false),
threadOpenGroupServer: threadData.openGroupServer,
threadOpenGroupPublicKey: threadData.openGroupPublicKey,
threadContactNameInternal: threadData.threadContactName(),
timestampMs: interaction.timestampMs,
receivedAtTimestampMs: interaction.receivedAtTimestampMs,
authorId: interaction.authorId,
authorNameInternal: currentUserProfile.displayName(),
body: interaction.body,
expiresStartedAtMs: interaction.expiresStartedAtMs,
expiresInSeconds: interaction.expiresInSeconds,
isSenderOpenGroupModerator: OpenGroupManager.isUserModeratorOrAdmin(
threadData.currentUserPublicKey,
for: threadData.openGroupRoomToken,
on: threadData.openGroupServer
),
currentUserProfile: currentUserProfile,
quote: quoteModel.map { model in
// Don't care about this optimistic quote (the proper one will be generated in the database)
Quote(
interactionId: -1, // Can't save to db optimistically
authorId: model.authorId,
timestampMs: model.timestampMs,
body: model.body,
attachmentId: model.attachment?.id
)
},
quoteAttachment: quoteModel?.attachment,
linkPreview: linkPreviewDraft.map { draft in
LinkPreview(
url: draft.urlString,
title: draft.title,
attachmentId: nil // Can't save to db optimistically
)
},
linkPreviewAttachment: linkPreviewAttachment,
attachments: optimisticAttachments?.attachments
)
optimisticallyInsertedMessages.mutate { $0[optimisticMessageId] = messageViewModel }
// If we can't get the current page data then don't bother trying to update (it's not going to work)
guard let currentPageInfo: PagedData.PageInfo = self.pagedDataObserver?.pageInfo.wrappedValue else {
return optimisticData
}
/// **MUST** have the same logic as in the 'PagedDataObserver.onChangeUnsorted' above
let currentData: [SectionModel] = (unobservedInteractionDataChanges?.0 ?? interactionData)
PagedData.processAndTriggerUpdates(
updatedData: process(
data: (currentData.first(where: { $0.model == .messages })?.elements ?? []),
for: currentPageInfo,
optimisticMessages: Array(optimisticallyInsertedMessages.wrappedValue.values),
initialUnreadInteractionId: initialUnreadInteractionId
),
currentDataRetriever: { [weak self] in self?.interactionData },
onDataChange: self.onInteractionChange,
onUnobservedDataChange: { [weak self] updatedData, changeset in
self?.unobservedInteractionDataChanges = (changeset.isEmpty ?
nil :
(updatedData, changeset)
)
}
)
return optimisticData
} }
public func collapseReactions(for interactionId: Int64) { /// Record an association between an `optimisticMessageId` and a specific `interactionId`
reactionExpandedInteractionIds.remove(interactionId) public func associate(optimisticMessageId: UUID, to interactionId: Int64?) {
guard let interactionId: Int64 = interactionId else { return }
optimisticMessageAssociatedInteractionIds.mutate { $0[interactionId] = optimisticMessageId }
}
/// Remove any optimisticUpdate entries which have an associated interactionId in the provided data
private func resolveOptimisticUpdates(with data: [MessageViewModel]) {
let interactionIds: [Int64] = data.map { $0.id }
let idsToRemove: [UUID] = optimisticMessageAssociatedInteractionIds
.mutate { associatedIds in interactionIds.compactMap { associatedIds.removeValue(forKey: $0) } }
optimisticallyInsertedMessages.mutate { messages in idsToRemove.forEach { messages.removeValue(forKey: $0) } }
} }
// MARK: - Mentions // MARK: - Mentions
@ -575,6 +729,14 @@ public class ConversationViewModel: OWSAudioPlayerDelegate {
} }
} }
public func expandReactions(for interactionId: Int64) {
reactionExpandedInteractionIds.insert(interactionId)
}
public func collapseReactions(for interactionId: Int64) {
reactionExpandedInteractionIds.remove(interactionId)
}
// MARK: - Audio Playback // MARK: - Audio Playback
public struct PlaybackInfo { public struct PlaybackInfo {

View File

@ -332,6 +332,7 @@ final class InputView: UIView, InputViewButtonDelegate, InputTextViewDelegate, M
// Build the link preview // Build the link preview
LinkPreview.tryToBuildPreviewInfo(previewUrl: linkPreviewURL) LinkPreview.tryToBuildPreviewInfo(previewUrl: linkPreviewURL)
.subscribe(on: DispatchQueue.global(qos: .userInitiated))
.receive(on: DispatchQueue.main) .receive(on: DispatchQueue.main)
.sink( .sink(
receiveCompletion: { [weak self] result in receiveCompletion: { [weak self] result in

View File

@ -119,17 +119,6 @@ final class QuoteView: UIView {
contentView.pin([ UIView.HorizontalEdge.left, UIView.VerticalEdge.top, UIView.VerticalEdge.bottom ], to: self) contentView.pin([ UIView.HorizontalEdge.left, UIView.VerticalEdge.top, UIView.VerticalEdge.bottom ], to: self)
contentView.rightAnchor.constraint(lessThanOrEqualTo: self.rightAnchor).isActive = true contentView.rightAnchor.constraint(lessThanOrEqualTo: self.rightAnchor).isActive = true
// Line view
let lineColor: ThemeValue = {
switch mode {
case .regular: return (direction == .outgoing ? .messageBubble_outgoingText : .primary)
case .draft: return .primary
}
}()
let lineView = UIView()
lineView.themeBackgroundColor = lineColor
lineView.set(.width, to: Values.accentLineThickness)
if let attachment: Attachment = attachment { if let attachment: Attachment = attachment {
let isAudio: Bool = MIMETypeUtil.isAudio(attachment.contentType) let isAudio: Bool = MIMETypeUtil.isAudio(attachment.contentType)
let fallbackImageName: String = (isAudio ? "attachment_audio" : "actionsheet_document_black") let fallbackImageName: String = (isAudio ? "attachment_audio" : "actionsheet_document_black")
@ -181,13 +170,26 @@ final class QuoteView: UIView {
} }
} }
else { else {
// Line view
let lineColor: ThemeValue = {
switch mode {
case .regular: return (direction == .outgoing ? .messageBubble_outgoingText : .primary)
case .draft: return .primary
}
}()
let lineView = UIView()
lineView.themeBackgroundColor = lineColor
mainStackView.addArrangedSubview(lineView) mainStackView.addArrangedSubview(lineView)
lineView.pin(.top, to: .top, of: mainStackView)
lineView.pin(.bottom, to: .bottom, of: mainStackView)
lineView.set(.width, to: Values.accentLineThickness)
} }
// Body label // Body label
let bodyLabel = TappableLabel() let bodyLabel = TappableLabel()
bodyLabel.numberOfLines = 0
bodyLabel.lineBreakMode = .byTruncatingTail bodyLabel.lineBreakMode = .byTruncatingTail
bodyLabel.numberOfLines = 2
let targetThemeColor: ThemeValue = { let targetThemeColor: ThemeValue = {
switch mode { switch mode {
@ -229,7 +231,6 @@ final class QuoteView: UIView {
// Label stack view // Label stack view
let bodyLabelSize = bodyLabel.systemLayoutSizeFitting(availableSpace) let bodyLabelSize = bodyLabel.systemLayoutSizeFitting(availableSpace)
var authorLabelHeight: CGFloat?
let isCurrentUser: Bool = [ let isCurrentUser: Bool = [
currentUserPublicKey, currentUserPublicKey,
@ -259,16 +260,12 @@ final class QuoteView: UIView {
authorLabel.themeTextColor = targetThemeColor authorLabel.themeTextColor = targetThemeColor
authorLabel.lineBreakMode = .byTruncatingTail authorLabel.lineBreakMode = .byTruncatingTail
authorLabel.isHidden = (authorLabel.text == nil) authorLabel.isHidden = (authorLabel.text == nil)
authorLabel.numberOfLines = 1
let authorLabelSize = authorLabel.systemLayoutSizeFitting(availableSpace)
authorLabel.set(.height, to: authorLabelSize.height)
authorLabelHeight = authorLabelSize.height
let labelStackView = UIStackView(arrangedSubviews: [ authorLabel, bodyLabel ]) let labelStackView = UIStackView(arrangedSubviews: [ authorLabel, bodyLabel ])
labelStackView.axis = .vertical labelStackView.axis = .vertical
labelStackView.spacing = labelStackViewSpacing labelStackView.spacing = labelStackViewSpacing
labelStackView.distribution = .equalCentering labelStackView.distribution = .equalCentering
labelStackView.set(.width, to: max(bodyLabelSize.width, authorLabelSize.width))
labelStackView.isLayoutMarginsRelativeArrangement = true labelStackView.isLayoutMarginsRelativeArrangement = true
labelStackView.layoutMargins = UIEdgeInsets(top: labelStackViewVMargin, left: 0, bottom: labelStackViewVMargin, right: 0) labelStackView.layoutMargins = UIEdgeInsets(top: labelStackViewVMargin, left: 0, bottom: labelStackViewVMargin, right: 0)
mainStackView.addArrangedSubview(labelStackView) mainStackView.addArrangedSubview(labelStackView)
@ -277,29 +274,6 @@ final class QuoteView: UIView {
contentView.addSubview(mainStackView) contentView.addSubview(mainStackView)
mainStackView.pin(to: contentView) mainStackView.pin(to: contentView)
if threadVariant == .contact {
bodyLabel.set(.width, to: bodyLabelSize.width)
}
let bodyLabelHeight = bodyLabelSize.height.clamp(0, (mode == .regular ? 60 : 40))
let contentViewHeight: CGFloat
if attachment != nil {
contentViewHeight = thumbnailSize + 8 // Add a small amount of spacing above and below the thumbnail
bodyLabel.set(.height, to: 18) // Experimentally determined
}
else {
if let authorLabelHeight = authorLabelHeight { // Group thread
contentViewHeight = bodyLabelHeight + (authorLabelHeight + labelStackViewSpacing) + 2 * labelStackViewVMargin
}
else {
contentViewHeight = bodyLabelHeight + 2 * smallSpacing
}
}
contentView.set(.height, to: contentViewHeight)
lineView.set(.height, to: contentViewHeight - 8) // Add a small amount of spacing above and below the line
if mode == .draft { if mode == .draft {
// Cancel button // Cancel button
let cancelButton = UIButton(type: .custom) let cancelButton = UIButton(type: .custom)

View File

@ -149,6 +149,7 @@ class ThreadDisappearingMessagesSettingsViewModel: SessionTableViewModel<ThreadD
] ]
} }
.removeDuplicates() .removeDuplicates()
.handleEvents(didFail: { SNLog("[ThreadDisappearingMessageSettingsViewModel] Observation failed with error: \($0)") })
.publisher(in: dependencies.storage, scheduling: dependencies.scheduler) .publisher(in: dependencies.storage, scheduling: dependencies.scheduler)
.mapToSessionTableViewData(for: self) .mapToSessionTableViewData(for: self)

View File

@ -709,6 +709,7 @@ class ThreadSettingsViewModel: SessionTableViewModel<ThreadSettingsViewModel.Nav
] ]
} }
.removeDuplicates() .removeDuplicates()
.handleEvents(didFail: { SNLog("[ThreadSettingsViewModel] Observation failed with error: \($0)") })
.publisher(in: dependencies.storage, scheduling: dependencies.scheduler) .publisher(in: dependencies.storage, scheduling: dependencies.scheduler)
.mapToSessionTableViewData(for: self) .mapToSessionTableViewData(for: self)

View File

@ -20,7 +20,7 @@ public class HomeViewModel {
// MARK: - Variables // MARK: - Variables
public static let pageSize: Int = 15 public static let pageSize: Int = (UIDevice.current.isIPad ? 20 : 15)
public struct State: Equatable { public struct State: Equatable {
let showViewedSeedBanner: Bool let showViewedSeedBanner: Bool
@ -231,6 +231,7 @@ public class HomeViewModel {
public lazy var observableState = ValueObservation public lazy var observableState = ValueObservation
.trackingConstantRegion { db -> State in try HomeViewModel.retrieveState(db) } .trackingConstantRegion { db -> State in try HomeViewModel.retrieveState(db) }
.removeDuplicates() .removeDuplicates()
.handleEvents(didFail: { SNLog("[HomeViewModel] Observation failed with error: \($0)") })
private static func retrieveState(_ db: Database) throws -> State { private static func retrieveState(_ db: Database) throws -> State {
let hasViewedSeed: Bool = db[.hasViewedSeed] let hasViewedSeed: Bool = db[.hasViewedSeed]

View File

@ -17,7 +17,7 @@ public class MessageRequestsViewModel {
// MARK: - Variables // MARK: - Variables
public static let pageSize: Int = 15 public static let pageSize: Int = (UIDevice.current.isIPad ? 20 : 15)
// MARK: - Initialization // MARK: - Initialization

View File

@ -210,6 +210,7 @@ final class NewDMVC: BaseVC, UIPageViewControllerDataSource, UIPageViewControlle
.present(fromViewController: navigationController!, canCancel: false) { [weak self] modalActivityIndicator in .present(fromViewController: navigationController!, canCancel: false) { [weak self] modalActivityIndicator in
SnodeAPI SnodeAPI
.getSessionID(for: onsNameOrPublicKey) .getSessionID(for: onsNameOrPublicKey)
.subscribe(on: DispatchQueue.global(qos: .userInitiated))
.receive(on: DispatchQueue.main) .receive(on: DispatchQueue.main)
.sinkUntilComplete( .sinkUntilComplete(
receiveCompletion: { result in receiveCompletion: { result in

View File

@ -360,6 +360,7 @@ class GifPickerViewController: OWSViewController, UISearchBarDelegate, UICollect
cell cell
.requestRenditionForSending() .requestRenditionForSending()
.subscribe(on: DispatchQueue.global(qos: .userInitiated))
.receive(on: DispatchQueue.main) .receive(on: DispatchQueue.main)
.sink( .sink(
receiveCompletion: { [weak self] result in receiveCompletion: { [weak self] result in
@ -490,6 +491,7 @@ class GifPickerViewController: OWSViewController, UISearchBarDelegate, UICollect
assert(searchBar.text == nil || searchBar.text?.count == 0) assert(searchBar.text == nil || searchBar.text?.count == 0)
GiphyAPI.trending() GiphyAPI.trending()
.subscribe(on: DispatchQueue.global(qos: .userInitiated))
.receive(on: DispatchQueue.main) .receive(on: DispatchQueue.main)
.sink( .sink(
receiveCompletion: { result in receiveCompletion: { result in
@ -527,6 +529,7 @@ class GifPickerViewController: OWSViewController, UISearchBarDelegate, UICollect
GiphyAPI GiphyAPI
.search(query: query) .search(query: query)
.subscribe(on: DispatchQueue.global(qos: .userInitiated))
.receive(on: DispatchQueue.main) .receive(on: DispatchQueue.main)
.sink( .sink(
receiveCompletion: { [weak self] result in receiveCompletion: { [weak self] result in

View File

@ -291,7 +291,6 @@ enum GiphyAPI {
return urlSession return urlSession
.dataTaskPublisher(for: url) .dataTaskPublisher(for: url)
.subscribe(on: DispatchQueue.global(qos: .userInitiated))
.mapError { urlError in .mapError { urlError in
Logger.error("search request failed: \(urlError)") Logger.error("search request failed: \(urlError)")
@ -340,7 +339,6 @@ enum GiphyAPI {
return urlSession return urlSession
.dataTaskPublisher(for: request) .dataTaskPublisher(for: request)
.subscribe(on: DispatchQueue.global(qos: .userInitiated))
.mapError { urlError in .mapError { urlError in
Logger.error("search request failed: \(urlError)") Logger.error("search request failed: \(urlError)")

View File

@ -360,7 +360,7 @@ public class MediaGalleryViewModel {
/// this is due to the behaviour of `ValueConcurrentObserver.asyncStartObservation` which triggers it's own /// this is due to the behaviour of `ValueConcurrentObserver.asyncStartObservation` which triggers it's own
/// fetch (after the ones in `ValueConcurrentObserver.asyncStart`/`ValueConcurrentObserver.syncStart`) /// fetch (after the ones in `ValueConcurrentObserver.asyncStart`/`ValueConcurrentObserver.syncStart`)
/// just in case the database has changed between the two reads - unfortunately it doesn't look like there is a way to prevent this /// just in case the database has changed between the two reads - unfortunately it doesn't look like there is a way to prevent this
public typealias AlbumObservation = ValueObservation<ValueReducers.RemoveDuplicates<ValueReducers.Fetch<[Item]>>> public typealias AlbumObservation = ValueObservation<ValueReducers.Trace<ValueReducers.RemoveDuplicates<ValueReducers.Fetch<[Item]>>>>
public lazy var observableAlbumData: AlbumObservation = buildAlbumObservation(for: nil) public lazy var observableAlbumData: AlbumObservation = buildAlbumObservation(for: nil)
private func buildAlbumObservation(for interactionId: Int64?) -> AlbumObservation { private func buildAlbumObservation(for interactionId: Int64?) -> AlbumObservation {
@ -383,6 +383,7 @@ public class MediaGalleryViewModel {
.fetchAll(db) .fetchAll(db)
} }
.removeDuplicates() .removeDuplicates()
.handleEvents(didFail: { SNLog("[MediaGalleryViewModel] Observation failed with error: \($0)") })
} }
@discardableResult public func loadAndCacheAlbumData(for interactionId: Int64, in threadId: String) -> [Item] { @discardableResult public func loadAndCacheAlbumData(for interactionId: Int64, in threadId: String) -> [Item] {

View File

@ -84,7 +84,7 @@ class PhotoCapture: NSObject {
func startCapture() -> AnyPublisher<Void, Error> { func startCapture() -> AnyPublisher<Void, Error> {
return Just(()) return Just(())
.subscribe(on: sessionQueue) .subscribe(on: sessionQueue) // Must run this on a specific queue to prevent crashes
.setFailureType(to: Error.self) .setFailureType(to: Error.self)
.tryMap { [weak self] _ -> Void in .tryMap { [weak self] _ -> Void in
self?.session.beginConfiguration() self?.session.beginConfiguration()
@ -136,7 +136,7 @@ class PhotoCapture: NSObject {
func stopCapture() -> AnyPublisher<Void, Never> { func stopCapture() -> AnyPublisher<Void, Never> {
return Just(()) return Just(())
.subscribe(on: sessionQueue) .subscribe(on: sessionQueue) // Must run this on a specific queue to prevent crashes
.handleEvents( .handleEvents(
receiveOutput: { [weak self] in self?.session.stopRunning() } receiveOutput: { [weak self] in self?.session.stopRunning() }
) )
@ -160,7 +160,7 @@ class PhotoCapture: NSObject {
return Just(()) return Just(())
.setFailureType(to: Error.self) .setFailureType(to: Error.self)
.subscribe(on: sessionQueue) .subscribe(on: sessionQueue) // Must run this on a specific queue to prevent crashes
.tryMap { [weak self, newPosition = self.desiredPosition] _ -> Void in .tryMap { [weak self, newPosition = self.desiredPosition] _ -> Void in
self?.session.beginConfiguration() self?.session.beginConfiguration()
defer { self?.session.commitConfiguration() } defer { self?.session.commitConfiguration() }
@ -196,7 +196,7 @@ class PhotoCapture: NSObject {
func switchFlashMode() -> AnyPublisher<Void, Never> { func switchFlashMode() -> AnyPublisher<Void, Never> {
return Just(()) return Just(())
.subscribe(on: sessionQueue) .subscribe(on: sessionQueue) // Must run this on a specific queue to prevent crashes
.handleEvents( .handleEvents(
receiveOutput: { [weak self] _ in receiveOutput: { [weak self] _ in
switch self?.captureOutput.flashMode { switch self?.captureOutput.flashMode {
@ -351,7 +351,7 @@ extension PhotoCapture: CaptureButtonDelegate {
Logger.verbose("") Logger.verbose("")
Just(()) Just(())
.subscribe(on: sessionQueue) .subscribe(on: sessionQueue) // Must run this on a specific queue to prevent crashes
.sinkUntilComplete( .sinkUntilComplete(
receiveCompletion: { [weak self] _ in receiveCompletion: { [weak self] _ in
guard let strongSelf = self else { return } guard let strongSelf = self else { return }

View File

@ -1,6 +1,7 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. // Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import UIKit import UIKit
import SessionUIKit
class MediaDismissAnimationController: NSObject { class MediaDismissAnimationController: NSObject {
private let mediaItem: Media private let mediaItem: Media
@ -46,6 +47,18 @@ extension MediaDismissAnimationController: UIViewControllerAnimatedTransitioning
switch fromVC { switch fromVC {
case let contextProvider as MediaPresentationContextProvider: case let contextProvider as MediaPresentationContextProvider:
fromContextProvider = contextProvider fromContextProvider = contextProvider
case let topBannerController as TopBannerController:
guard
let firstChild: UIViewController = topBannerController.children.first,
let navController: UINavigationController = firstChild as? UINavigationController,
let contextProvider = navController.topViewController as? MediaPresentationContextProvider
else {
transitionContext.completeTransition(false)
return
}
fromContextProvider = contextProvider
case let navController as UINavigationController: case let navController as UINavigationController:
guard let contextProvider = navController.topViewController as? MediaPresentationContextProvider else { guard let contextProvider = navController.topViewController as? MediaPresentationContextProvider else {
@ -64,6 +77,19 @@ extension MediaDismissAnimationController: UIViewControllerAnimatedTransitioning
case let contextProvider as MediaPresentationContextProvider: case let contextProvider as MediaPresentationContextProvider:
toVC.view.layoutIfNeeded() toVC.view.layoutIfNeeded()
toContextProvider = contextProvider toContextProvider = contextProvider
case let topBannerController as TopBannerController:
guard
let firstChild: UIViewController = topBannerController.children.first,
let navController: UINavigationController = firstChild as? UINavigationController,
let contextProvider = navController.topViewController as? MediaPresentationContextProvider
else {
transitionContext.completeTransition(false)
return
}
toVC.view.layoutIfNeeded()
toContextProvider = contextProvider
case let navController as UINavigationController: case let navController as UINavigationController:
guard let contextProvider = navController.topViewController as? MediaPresentationContextProvider else { guard let contextProvider = navController.topViewController as? MediaPresentationContextProvider else {

View File

@ -1,6 +1,7 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. // Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import UIKit import UIKit
import SessionUIKit
class MediaZoomAnimationController: NSObject { class MediaZoomAnimationController: NSObject {
private let mediaItem: Media private let mediaItem: Media
@ -34,6 +35,18 @@ extension MediaZoomAnimationController: UIViewControllerAnimatedTransitioning {
switch fromVC { switch fromVC {
case let contextProvider as MediaPresentationContextProvider: case let contextProvider as MediaPresentationContextProvider:
fromContextProvider = contextProvider fromContextProvider = contextProvider
case let topBannerController as TopBannerController:
guard
let firstChild: UIViewController = topBannerController.children.first,
let navController: UINavigationController = firstChild as? UINavigationController,
let contextProvider = navController.topViewController as? MediaPresentationContextProvider
else {
transitionContext.completeTransition(false)
return
}
fromContextProvider = contextProvider
case let navController as UINavigationController: case let navController as UINavigationController:
guard let contextProvider = navController.topViewController as? MediaPresentationContextProvider else { guard let contextProvider = navController.topViewController as? MediaPresentationContextProvider else {
@ -51,6 +64,18 @@ extension MediaZoomAnimationController: UIViewControllerAnimatedTransitioning {
switch toVC { switch toVC {
case let contextProvider as MediaPresentationContextProvider: case let contextProvider as MediaPresentationContextProvider:
toContextProvider = contextProvider toContextProvider = contextProvider
case let topBannerController as TopBannerController:
guard
let firstChild: UIViewController = topBannerController.children.first,
let navController: UINavigationController = firstChild as? UINavigationController,
let contextProvider = navController.topViewController as? MediaPresentationContextProvider
else {
transitionContext.completeTransition(false)
return
}
toContextProvider = contextProvider
case let navController as UINavigationController: case let navController as UINavigationController:
guard let contextProvider = navController.topViewController as? MediaPresentationContextProvider else { guard let contextProvider = navController.topViewController as? MediaPresentationContextProvider else {

View File

@ -332,12 +332,17 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
} }
} }
private func showFailedMigrationAlert(calledFrom lifecycleMethod: LifecycleMethod, error: Error?) { private func showFailedMigrationAlert(
calledFrom lifecycleMethod: LifecycleMethod,
error: Error?,
isRestoreError: Bool = false
) {
let alert = UIAlertController( let alert = UIAlertController(
title: "Session", title: "Session",
message: { message: {
switch (error ?? StorageError.generic) { switch (isRestoreError, (error ?? StorageError.generic)) {
case StorageError.startupFailed: return "DATABASE_STARTUP_FAILED".localized() case (true, _): return "DATABASE_RESTORE_FAILED".localized()
case (_, StorageError.startupFailed): return "DATABASE_STARTUP_FAILED".localized()
default: return "DATABASE_MIGRATION_FAILED".localized() default: return "DATABASE_MIGRATION_FAILED".localized()
} }
}(), }(),
@ -348,32 +353,45 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
self?.showFailedMigrationAlert(calledFrom: lifecycleMethod, error: error) self?.showFailedMigrationAlert(calledFrom: lifecycleMethod, error: error)
} }
}) })
alert.addAction(UIAlertAction(title: "vc_restore_title".localized(), style: .destructive) { _ in
// Remove the legacy database and any message hashes that have been migrated to the new DB // Only offer the 'Restore' option if the user hasn't already tried to restore
try? SUKLegacy.deleteLegacyDatabaseFilesAndKey() if !isRestoreError {
alert.addAction(UIAlertAction(title: "vc_restore_title".localized(), style: .destructive) { _ in
Storage.shared.write { db in if SUKLegacy.hasLegacyDatabaseFile {
try SnodeReceivedMessageInfo.deleteAll(db) // Remove the legacy database and any message hashes that have been migrated to the new DB
} try? SUKLegacy.deleteLegacyDatabaseFilesAndKey()
// The re-run the migration (should succeed since there is no data)
AppSetup.runPostSetupMigrations(
migrationProgressChanged: { [weak self] progress, minEstimatedTotalTime in
self?.loadingViewController?.updateProgress(
progress: progress,
minEstimatedTotalTime: minEstimatedTotalTime
)
},
migrationsCompletion: { [weak self] result, needsConfigSync in
if case .failure(let error) = result {
self?.showFailedMigrationAlert(calledFrom: lifecycleMethod, error: error)
return
}
self?.completePostMigrationSetup(calledFrom: lifecycleMethod, needsConfigSync: needsConfigSync) Storage.shared.write { db in
try SnodeReceivedMessageInfo.deleteAll(db)
}
} }
) else {
}) // If we don't have a legacy database then reset the current database for a clean migration
Storage.resetForCleanMigration()
}
// Hide the top banner if there was one
TopBannerController.hide()
// The re-run the migration (should succeed since there is no data)
AppSetup.runPostSetupMigrations(
migrationProgressChanged: { [weak self] progress, minEstimatedTotalTime in
self?.loadingViewController?.updateProgress(
progress: progress,
minEstimatedTotalTime: minEstimatedTotalTime
)
},
migrationsCompletion: { [weak self] result, needsConfigSync in
if case .failure(let error) = result {
self?.showFailedMigrationAlert(calledFrom: lifecycleMethod, error: error, isRestoreError: true)
return
}
self?.completePostMigrationSetup(calledFrom: lifecycleMethod, needsConfigSync: needsConfigSync)
}
)
})
}
alert.addAction(UIAlertAction(title: "Close", style: .default) { _ in alert.addAction(UIAlertAction(title: "Close", style: .default) { _ in
DDLog.flushLog() DDLog.flushLog()
@ -612,12 +630,17 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
public func startPollersIfNeeded(shouldStartGroupPollers: Bool = true) { public func startPollersIfNeeded(shouldStartGroupPollers: Bool = true) {
guard Identity.userExists() else { return } guard Identity.userExists() else { return }
poller.start() /// There is a fun issue where if you launch without any valid paths then the pollers are guaranteed to fail their first poll due to
/// trying and failing to build paths without having the `SnodeAPI.snodePool` populated, by waiting for the
guard shouldStartGroupPollers else { return } /// `JobRunner.blockingQueue` to complete we can have more confidence that paths won't fail to build incorrectly
JobRunner.afterBlockingQueue { [weak self] in
ClosedGroupPoller.shared.start() self?.poller.start()
OpenGroupManager.shared.startPolling()
guard shouldStartGroupPollers else { return }
ClosedGroupPoller.shared.start()
OpenGroupManager.shared.startPolling()
}
} }
public func stopPollers(shouldStopUserPoller: Bool = true) { public func stopPollers(shouldStopUserPoller: Bool = true) {

View File

@ -415,6 +415,7 @@
"DEFAULT_OPEN_GROUP_LOAD_ERROR_SUBTITLE" = "Please try again later"; "DEFAULT_OPEN_GROUP_LOAD_ERROR_SUBTITLE" = "Please try again later";
"LOADING_CONVERSATIONS" = "Loading Conversations..."; "LOADING_CONVERSATIONS" = "Loading Conversations...";
"DATABASE_STARTUP_FAILED" = "An error occurred when opening the database\n\nYou can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks"; "DATABASE_STARTUP_FAILED" = "An error occurred when opening the database\n\nYou can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks";
"DATABASE_RESTORE_FAILED" = "An error occurred when opening the restored database\n\nYou can export your application logs to be able to share for troubleshooting but to continue to use Session you may need to reinstall it";
"DATABASE_MIGRATION_FAILED" = "An error occurred when optimising the database\n\nYou can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks"; "DATABASE_MIGRATION_FAILED" = "An error occurred when optimising the database\n\nYou can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks";
"RECOVERY_PHASE_ERROR_GENERIC" = "Something went wrong. Please check your recovery phrase and try again."; "RECOVERY_PHASE_ERROR_GENERIC" = "Something went wrong. Please check your recovery phrase and try again.";
"RECOVERY_PHASE_ERROR_LENGTH" = "Looks like you didn't enter enough words. Please check your recovery phrase and try again."; "RECOVERY_PHASE_ERROR_LENGTH" = "Looks like you didn't enter enough words. Please check your recovery phrase and try again.";

View File

@ -415,6 +415,7 @@
"DEFAULT_OPEN_GROUP_LOAD_ERROR_SUBTITLE" = "Please try again later"; "DEFAULT_OPEN_GROUP_LOAD_ERROR_SUBTITLE" = "Please try again later";
"LOADING_CONVERSATIONS" = "Loading Conversations..."; "LOADING_CONVERSATIONS" = "Loading Conversations...";
"DATABASE_STARTUP_FAILED" = "An error occurred when opening the database\n\nYou can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks"; "DATABASE_STARTUP_FAILED" = "An error occurred when opening the database\n\nYou can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks";
"DATABASE_RESTORE_FAILED" = "An error occurred when opening the restored database\n\nYou can export your application logs to be able to share for troubleshooting but to continue to use Session you may need to reinstall it";
"DATABASE_MIGRATION_FAILED" = "An error occurred when optimising the database\n\nYou can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks"; "DATABASE_MIGRATION_FAILED" = "An error occurred when optimising the database\n\nYou can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks";
"RECOVERY_PHASE_ERROR_GENERIC" = "Something went wrong. Please check your recovery phrase and try again."; "RECOVERY_PHASE_ERROR_GENERIC" = "Something went wrong. Please check your recovery phrase and try again.";
"RECOVERY_PHASE_ERROR_LENGTH" = "Looks like you didn't enter enough words. Please check your recovery phrase and try again."; "RECOVERY_PHASE_ERROR_LENGTH" = "Looks like you didn't enter enough words. Please check your recovery phrase and try again.";

View File

@ -415,6 +415,7 @@
"DEFAULT_OPEN_GROUP_LOAD_ERROR_SUBTITLE" = "Please try again later"; "DEFAULT_OPEN_GROUP_LOAD_ERROR_SUBTITLE" = "Please try again later";
"LOADING_CONVERSATIONS" = "Loading Conversations..."; "LOADING_CONVERSATIONS" = "Loading Conversations...";
"DATABASE_STARTUP_FAILED" = "An error occurred when opening the database\n\nYou can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks"; "DATABASE_STARTUP_FAILED" = "An error occurred when opening the database\n\nYou can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks";
"DATABASE_RESTORE_FAILED" = "An error occurred when opening the restored database\n\nYou can export your application logs to be able to share for troubleshooting but to continue to use Session you may need to reinstall it";
"DATABASE_MIGRATION_FAILED" = "An error occurred when optimising the database\n\nYou can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks"; "DATABASE_MIGRATION_FAILED" = "An error occurred when optimising the database\n\nYou can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks";
"RECOVERY_PHASE_ERROR_GENERIC" = "Something went wrong. Please check your recovery phrase and try again."; "RECOVERY_PHASE_ERROR_GENERIC" = "Something went wrong. Please check your recovery phrase and try again.";
"RECOVERY_PHASE_ERROR_LENGTH" = "Looks like you didn't enter enough words. Please check your recovery phrase and try again."; "RECOVERY_PHASE_ERROR_LENGTH" = "Looks like you didn't enter enough words. Please check your recovery phrase and try again.";

View File

@ -415,6 +415,7 @@
"DEFAULT_OPEN_GROUP_LOAD_ERROR_SUBTITLE" = "لطفا بعدا دوباره تلاش کنید"; "DEFAULT_OPEN_GROUP_LOAD_ERROR_SUBTITLE" = "لطفا بعدا دوباره تلاش کنید";
"LOADING_CONVERSATIONS" = "درحال بارگزاری پیام ها..."; "LOADING_CONVERSATIONS" = "درحال بارگزاری پیام ها...";
"DATABASE_STARTUP_FAILED" = "An error occurred when opening the database\n\nYou can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks"; "DATABASE_STARTUP_FAILED" = "An error occurred when opening the database\n\nYou can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks";
"DATABASE_RESTORE_FAILED" = "An error occurred when opening the restored database\n\nYou can export your application logs to be able to share for troubleshooting but to continue to use Session you may need to reinstall it";
"DATABASE_MIGRATION_FAILED" = "هنگام بهینه‌سازی پایگاه داده خطایی روی داد\n\nشما می‌توانید گزارش‌های برنامه خود را صادر کنید تا بتوانید برای عیب‌یابی به اشتراک بگذارید یا می‌توانید دستگاه خود را بازیابی کنید\n\nهشدار: بازیابی دستگاه شما منجر به از دست رفتن داده‌های قدیمی‌تر از دو هفته می‌شود."; "DATABASE_MIGRATION_FAILED" = "هنگام بهینه‌سازی پایگاه داده خطایی روی داد\n\nشما می‌توانید گزارش‌های برنامه خود را صادر کنید تا بتوانید برای عیب‌یابی به اشتراک بگذارید یا می‌توانید دستگاه خود را بازیابی کنید\n\nهشدار: بازیابی دستگاه شما منجر به از دست رفتن داده‌های قدیمی‌تر از دو هفته می‌شود.";
"RECOVERY_PHASE_ERROR_GENERIC" = "مشکلی پیش آمد. لطفاً عبارت بازیابی خود را بررسی کنید و دوباره امتحان کنید."; "RECOVERY_PHASE_ERROR_GENERIC" = "مشکلی پیش آمد. لطفاً عبارت بازیابی خود را بررسی کنید و دوباره امتحان کنید.";
"RECOVERY_PHASE_ERROR_LENGTH" = "به نظر می رسد کلمات کافی وارد نکرده اید. لطفاً عبارت بازیابی خود را بررسی کنید و دوباره امتحان کنید."; "RECOVERY_PHASE_ERROR_LENGTH" = "به نظر می رسد کلمات کافی وارد نکرده اید. لطفاً عبارت بازیابی خود را بررسی کنید و دوباره امتحان کنید.";

View File

@ -415,6 +415,7 @@
"DEFAULT_OPEN_GROUP_LOAD_ERROR_SUBTITLE" = "Please try again later"; "DEFAULT_OPEN_GROUP_LOAD_ERROR_SUBTITLE" = "Please try again later";
"LOADING_CONVERSATIONS" = "Loading Conversations..."; "LOADING_CONVERSATIONS" = "Loading Conversations...";
"DATABASE_STARTUP_FAILED" = "An error occurred when opening the database\n\nYou can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks"; "DATABASE_STARTUP_FAILED" = "An error occurred when opening the database\n\nYou can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks";
"DATABASE_RESTORE_FAILED" = "An error occurred when opening the restored database\n\nYou can export your application logs to be able to share for troubleshooting but to continue to use Session you may need to reinstall it";
"DATABASE_MIGRATION_FAILED" = "An error occurred when optimising the database\n\nYou can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks"; "DATABASE_MIGRATION_FAILED" = "An error occurred when optimising the database\n\nYou can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks";
"RECOVERY_PHASE_ERROR_GENERIC" = "Something went wrong. Please check your recovery phrase and try again."; "RECOVERY_PHASE_ERROR_GENERIC" = "Something went wrong. Please check your recovery phrase and try again.";
"RECOVERY_PHASE_ERROR_LENGTH" = "Looks like you didn't enter enough words. Please check your recovery phrase and try again."; "RECOVERY_PHASE_ERROR_LENGTH" = "Looks like you didn't enter enough words. Please check your recovery phrase and try again.";

View File

@ -415,6 +415,7 @@
"DEFAULT_OPEN_GROUP_LOAD_ERROR_SUBTITLE" = "Veuillez réessayer plus tard"; "DEFAULT_OPEN_GROUP_LOAD_ERROR_SUBTITLE" = "Veuillez réessayer plus tard";
"LOADING_CONVERSATIONS" = "Chargement des conversations..."; "LOADING_CONVERSATIONS" = "Chargement des conversations...";
"DATABASE_STARTUP_FAILED" = "An error occurred when opening the database\n\nYou can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks"; "DATABASE_STARTUP_FAILED" = "An error occurred when opening the database\n\nYou can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks";
"DATABASE_RESTORE_FAILED" = "An error occurred when opening the restored database\n\nYou can export your application logs to be able to share for troubleshooting but to continue to use Session you may need to reinstall it";
"DATABASE_MIGRATION_FAILED" = "Une erreur est survenue pendant l'optimisation de la base de données\n\nVous pouvez exporter votre journal d'application pour le partager et aider à régler le problème ou vous pouvez restaurer votre appareil\n\nAttention : restaurer votre appareil résultera en une perte des données des deux dernières semaines"; "DATABASE_MIGRATION_FAILED" = "Une erreur est survenue pendant l'optimisation de la base de données\n\nVous pouvez exporter votre journal d'application pour le partager et aider à régler le problème ou vous pouvez restaurer votre appareil\n\nAttention : restaurer votre appareil résultera en une perte des données des deux dernières semaines";
"RECOVERY_PHASE_ERROR_GENERIC" = "Quelque chose s'est mal passé. Vérifiez votre phrase de récupération et réessayez s'il vous plaît."; "RECOVERY_PHASE_ERROR_GENERIC" = "Quelque chose s'est mal passé. Vérifiez votre phrase de récupération et réessayez s'il vous plaît.";
"RECOVERY_PHASE_ERROR_LENGTH" = "Il semble que vous n'avez pas saisi tous les mots. Vérifiez votre phrase de récupération et réessayez s'il vous plaît."; "RECOVERY_PHASE_ERROR_LENGTH" = "Il semble que vous n'avez pas saisi tous les mots. Vérifiez votre phrase de récupération et réessayez s'il vous plaît.";

View File

@ -415,6 +415,7 @@
"DEFAULT_OPEN_GROUP_LOAD_ERROR_SUBTITLE" = "Please try again later"; "DEFAULT_OPEN_GROUP_LOAD_ERROR_SUBTITLE" = "Please try again later";
"LOADING_CONVERSATIONS" = "Loading Conversations..."; "LOADING_CONVERSATIONS" = "Loading Conversations...";
"DATABASE_STARTUP_FAILED" = "An error occurred when opening the database\n\nYou can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks"; "DATABASE_STARTUP_FAILED" = "An error occurred when opening the database\n\nYou can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks";
"DATABASE_RESTORE_FAILED" = "An error occurred when opening the restored database\n\nYou can export your application logs to be able to share for troubleshooting but to continue to use Session you may need to reinstall it";
"DATABASE_MIGRATION_FAILED" = "An error occurred when optimising the database\n\nYou can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks"; "DATABASE_MIGRATION_FAILED" = "An error occurred when optimising the database\n\nYou can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks";
"RECOVERY_PHASE_ERROR_GENERIC" = "Something went wrong. Please check your recovery phrase and try again."; "RECOVERY_PHASE_ERROR_GENERIC" = "Something went wrong. Please check your recovery phrase and try again.";
"RECOVERY_PHASE_ERROR_LENGTH" = "Looks like you didn't enter enough words. Please check your recovery phrase and try again."; "RECOVERY_PHASE_ERROR_LENGTH" = "Looks like you didn't enter enough words. Please check your recovery phrase and try again.";

View File

@ -415,6 +415,7 @@
"DEFAULT_OPEN_GROUP_LOAD_ERROR_SUBTITLE" = "Please try again later"; "DEFAULT_OPEN_GROUP_LOAD_ERROR_SUBTITLE" = "Please try again later";
"LOADING_CONVERSATIONS" = "Loading Conversations..."; "LOADING_CONVERSATIONS" = "Loading Conversations...";
"DATABASE_STARTUP_FAILED" = "An error occurred when opening the database\n\nYou can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks"; "DATABASE_STARTUP_FAILED" = "An error occurred when opening the database\n\nYou can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks";
"DATABASE_RESTORE_FAILED" = "An error occurred when opening the restored database\n\nYou can export your application logs to be able to share for troubleshooting but to continue to use Session you may need to reinstall it";
"DATABASE_MIGRATION_FAILED" = "An error occurred when optimising the database\n\nYou can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks"; "DATABASE_MIGRATION_FAILED" = "An error occurred when optimising the database\n\nYou can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks";
"RECOVERY_PHASE_ERROR_GENERIC" = "Something went wrong. Please check your recovery phrase and try again."; "RECOVERY_PHASE_ERROR_GENERIC" = "Something went wrong. Please check your recovery phrase and try again.";
"RECOVERY_PHASE_ERROR_LENGTH" = "Looks like you didn't enter enough words. Please check your recovery phrase and try again."; "RECOVERY_PHASE_ERROR_LENGTH" = "Looks like you didn't enter enough words. Please check your recovery phrase and try again.";

View File

@ -415,6 +415,7 @@
"DEFAULT_OPEN_GROUP_LOAD_ERROR_SUBTITLE" = "Please try again later"; "DEFAULT_OPEN_GROUP_LOAD_ERROR_SUBTITLE" = "Please try again later";
"LOADING_CONVERSATIONS" = "Loading Conversations..."; "LOADING_CONVERSATIONS" = "Loading Conversations...";
"DATABASE_STARTUP_FAILED" = "An error occurred when opening the database\n\nYou can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks"; "DATABASE_STARTUP_FAILED" = "An error occurred when opening the database\n\nYou can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks";
"DATABASE_RESTORE_FAILED" = "An error occurred when opening the restored database\n\nYou can export your application logs to be able to share for troubleshooting but to continue to use Session you may need to reinstall it";
"DATABASE_MIGRATION_FAILED" = "An error occurred when optimising the database\n\nYou can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks"; "DATABASE_MIGRATION_FAILED" = "An error occurred when optimising the database\n\nYou can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks";
"RECOVERY_PHASE_ERROR_GENERIC" = "Something went wrong. Please check your recovery phrase and try again."; "RECOVERY_PHASE_ERROR_GENERIC" = "Something went wrong. Please check your recovery phrase and try again.";
"RECOVERY_PHASE_ERROR_LENGTH" = "Looks like you didn't enter enough words. Please check your recovery phrase and try again."; "RECOVERY_PHASE_ERROR_LENGTH" = "Looks like you didn't enter enough words. Please check your recovery phrase and try again.";

View File

@ -415,6 +415,7 @@
"DEFAULT_OPEN_GROUP_LOAD_ERROR_SUBTITLE" = "Please try again later"; "DEFAULT_OPEN_GROUP_LOAD_ERROR_SUBTITLE" = "Please try again later";
"LOADING_CONVERSATIONS" = "Loading Conversations..."; "LOADING_CONVERSATIONS" = "Loading Conversations...";
"DATABASE_STARTUP_FAILED" = "An error occurred when opening the database\n\nYou can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks"; "DATABASE_STARTUP_FAILED" = "An error occurred when opening the database\n\nYou can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks";
"DATABASE_RESTORE_FAILED" = "An error occurred when opening the restored database\n\nYou can export your application logs to be able to share for troubleshooting but to continue to use Session you may need to reinstall it";
"DATABASE_MIGRATION_FAILED" = "An error occurred when optimising the database\n\nYou can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks"; "DATABASE_MIGRATION_FAILED" = "An error occurred when optimising the database\n\nYou can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks";
"RECOVERY_PHASE_ERROR_GENERIC" = "Something went wrong. Please check your recovery phrase and try again."; "RECOVERY_PHASE_ERROR_GENERIC" = "Something went wrong. Please check your recovery phrase and try again.";
"RECOVERY_PHASE_ERROR_LENGTH" = "Looks like you didn't enter enough words. Please check your recovery phrase and try again."; "RECOVERY_PHASE_ERROR_LENGTH" = "Looks like you didn't enter enough words. Please check your recovery phrase and try again.";

View File

@ -415,6 +415,7 @@
"DEFAULT_OPEN_GROUP_LOAD_ERROR_SUBTITLE" = "Please try again later"; "DEFAULT_OPEN_GROUP_LOAD_ERROR_SUBTITLE" = "Please try again later";
"LOADING_CONVERSATIONS" = "Loading Conversations..."; "LOADING_CONVERSATIONS" = "Loading Conversations...";
"DATABASE_STARTUP_FAILED" = "An error occurred when opening the database\n\nYou can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks"; "DATABASE_STARTUP_FAILED" = "An error occurred when opening the database\n\nYou can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks";
"DATABASE_RESTORE_FAILED" = "An error occurred when opening the restored database\n\nYou can export your application logs to be able to share for troubleshooting but to continue to use Session you may need to reinstall it";
"DATABASE_MIGRATION_FAILED" = "An error occurred when optimising the database\n\nYou can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks"; "DATABASE_MIGRATION_FAILED" = "An error occurred when optimising the database\n\nYou can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks";
"RECOVERY_PHASE_ERROR_GENERIC" = "Something went wrong. Please check your recovery phrase and try again."; "RECOVERY_PHASE_ERROR_GENERIC" = "Something went wrong. Please check your recovery phrase and try again.";
"RECOVERY_PHASE_ERROR_LENGTH" = "Looks like you didn't enter enough words. Please check your recovery phrase and try again."; "RECOVERY_PHASE_ERROR_LENGTH" = "Looks like you didn't enter enough words. Please check your recovery phrase and try again.";

View File

@ -415,6 +415,7 @@
"DEFAULT_OPEN_GROUP_LOAD_ERROR_SUBTITLE" = "Please try again later"; "DEFAULT_OPEN_GROUP_LOAD_ERROR_SUBTITLE" = "Please try again later";
"LOADING_CONVERSATIONS" = "Loading Conversations..."; "LOADING_CONVERSATIONS" = "Loading Conversations...";
"DATABASE_STARTUP_FAILED" = "An error occurred when opening the database\n\nYou can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks"; "DATABASE_STARTUP_FAILED" = "An error occurred when opening the database\n\nYou can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks";
"DATABASE_RESTORE_FAILED" = "An error occurred when opening the restored database\n\nYou can export your application logs to be able to share for troubleshooting but to continue to use Session you may need to reinstall it";
"DATABASE_MIGRATION_FAILED" = "An error occurred when optimising the database\n\nYou can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks"; "DATABASE_MIGRATION_FAILED" = "An error occurred when optimising the database\n\nYou can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks";
"RECOVERY_PHASE_ERROR_GENERIC" = "Something went wrong. Please check your recovery phrase and try again."; "RECOVERY_PHASE_ERROR_GENERIC" = "Something went wrong. Please check your recovery phrase and try again.";
"RECOVERY_PHASE_ERROR_LENGTH" = "Looks like you didn't enter enough words. Please check your recovery phrase and try again."; "RECOVERY_PHASE_ERROR_LENGTH" = "Looks like you didn't enter enough words. Please check your recovery phrase and try again.";

View File

@ -415,6 +415,7 @@
"DEFAULT_OPEN_GROUP_LOAD_ERROR_SUBTITLE" = "Please try again later"; "DEFAULT_OPEN_GROUP_LOAD_ERROR_SUBTITLE" = "Please try again later";
"LOADING_CONVERSATIONS" = "Loading Conversations..."; "LOADING_CONVERSATIONS" = "Loading Conversations...";
"DATABASE_STARTUP_FAILED" = "An error occurred when opening the database\n\nYou can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks"; "DATABASE_STARTUP_FAILED" = "An error occurred when opening the database\n\nYou can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks";
"DATABASE_RESTORE_FAILED" = "An error occurred when opening the restored database\n\nYou can export your application logs to be able to share for troubleshooting but to continue to use Session you may need to reinstall it";
"DATABASE_MIGRATION_FAILED" = "An error occurred when optimising the database\n\nYou can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks"; "DATABASE_MIGRATION_FAILED" = "An error occurred when optimising the database\n\nYou can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks";
"RECOVERY_PHASE_ERROR_GENERIC" = "Something went wrong. Please check your recovery phrase and try again."; "RECOVERY_PHASE_ERROR_GENERIC" = "Something went wrong. Please check your recovery phrase and try again.";
"RECOVERY_PHASE_ERROR_LENGTH" = "Looks like you didn't enter enough words. Please check your recovery phrase and try again."; "RECOVERY_PHASE_ERROR_LENGTH" = "Looks like you didn't enter enough words. Please check your recovery phrase and try again.";

View File

@ -415,6 +415,7 @@
"DEFAULT_OPEN_GROUP_LOAD_ERROR_SUBTITLE" = "Please try again later"; "DEFAULT_OPEN_GROUP_LOAD_ERROR_SUBTITLE" = "Please try again later";
"LOADING_CONVERSATIONS" = "Loading Conversations..."; "LOADING_CONVERSATIONS" = "Loading Conversations...";
"DATABASE_STARTUP_FAILED" = "An error occurred when opening the database\n\nYou can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks"; "DATABASE_STARTUP_FAILED" = "An error occurred when opening the database\n\nYou can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks";
"DATABASE_RESTORE_FAILED" = "An error occurred when opening the restored database\n\nYou can export your application logs to be able to share for troubleshooting but to continue to use Session you may need to reinstall it";
"DATABASE_MIGRATION_FAILED" = "An error occurred when optimising the database\n\nYou can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks"; "DATABASE_MIGRATION_FAILED" = "An error occurred when optimising the database\n\nYou can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks";
"RECOVERY_PHASE_ERROR_GENERIC" = "Something went wrong. Please check your recovery phrase and try again."; "RECOVERY_PHASE_ERROR_GENERIC" = "Something went wrong. Please check your recovery phrase and try again.";
"RECOVERY_PHASE_ERROR_LENGTH" = "Looks like you didn't enter enough words. Please check your recovery phrase and try again."; "RECOVERY_PHASE_ERROR_LENGTH" = "Looks like you didn't enter enough words. Please check your recovery phrase and try again.";

View File

@ -415,6 +415,7 @@
"DEFAULT_OPEN_GROUP_LOAD_ERROR_SUBTITLE" = "Please try again later"; "DEFAULT_OPEN_GROUP_LOAD_ERROR_SUBTITLE" = "Please try again later";
"LOADING_CONVERSATIONS" = "Loading Conversations..."; "LOADING_CONVERSATIONS" = "Loading Conversations...";
"DATABASE_STARTUP_FAILED" = "An error occurred when opening the database\n\nYou can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks"; "DATABASE_STARTUP_FAILED" = "An error occurred when opening the database\n\nYou can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks";
"DATABASE_RESTORE_FAILED" = "An error occurred when opening the restored database\n\nYou can export your application logs to be able to share for troubleshooting but to continue to use Session you may need to reinstall it";
"DATABASE_MIGRATION_FAILED" = "An error occurred when optimising the database\n\nYou can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks"; "DATABASE_MIGRATION_FAILED" = "An error occurred when optimising the database\n\nYou can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks";
"RECOVERY_PHASE_ERROR_GENERIC" = "Something went wrong. Please check your recovery phrase and try again."; "RECOVERY_PHASE_ERROR_GENERIC" = "Something went wrong. Please check your recovery phrase and try again.";
"RECOVERY_PHASE_ERROR_LENGTH" = "Looks like you didn't enter enough words. Please check your recovery phrase and try again."; "RECOVERY_PHASE_ERROR_LENGTH" = "Looks like you didn't enter enough words. Please check your recovery phrase and try again.";

View File

@ -415,6 +415,7 @@
"DEFAULT_OPEN_GROUP_LOAD_ERROR_SUBTITLE" = "Please try again later"; "DEFAULT_OPEN_GROUP_LOAD_ERROR_SUBTITLE" = "Please try again later";
"LOADING_CONVERSATIONS" = "Loading Conversations..."; "LOADING_CONVERSATIONS" = "Loading Conversations...";
"DATABASE_STARTUP_FAILED" = "An error occurred when opening the database\n\nYou can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks"; "DATABASE_STARTUP_FAILED" = "An error occurred when opening the database\n\nYou can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks";
"DATABASE_RESTORE_FAILED" = "An error occurred when opening the restored database\n\nYou can export your application logs to be able to share for troubleshooting but to continue to use Session you may need to reinstall it";
"DATABASE_MIGRATION_FAILED" = "An error occurred when optimising the database\n\nYou can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks"; "DATABASE_MIGRATION_FAILED" = "An error occurred when optimising the database\n\nYou can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks";
"RECOVERY_PHASE_ERROR_GENERIC" = "Something went wrong. Please check your recovery phrase and try again."; "RECOVERY_PHASE_ERROR_GENERIC" = "Something went wrong. Please check your recovery phrase and try again.";
"RECOVERY_PHASE_ERROR_LENGTH" = "Looks like you didn't enter enough words. Please check your recovery phrase and try again."; "RECOVERY_PHASE_ERROR_LENGTH" = "Looks like you didn't enter enough words. Please check your recovery phrase and try again.";

View File

@ -415,6 +415,7 @@
"DEFAULT_OPEN_GROUP_LOAD_ERROR_SUBTITLE" = "Please try again later"; "DEFAULT_OPEN_GROUP_LOAD_ERROR_SUBTITLE" = "Please try again later";
"LOADING_CONVERSATIONS" = "Loading Conversations..."; "LOADING_CONVERSATIONS" = "Loading Conversations...";
"DATABASE_STARTUP_FAILED" = "An error occurred when opening the database\n\nYou can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks"; "DATABASE_STARTUP_FAILED" = "An error occurred when opening the database\n\nYou can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks";
"DATABASE_RESTORE_FAILED" = "An error occurred when opening the restored database\n\nYou can export your application logs to be able to share for troubleshooting but to continue to use Session you may need to reinstall it";
"DATABASE_MIGRATION_FAILED" = "An error occurred when optimising the database\n\nYou can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks"; "DATABASE_MIGRATION_FAILED" = "An error occurred when optimising the database\n\nYou can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks";
"RECOVERY_PHASE_ERROR_GENERIC" = "Something went wrong. Please check your recovery phrase and try again."; "RECOVERY_PHASE_ERROR_GENERIC" = "Something went wrong. Please check your recovery phrase and try again.";
"RECOVERY_PHASE_ERROR_LENGTH" = "Looks like you didn't enter enough words. Please check your recovery phrase and try again."; "RECOVERY_PHASE_ERROR_LENGTH" = "Looks like you didn't enter enough words. Please check your recovery phrase and try again.";

View File

@ -415,6 +415,7 @@
"DEFAULT_OPEN_GROUP_LOAD_ERROR_SUBTITLE" = "Please try again later"; "DEFAULT_OPEN_GROUP_LOAD_ERROR_SUBTITLE" = "Please try again later";
"LOADING_CONVERSATIONS" = "Loading Conversations..."; "LOADING_CONVERSATIONS" = "Loading Conversations...";
"DATABASE_STARTUP_FAILED" = "An error occurred when opening the database\n\nYou can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks"; "DATABASE_STARTUP_FAILED" = "An error occurred when opening the database\n\nYou can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks";
"DATABASE_RESTORE_FAILED" = "An error occurred when opening the restored database\n\nYou can export your application logs to be able to share for troubleshooting but to continue to use Session you may need to reinstall it";
"DATABASE_MIGRATION_FAILED" = "An error occurred when optimising the database\n\nYou can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks"; "DATABASE_MIGRATION_FAILED" = "An error occurred when optimising the database\n\nYou can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks";
"RECOVERY_PHASE_ERROR_GENERIC" = "Something went wrong. Please check your recovery phrase and try again."; "RECOVERY_PHASE_ERROR_GENERIC" = "Something went wrong. Please check your recovery phrase and try again.";
"RECOVERY_PHASE_ERROR_LENGTH" = "Looks like you didn't enter enough words. Please check your recovery phrase and try again."; "RECOVERY_PHASE_ERROR_LENGTH" = "Looks like you didn't enter enough words. Please check your recovery phrase and try again.";

View File

@ -415,6 +415,7 @@
"DEFAULT_OPEN_GROUP_LOAD_ERROR_SUBTITLE" = "Please try again later"; "DEFAULT_OPEN_GROUP_LOAD_ERROR_SUBTITLE" = "Please try again later";
"LOADING_CONVERSATIONS" = "Loading Conversations..."; "LOADING_CONVERSATIONS" = "Loading Conversations...";
"DATABASE_STARTUP_FAILED" = "An error occurred when opening the database\n\nYou can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks"; "DATABASE_STARTUP_FAILED" = "An error occurred when opening the database\n\nYou can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks";
"DATABASE_RESTORE_FAILED" = "An error occurred when opening the restored database\n\nYou can export your application logs to be able to share for troubleshooting but to continue to use Session you may need to reinstall it";
"DATABASE_MIGRATION_FAILED" = "An error occurred when optimising the database\n\nYou can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks"; "DATABASE_MIGRATION_FAILED" = "An error occurred when optimising the database\n\nYou can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks";
"RECOVERY_PHASE_ERROR_GENERIC" = "Something went wrong. Please check your recovery phrase and try again."; "RECOVERY_PHASE_ERROR_GENERIC" = "Something went wrong. Please check your recovery phrase and try again.";
"RECOVERY_PHASE_ERROR_LENGTH" = "Looks like you didn't enter enough words. Please check your recovery phrase and try again."; "RECOVERY_PHASE_ERROR_LENGTH" = "Looks like you didn't enter enough words. Please check your recovery phrase and try again.";

View File

@ -415,6 +415,7 @@
"DEFAULT_OPEN_GROUP_LOAD_ERROR_SUBTITLE" = "Please try again later"; "DEFAULT_OPEN_GROUP_LOAD_ERROR_SUBTITLE" = "Please try again later";
"LOADING_CONVERSATIONS" = "Loading Conversations..."; "LOADING_CONVERSATIONS" = "Loading Conversations...";
"DATABASE_STARTUP_FAILED" = "An error occurred when opening the database\n\nYou can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks"; "DATABASE_STARTUP_FAILED" = "An error occurred when opening the database\n\nYou can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks";
"DATABASE_RESTORE_FAILED" = "An error occurred when opening the restored database\n\nYou can export your application logs to be able to share for troubleshooting but to continue to use Session you may need to reinstall it";
"DATABASE_MIGRATION_FAILED" = "An error occurred when optimising the database\n\nYou can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks"; "DATABASE_MIGRATION_FAILED" = "An error occurred when optimising the database\n\nYou can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks";
"RECOVERY_PHASE_ERROR_GENERIC" = "Something went wrong. Please check your recovery phrase and try again."; "RECOVERY_PHASE_ERROR_GENERIC" = "Something went wrong. Please check your recovery phrase and try again.";
"RECOVERY_PHASE_ERROR_LENGTH" = "Looks like you didn't enter enough words. Please check your recovery phrase and try again."; "RECOVERY_PHASE_ERROR_LENGTH" = "Looks like you didn't enter enough words. Please check your recovery phrase and try again.";

View File

@ -415,6 +415,7 @@
"DEFAULT_OPEN_GROUP_LOAD_ERROR_SUBTITLE" = "Please try again later"; "DEFAULT_OPEN_GROUP_LOAD_ERROR_SUBTITLE" = "Please try again later";
"LOADING_CONVERSATIONS" = "Loading Conversations..."; "LOADING_CONVERSATIONS" = "Loading Conversations...";
"DATABASE_STARTUP_FAILED" = "An error occurred when opening the database\n\nYou can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks"; "DATABASE_STARTUP_FAILED" = "An error occurred when opening the database\n\nYou can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks";
"DATABASE_RESTORE_FAILED" = "An error occurred when opening the restored database\n\nYou can export your application logs to be able to share for troubleshooting but to continue to use Session you may need to reinstall it";
"DATABASE_MIGRATION_FAILED" = "An error occurred when optimising the database\n\nYou can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks"; "DATABASE_MIGRATION_FAILED" = "An error occurred when optimising the database\n\nYou can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks";
"RECOVERY_PHASE_ERROR_GENERIC" = "Something went wrong. Please check your recovery phrase and try again."; "RECOVERY_PHASE_ERROR_GENERIC" = "Something went wrong. Please check your recovery phrase and try again.";
"RECOVERY_PHASE_ERROR_LENGTH" = "Looks like you didn't enter enough words. Please check your recovery phrase and try again."; "RECOVERY_PHASE_ERROR_LENGTH" = "Looks like you didn't enter enough words. Please check your recovery phrase and try again.";

View File

@ -415,6 +415,7 @@
"DEFAULT_OPEN_GROUP_LOAD_ERROR_SUBTITLE" = "Please try again later"; "DEFAULT_OPEN_GROUP_LOAD_ERROR_SUBTITLE" = "Please try again later";
"LOADING_CONVERSATIONS" = "Loading Conversations..."; "LOADING_CONVERSATIONS" = "Loading Conversations...";
"DATABASE_STARTUP_FAILED" = "An error occurred when opening the database\n\nYou can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks"; "DATABASE_STARTUP_FAILED" = "An error occurred when opening the database\n\nYou can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks";
"DATABASE_RESTORE_FAILED" = "An error occurred when opening the restored database\n\nYou can export your application logs to be able to share for troubleshooting but to continue to use Session you may need to reinstall it";
"DATABASE_MIGRATION_FAILED" = "An error occurred when optimising the database\n\nYou can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks"; "DATABASE_MIGRATION_FAILED" = "An error occurred when optimising the database\n\nYou can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks";
"RECOVERY_PHASE_ERROR_GENERIC" = "Something went wrong. Please check your recovery phrase and try again."; "RECOVERY_PHASE_ERROR_GENERIC" = "Something went wrong. Please check your recovery phrase and try again.";
"RECOVERY_PHASE_ERROR_LENGTH" = "Looks like you didn't enter enough words. Please check your recovery phrase and try again."; "RECOVERY_PHASE_ERROR_LENGTH" = "Looks like you didn't enter enough words. Please check your recovery phrase and try again.";

View File

@ -575,9 +575,7 @@ class NotificationActionHandler {
threadVariant: thread.variant threadVariant: thread.variant
) )
} }
.subscribe(on: DispatchQueue.global(qos: .userInitiated))
.flatMap { MessageSender.sendImmediate(preparedSendData: $0) } .flatMap { MessageSender.sendImmediate(preparedSendData: $0) }
.receive(on: DispatchQueue.main)
.handleEvents( .handleEvents(
receiveCompletion: { result in receiveCompletion: { result in
switch result { switch result {

View File

@ -52,8 +52,9 @@ public enum PushRegistrationError: Error {
Logger.info("") Logger.info("")
return registerUserNotificationSettings() return registerUserNotificationSettings()
.setFailureType(to: Error.self) .subscribe(on: DispatchQueue.global(qos: .default))
.receive(on: DispatchQueue.main) // MUST be on main thread .receive(on: DispatchQueue.main) // MUST be on main thread
.setFailureType(to: Error.self)
.tryFlatMap { _ -> AnyPublisher<(pushToken: String, voipToken: String), Error> in .tryFlatMap { _ -> AnyPublisher<(pushToken: String, voipToken: String), Error> in
#if targetEnvironment(simulator) #if targetEnvironment(simulator)
throw PushRegistrationError.pushNotSupported(description: "Push not supported on simulators") throw PushRegistrationError.pushNotSupported(description: "Push not supported on simulators")

View File

@ -251,6 +251,8 @@ public class UserNotificationActionHandler: NSObject {
func handleNotificationResponse( _ response: UNNotificationResponse, completionHandler: @escaping () -> Void) { func handleNotificationResponse( _ response: UNNotificationResponse, completionHandler: @escaping () -> Void) {
AssertIsOnMainThread() AssertIsOnMainThread()
handleNotificationResponse(response) handleNotificationResponse(response)
.subscribe(on: DispatchQueue.global(qos: .userInitiated))
.receive(on: DispatchQueue.main)
.sinkUntilComplete( .sinkUntilComplete(
receiveCompletion: { result in receiveCompletion: { result in
switch result { switch result {

View File

@ -189,7 +189,7 @@ final class DisplayNameVC: BaseVC {
// Try to save the user name but ignore the result // Try to save the user name but ignore the result
ProfileManager.updateLocal( ProfileManager.updateLocal(
queue: DispatchQueue.global(qos: .default), queue: .global(qos: .default),
profileName: displayName profileName: displayName
) )

View File

@ -36,14 +36,12 @@ enum Onboarding {
let userPublicKey: String = getUserHexEncodedPublicKey() let userPublicKey: String = getUserHexEncodedPublicKey()
return SnodeAPI.getSwarm(for: userPublicKey) return SnodeAPI.getSwarm(for: userPublicKey)
.subscribe(on: DispatchQueue.global(qos: .userInitiated))
.tryFlatMapWithRandomSnode { snode -> AnyPublisher<Void, Error> in .tryFlatMapWithRandomSnode { snode -> AnyPublisher<Void, Error> in
CurrentUserPoller CurrentUserPoller
.poll( .poll(
namespaces: [.configUserProfile], namespaces: [.configUserProfile],
from: snode, from: snode,
for: userPublicKey, for: userPublicKey,
on: DispatchQueue.global(qos: .userInitiated),
// Note: These values mean the received messages will be // Note: These values mean the received messages will be
// processed immediately rather than async as part of a Job // processed immediately rather than async as part of a Job
calledFromBackgroundPoller: true, calledFromBackgroundPoller: true,
@ -67,7 +65,6 @@ enum Onboarding {
namespaces: [.default], namespaces: [.default],
from: snode, from: snode,
for: userPublicKey, for: userPublicKey,
on: DispatchQueue.global(qos: .userInitiated),
// Note: These values mean the received messages will be // Note: These values mean the received messages will be
// processed immediately rather than async as part of a Job // processed immediately rather than async as part of a Job
calledFromBackgroundPoller: true, calledFromBackgroundPoller: true,
@ -215,7 +212,9 @@ enum Onboarding {
guard self != .register else { return } guard self != .register else { return }
// Fetch the // Fetch the
Onboarding.profileNamePublisher.sinkUntilComplete() Onboarding.profileNamePublisher
.subscribe(on: DispatchQueue.global(qos: .userInitiated))
.sinkUntilComplete()
} }
func completeRegistration() { func completeRegistration() {

View File

@ -176,6 +176,7 @@ final class PNModeVC: BaseVC, OptionViewDelegate {
// If we don't have one then show a loading indicator and try to retrieve the existing name // If we don't have one then show a loading indicator and try to retrieve the existing name
ModalActivityIndicatorViewController.present(fromViewController: self) { [weak self, flow = self.flow] viewController in ModalActivityIndicatorViewController.present(fromViewController: self) { [weak self, flow = self.flow] viewController in
Onboarding.profileNamePublisher Onboarding.profileNamePublisher
.subscribe(on: DispatchQueue.global(qos: .userInitiated))
.timeout(.seconds(15), scheduler: DispatchQueue.main, customError: { HTTPError.timeout }) .timeout(.seconds(15), scheduler: DispatchQueue.main, customError: { HTTPError.timeout })
.catch { _ -> AnyPublisher<String?, Error> in .catch { _ -> AnyPublisher<String?, Error> in
SNLog("Onboarding failed to retrieve existing profile information") SNLog("Onboarding failed to retrieve existing profile information")

View File

@ -143,7 +143,8 @@ final class OpenGroupSuggestionGrid: UIView, UICollectionViewDataSource, UIColle
widthAnchor.constraint(greaterThanOrEqualToConstant: OpenGroupSuggestionGrid.cellHeight).isActive = true widthAnchor.constraint(greaterThanOrEqualToConstant: OpenGroupSuggestionGrid.cellHeight).isActive = true
OpenGroupManager.getDefaultRoomsIfNeeded() OpenGroupManager.getDefaultRoomsIfNeeded()
.receive(on: DispatchQueue.main) .subscribe(on: DispatchQueue.global(qos: .default))
.receive(on: DispatchQueue.main, immediatelyIfMain: true)
.sinkUntilComplete( .sinkUntilComplete(
receiveCompletion: { [weak self] _ in self?.update() }, receiveCompletion: { [weak self] _ in self?.update() },
receiveValue: { [weak self] rooms in self?.rooms = rooms } receiveValue: { [weak self] rooms in self?.rooms = rooms }
@ -336,7 +337,7 @@ extension OpenGroupSuggestionGrid {
.eraseToAnyPublisher() .eraseToAnyPublisher()
) )
.subscribe(on: DispatchQueue.global(qos: .userInitiated)) .subscribe(on: DispatchQueue.global(qos: .userInitiated))
.receiveOnMain(immediately: true) .receive(on: DispatchQueue.main, immediatelyIfMain: true)
.sinkUntilComplete( .sinkUntilComplete(
receiveValue: { [weak self] imageData, hasData in receiveValue: { [weak self] imageData, hasData in
guard hasData else { guard hasData else {

View File

@ -104,6 +104,7 @@ class ConversationSettingsViewModel: SessionTableViewModel<NoNav, ConversationSe
] ]
} }
.removeDuplicates() .removeDuplicates()
.handleEvents(didFail: { SNLog("[ConversationSettingsViewModel] Observation failed with error: \($0)") })
.publisher(in: Storage.shared) .publisher(in: Storage.shared)
.mapToSessionTableViewData(for: self) .mapToSessionTableViewData(for: self)
} }

View File

@ -168,6 +168,7 @@ class HelpViewModel: SessionTableViewModel<NoNav, HelpViewModel.Section, HelpVie
#endif #endif
} }
.removeDuplicates() .removeDuplicates()
.handleEvents(didFail: { SNLog("[HelpViewModel] Observation failed with error: \($0)") })
.publisher(in: Storage.shared) .publisher(in: Storage.shared)
.mapToSessionTableViewData(for: self) .mapToSessionTableViewData(for: self)

View File

@ -69,6 +69,7 @@ class NotificationContentViewModel: SessionTableViewModel<NoNav, NotificationSet
] ]
} }
.removeDuplicates() .removeDuplicates()
.handleEvents(didFail: { SNLog("[NotificationContentViewModel] Observation failed with error: \($0)") })
.publisher(in: storage, scheduling: scheduler) .publisher(in: storage, scheduling: scheduler)
.mapToSessionTableViewData(for: self) .mapToSessionTableViewData(for: self)
} }

View File

@ -147,6 +147,7 @@ class NotificationSettingsViewModel: SessionTableViewModel<NoNav, NotificationSe
] ]
} }
.removeDuplicates() .removeDuplicates()
.handleEvents(didFail: { SNLog("[NotificationSettingsViewModel] Observation failed with error: \($0)") })
.publisher(in: Storage.shared) .publisher(in: Storage.shared)
.mapToSessionTableViewData(for: self) .mapToSessionTableViewData(for: self)
} }

View File

@ -146,6 +146,7 @@ class NotificationSoundViewModel: SessionTableViewModel<NotificationSoundViewMod
] ]
} }
.removeDuplicates() .removeDuplicates()
.handleEvents(didFail: { SNLog("[NotificationSoundViewModel] Observation failed with error: \($0)") })
.publisher(in: Storage.shared) .publisher(in: Storage.shared)
.mapToSessionTableViewData(for: self) .mapToSessionTableViewData(for: self)

View File

@ -150,6 +150,7 @@ final class NukeDataModal: Modal {
private func clearDeviceOnly() { private func clearDeviceOnly() {
ModalActivityIndicatorViewController.present(fromViewController: self, canCancel: false) { [weak self] _ in ModalActivityIndicatorViewController.present(fromViewController: self, canCancel: false) { [weak self] _ in
ConfigurationSyncJob.run() ConfigurationSyncJob.run()
.subscribe(on: DispatchQueue.global(qos: .userInitiated))
.receive(on: DispatchQueue.main) .receive(on: DispatchQueue.main)
.sinkUntilComplete( .sinkUntilComplete(
receiveCompletion: { _ in receiveCompletion: { _ in
@ -164,7 +165,7 @@ final class NukeDataModal: Modal {
ModalActivityIndicatorViewController ModalActivityIndicatorViewController
.present(fromViewController: presentedViewController, canCancel: false) { [weak self] _ in .present(fromViewController: presentedViewController, canCancel: false) { [weak self] _ in
SnodeAPI.deleteAllMessages(namespace: .all) SnodeAPI.deleteAllMessages(namespace: .all)
.subscribe(on: DispatchQueue.global(qos: .default)) .subscribe(on: DispatchQueue.global(qos: .userInitiated))
.receive(on: DispatchQueue.main) .receive(on: DispatchQueue.main)
.sinkUntilComplete( .sinkUntilComplete(
receiveCompletion: { result in receiveCompletion: { result in

View File

@ -232,6 +232,7 @@ class PrivacySettingsViewModel: SessionTableViewModel<PrivacySettingsViewModel.N
] ]
} }
.removeDuplicates() .removeDuplicates()
.handleEvents(didFail: { SNLog("[PrivacySettingsViewModel] Observation failed with error: \($0)") })
.publisher(in: Storage.shared) .publisher(in: Storage.shared)
.mapToSessionTableViewData(for: self) .mapToSessionTableViewData(for: self)
} }

View File

@ -466,6 +466,7 @@ class SettingsViewModel: SessionTableViewModel<SettingsViewModel.NavButton, Sett
] ]
} }
.removeDuplicates() .removeDuplicates()
.handleEvents(didFail: { SNLog("[SettingsViewModel] Observation failed with error: \($0)") })
.publisher(in: Storage.shared) .publisher(in: Storage.shared)
.mapToSessionTableViewData(for: self) .mapToSessionTableViewData(for: self)
@ -572,7 +573,7 @@ class SettingsViewModel: SessionTableViewModel<SettingsViewModel.NavButton, Sett
) { ) {
let viewController = ModalActivityIndicatorViewController(canCancel: false) { [weak self] modalActivityIndicator in let viewController = ModalActivityIndicatorViewController(canCancel: false) { [weak self] modalActivityIndicator in
ProfileManager.updateLocal( ProfileManager.updateLocal(
queue: DispatchQueue.global(qos: .default), queue: .global(qos: .default),
profileName: name, profileName: name,
avatarUpdate: avatarUpdate, avatarUpdate: avatarUpdate,
success: { db in success: { db in

View File

@ -187,11 +187,12 @@ class SessionTableViewController<NavItemId: Equatable, Section: SessionTableSect
private func startObservingChanges() { private func startObservingChanges() {
// Start observing for data changes // Start observing for data changes
dataChangeCancellable = viewModel.observableTableData dataChangeCancellable = viewModel.observableTableData
.receiveOnMain( .receive(
on: DispatchQueue.main,
// If we haven't done the initial load the trigger it immediately (blocking the main // If we haven't done the initial load the trigger it immediately (blocking the main
// thread so we remain on the launch screen until it completes to be consistent with // thread so we remain on the launch screen until it completes to be consistent with
// the old behaviour) // the old behaviour)
immediately: !hasLoadedInitialTableData immediatelyIfMain: !hasLoadedInitialTableData
) )
.sink( .sink(
receiveCompletion: { [weak self] result in receiveCompletion: { [weak self] result in
@ -333,7 +334,7 @@ class SessionTableViewController<NavItemId: Equatable, Section: SessionTableSect
.store(in: &disposables) .store(in: &disposables)
viewModel.leftNavItems viewModel.leftNavItems
.receiveOnMain(immediately: true) .receive(on: DispatchQueue.main, immediatelyIfMain: true)
.sink { [weak self] maybeItems in .sink { [weak self] maybeItems in
self?.navigationItem.setLeftBarButtonItems( self?.navigationItem.setLeftBarButtonItems(
maybeItems.map { items in maybeItems.map { items in
@ -355,7 +356,7 @@ class SessionTableViewController<NavItemId: Equatable, Section: SessionTableSect
.store(in: &disposables) .store(in: &disposables)
viewModel.rightNavItems viewModel.rightNavItems
.receiveOnMain(immediately: true) .receive(on: DispatchQueue.main, immediatelyIfMain: true)
.sink { [weak self] maybeItems in .sink { [weak self] maybeItems in
self?.navigationItem.setRightBarButtonItems( self?.navigationItem.setRightBarButtonItems(
maybeItems.map { items in maybeItems.map { items in
@ -377,21 +378,21 @@ class SessionTableViewController<NavItemId: Equatable, Section: SessionTableSect
.store(in: &disposables) .store(in: &disposables)
viewModel.emptyStateTextPublisher viewModel.emptyStateTextPublisher
.receiveOnMain(immediately: true) .receive(on: DispatchQueue.main, immediatelyIfMain: true)
.sink { [weak self] text in .sink { [weak self] text in
self?.emptyStateLabel.text = text self?.emptyStateLabel.text = text
} }
.store(in: &disposables) .store(in: &disposables)
viewModel.footerView viewModel.footerView
.receiveOnMain(immediately: true) .receive(on: DispatchQueue.main, immediatelyIfMain: true)
.sink { [weak self] footerView in .sink { [weak self] footerView in
self?.tableView.tableFooterView = footerView self?.tableView.tableFooterView = footerView
} }
.store(in: &disposables) .store(in: &disposables)
viewModel.footerButtonInfo viewModel.footerButtonInfo
.receiveOnMain(immediately: true) .receive(on: DispatchQueue.main, immediatelyIfMain: true)
.sink { [weak self] buttonInfo in .sink { [weak self] buttonInfo in
if let buttonInfo: SessionButton.Info = buttonInfo { if let buttonInfo: SessionButton.Info = buttonInfo {
self?.footerButton.setTitle(buttonInfo.title, for: .normal) self?.footerButton.setTitle(buttonInfo.title, for: .normal)

View File

@ -11,21 +11,32 @@ public final class BackgroundPoller {
private static var publishers: [AnyPublisher<Void, Error>] = [] private static var publishers: [AnyPublisher<Void, Error>] = []
public static var isValid: Bool = false public static var isValid: Bool = false
public static func poll(completionHandler: @escaping (UIBackgroundFetchResult) -> Void) { public static func poll(
completionHandler: @escaping (UIBackgroundFetchResult) -> Void,
dependencies: OpenGroupManager.OGMDependencies = OpenGroupManager.OGMDependencies(
subscribeQueue: .global(qos: .background),
receiveQueue: .main
)
) {
Publishers Publishers
.MergeMany( .MergeMany(
[pollForMessages()] [pollForMessages(using: dependencies)]
.appending(contentsOf: pollForClosedGroupMessages()) .appending(contentsOf: pollForClosedGroupMessages(using: dependencies))
.appending( .appending(
contentsOf: Storage.shared contentsOf: Storage.shared
.read { db in .read { db in
// The default room promise creates an OpenGroup with an empty /// The default room promise creates an OpenGroup with an empty `roomToken` value, we
// `roomToken` value, we don't want to start a poller for this /// don't want to start a poller for this as the user hasn't actually joined a room
// as the user hasn't actually joined a room ///
/// We also want to exclude any rooms which have failed to poll too many times in a row from
/// the background poll as they are likely to fail again
try OpenGroup try OpenGroup
.select(.server) .select(.server)
.filter(OpenGroup.Columns.roomToken != "") .filter(
.filter(OpenGroup.Columns.isActive) OpenGroup.Columns.roomToken != "" &&
OpenGroup.Columns.isActive &&
OpenGroup.Columns.pollFailureCount < OpenGroupAPI.Poller.maxRoomFailureCountForBackgroundPoll
)
.distinct() .distinct()
.asRequest(of: String.self) .asRequest(of: String.self)
.fetchSet(db) .fetchSet(db)
@ -38,13 +49,14 @@ public final class BackgroundPoller {
return poller.poll( return poller.poll(
calledFromBackgroundPoller: true, calledFromBackgroundPoller: true,
isBackgroundPollerValid: { BackgroundPoller.isValid }, isBackgroundPollerValid: { BackgroundPoller.isValid },
isPostCapabilitiesRetry: false isPostCapabilitiesRetry: false,
using: dependencies
) )
} }
) )
) )
.subscribeOnMain(immediately: true) .subscribe(on: dependencies.subscribeQueue, immediatelyIfMain: true)
.receiveOnMain(immediately: true) .receive(on: dependencies.receiveQueue, immediatelyIfMain: true)
.collect() .collect()
.sinkUntilComplete( .sinkUntilComplete(
receiveCompletion: { result in receiveCompletion: { result in
@ -61,27 +73,29 @@ public final class BackgroundPoller {
) )
} }
private static func pollForMessages() -> AnyPublisher<Void, Error> { private static func pollForMessages(
using dependencies: OpenGroupManager.OGMDependencies
) -> AnyPublisher<Void, Error> {
let userPublicKey: String = getUserHexEncodedPublicKey() let userPublicKey: String = getUserHexEncodedPublicKey()
return SnodeAPI.getSwarm(for: userPublicKey) return SnodeAPI.getSwarm(for: userPublicKey)
.subscribeOnMain(immediately: true)
.receiveOnMain(immediately: true)
.tryFlatMapWithRandomSnode { snode -> AnyPublisher<[Message], Error> in .tryFlatMapWithRandomSnode { snode -> AnyPublisher<[Message], Error> in
CurrentUserPoller.poll( CurrentUserPoller.poll(
namespaces: CurrentUserPoller.namespaces, namespaces: CurrentUserPoller.namespaces,
from: snode, from: snode,
for: userPublicKey, for: userPublicKey,
on: DispatchQueue.main,
calledFromBackgroundPoller: true, calledFromBackgroundPoller: true,
isBackgroundPollValid: { BackgroundPoller.isValid } isBackgroundPollValid: { BackgroundPoller.isValid },
using: dependencies
) )
} }
.map { _ in () } .map { _ in () }
.eraseToAnyPublisher() .eraseToAnyPublisher()
} }
private static func pollForClosedGroupMessages() -> [AnyPublisher<Void, Error>] { private static func pollForClosedGroupMessages(
using dependencies: OpenGroupManager.OGMDependencies
) -> [AnyPublisher<Void, Error>] {
// Fetch all closed groups (excluding any don't contain the current user as a // Fetch all closed groups (excluding any don't contain the current user as a
// GroupMemeber as the user is no longer a member of those) // GroupMemeber as the user is no longer a member of those)
return Storage.shared return Storage.shared
@ -98,8 +112,6 @@ public final class BackgroundPoller {
.defaulting(to: []) .defaulting(to: [])
.map { groupPublicKey in .map { groupPublicKey in
SnodeAPI.getSwarm(for: groupPublicKey) SnodeAPI.getSwarm(for: groupPublicKey)
.subscribeOnMain(immediately: true)
.receiveOnMain(immediately: true)
.tryFlatMap { swarm -> AnyPublisher<[Message], Error> in .tryFlatMap { swarm -> AnyPublisher<[Message], Error> in
guard let snode: Snode = swarm.randomElement() else { guard let snode: Snode = swarm.randomElement() else {
throw OnionRequestAPIError.insufficientSnodes throw OnionRequestAPIError.insufficientSnodes
@ -109,9 +121,9 @@ public final class BackgroundPoller {
namespaces: ClosedGroupPoller.namespaces, namespaces: ClosedGroupPoller.namespaces,
from: snode, from: snode,
for: groupPublicKey, for: groupPublicKey,
on: DispatchQueue.main,
calledFromBackgroundPoller: true, calledFromBackgroundPoller: true,
isBackgroundPollValid: { BackgroundPoller.isValid } isBackgroundPollValid: { BackgroundPoller.isValid },
using: dependencies
) )
} }
.map { _ in () } .map { _ in () }

View File

@ -198,6 +198,7 @@ public final class WebRTCSession : NSObject, RTCPeerConnectionDelegate {
) )
} }
.flatMap { MessageSender.sendImmediate(preparedSendData: $0) } .flatMap { MessageSender.sendImmediate(preparedSendData: $0) }
.subscribe(on: DispatchQueue.global(qos: .userInitiated))
.sinkUntilComplete( .sinkUntilComplete(
receiveCompletion: { result in receiveCompletion: { result in
switch result { switch result {
@ -263,6 +264,7 @@ public final class WebRTCSession : NSObject, RTCPeerConnectionDelegate {
) )
} }
.flatMap { MessageSender.sendImmediate(preparedSendData: $0) } .flatMap { MessageSender.sendImmediate(preparedSendData: $0) }
.subscribe(on: DispatchQueue.global(qos: .userInitiated))
.sinkUntilComplete( .sinkUntilComplete(
receiveCompletion: { result in receiveCompletion: { result in
switch result { switch result {

View File

@ -994,11 +994,14 @@ extension Attachment {
} }
} }
public static func prepare(_ db: Database, attachments: [SignalAttachment], for interactionId: Int64) throws { public struct PreparedData {
// Prepare any attachments public let attachments: [Attachment]
try attachments.enumerated() }
.forEach { index, signalAttachment in
let maybeAttachment: Attachment? = Attachment( public static func prepare(attachments: [SignalAttachment]) -> PreparedData {
return PreparedData(
attachments: attachments.compactMap { signalAttachment in
Attachment(
variant: (signalAttachment.isVoiceMessage ? variant: (signalAttachment.isVoiceMessage ?
.voiceMessage : .voiceMessage :
.standard .standard
@ -1008,9 +1011,23 @@ extension Attachment {
sourceFilename: signalAttachment.sourceFilename, sourceFilename: signalAttachment.sourceFilename,
caption: signalAttachment.captionText caption: signalAttachment.captionText
) )
}
)
}
public static func process(
_ db: Database,
data: PreparedData?,
for interactionId: Int64?
) throws {
guard
let data: PreparedData = data,
let interactionId: Int64 = interactionId
else { return }
guard let attachment: Attachment = maybeAttachment else { return } try data.attachments
.enumerated()
.forEach { index, attachment in
let interactionAttachment: InteractionAttachment = InteractionAttachment( let interactionAttachment: InteractionAttachment = InteractionAttachment(
albumIndex: index, albumIndex: index,
interactionId: interactionId, interactionId: interactionId,
@ -1042,7 +1059,7 @@ extension Attachment {
let attachmentId: String = self.id let attachmentId: String = self.id
return Storage.shared return Storage.shared
.writePublisherFlatMap { db -> AnyPublisher<(String?, Data?, Data?), Error> in .writePublisher { db -> (OpenGroupAPI.PreparedSendData<FileUploadResponse>?, String?, Data?, Data?) in
// If the attachment is a downloaded attachment, check if it came from // If the attachment is a downloaded attachment, check if it came from
// the server and if so just succeed immediately (no use re-uploading // the server and if so just succeed immediately (no use re-uploading
// an attachment that is already present on the server) - or if we want // an attachment that is already present on the server) - or if we want
@ -1062,9 +1079,7 @@ extension Attachment {
.filter(id: attachmentId) .filter(id: attachmentId)
.updateAll(db, Attachment.Columns.state.set(to: Attachment.State.uploaded)) .updateAll(db, Attachment.Columns.state.set(to: Attachment.State.uploaded))
return Just((Attachment.fileId(for: self.downloadUrl), nil, nil)) return (nil, Attachment.fileId(for: self.downloadUrl), nil, nil)
.setFailureType(to: Error.self)
.eraseToAnyPublisher()
} }
var encryptionKey: NSData = NSData() var encryptionKey: NSData = NSData()
@ -1089,42 +1104,41 @@ extension Attachment {
.filter(id: attachmentId) .filter(id: attachmentId)
.updateAll(db, Attachment.Columns.state.set(to: Attachment.State.uploading)) .updateAll(db, Attachment.Columns.state.set(to: Attachment.State.uploading))
switch destination { // We need database access for OpenGroup uploads so generate prepared data
case .openGroup(let openGroup): let preparedSendData: OpenGroupAPI.PreparedSendData<FileUploadResponse>? = try {
return OpenGroupAPI switch destination {
.uploadFile( case .openGroup(let openGroup):
db, return try OpenGroupAPI
bytes: data.bytes, .preparedUploadFile(
to: openGroup.roomToken, db,
on: openGroup.server bytes: data.bytes,
) to: openGroup.roomToken,
.map { _, response -> (String, Data?, Data?) in on: openGroup.server
(
response.id,
(destination.shouldEncrypt ? encryptionKey as Data : nil),
(destination.shouldEncrypt ? digest as Data : nil)
) )
}
.eraseToAnyPublisher()
case .fileServer: default: return nil
/// **Note:** FileServer uploads don't need database access so }
return Just(( }()
nil,
(destination.shouldEncrypt ? encryptionKey as Data : nil), return (
(destination.shouldEncrypt ? digest as Data : nil) preparedSendData,
)) nil,
.setFailureType(to: Error.self) (destination.shouldEncrypt ? encryptionKey as Data : nil),
.eraseToAnyPublisher() (destination.shouldEncrypt ? digest as Data : nil)
} )
} }
.flatMap { maybeFileId, encryptionKey, digest -> AnyPublisher<(String?, Data?, Data?), Error> in .flatMap { preparedSendData, existingFileId, encryptionKey, digest -> AnyPublisher<(String?, Data?, Data?), Error> in
// No need to upload if the file was already uploaded
if let fileId: String = existingFileId {
return Just((fileId, encryptionKey, digest))
.setFailureType(to: Error.self)
.eraseToAnyPublisher()
}
switch destination { switch destination {
case .openGroup: case .openGroup:
/// **Note:** OpenGroup uploads need database access so this should return OpenGroupAPI.send(data: preparedSendData)
/// have already been uploaded .map { _, response -> (String, Data?, Data?) in (response.id, encryptionKey, digest) }
return Just((maybeFileId, encryptionKey, digest))
.setFailureType(to: Error.self)
.eraseToAnyPublisher() .eraseToAnyPublisher()
case .fileServer: case .fileServer:

View File

@ -5,7 +5,7 @@ import GRDB
import SessionUtilitiesKit import SessionUtilitiesKit
import SessionSnodeKit import SessionSnodeKit
public struct DisappearingMessagesConfiguration: Codable, Identifiable, Equatable, FetchableRecord, PersistableRecord, TableRecord, ColumnExpressible { public struct DisappearingMessagesConfiguration: Codable, Identifiable, Equatable, Hashable, FetchableRecord, PersistableRecord, TableRecord, ColumnExpressible {
public static var databaseTableName: String { "disappearingMessagesConfiguration" } public static var databaseTableName: String { "disappearingMessagesConfiguration" }
internal static let threadForeignKey = ForeignKey([Columns.threadId], to: [SessionThread.Columns.id]) internal static let threadForeignKey = ForeignKey([Columns.threadId], to: [SessionThread.Columns.id])
private static let thread = belongsTo(SessionThread.self, using: threadForeignKey) private static let thread = belongsTo(SessionThread.self, using: threadForeignKey)

View File

@ -319,7 +319,7 @@ public struct Interaction: Codable, Identifiable, Equatable, FetchableRecord, Mu
openGroupServerMessageId: Int64? = nil, openGroupServerMessageId: Int64? = nil,
openGroupWhisperMods: Bool = false, openGroupWhisperMods: Bool = false,
openGroupWhisperTo: String? = nil openGroupWhisperTo: String? = nil
) throws { ) {
self.serverHash = serverHash self.serverHash = serverHash
self.messageUuid = messageUuid self.messageUuid = messageUuid
self.threadId = threadId self.threadId = threadId
@ -821,6 +821,18 @@ public extension Interaction {
} }
} }
return isUserMentioned(
publicKeysToCheck: publicKeysToCheck,
body: body,
quoteAuthorId: quoteAuthorId
)
}
static func isUserMentioned(
publicKeysToCheck: [String],
body: String?,
quoteAuthorId: String? = nil
) -> Bool {
// A user is mentioned if their public key is in the body of a message or one of their messages // A user is mentioned if their public key is in the body of a message or one of their messages
// was quoted // was quoted
return publicKeysToCheck.contains { publicKey in return publicKeysToCheck.contains { publicKey in

View File

@ -130,7 +130,7 @@ public extension LinkPreview {
return (floor(sentTimestampMs / 1000 / LinkPreview.timstampResolution) * LinkPreview.timstampResolution) return (floor(sentTimestampMs / 1000 / LinkPreview.timstampResolution) * LinkPreview.timstampResolution)
} }
static func saveAttachmentIfPossible(_ db: Database, imageData: Data?, mimeType: String) throws -> String? { static func generateAttachmentIfPossible(imageData: Data?, mimeType: String) throws -> Attachment? {
guard let imageData: Data = imageData, !imageData.isEmpty else { return nil } guard let imageData: Data = imageData, !imageData.isEmpty else { return nil }
guard let fileExtension: String = MIMETypeUtil.fileExtension(forMIMEType: mimeType) else { return nil } guard let fileExtension: String = MIMETypeUtil.fileExtension(forMIMEType: mimeType) else { return nil }
@ -141,9 +141,7 @@ public extension LinkPreview {
return nil return nil
} }
return try Attachment(contentType: mimeType, dataSource: dataSource)? return Attachment(contentType: mimeType, dataSource: dataSource)
.inserted(db)
.id
} }
static func isValidLinkUrl(_ urlString: String) -> Bool { static func isValidLinkUrl(_ urlString: String) -> Bool {
@ -355,7 +353,6 @@ public extension LinkPreview {
return session return session
.dataTaskPublisher(for: request) .dataTaskPublisher(for: request)
.subscribe(on: DispatchQueue.global(qos: .userInitiated))
.mapError { _ -> Error in HTTPError.generic } // URLError codes are negative values .mapError { _ -> Error in HTTPError.generic } // URLError codes are negative values
.tryMap { data, response -> (Data, URLResponse) in .tryMap { data, response -> (Data, URLResponse) in
guard let urlResponse: HTTPURLResponse = response as? HTTPURLResponse else { guard let urlResponse: HTTPURLResponse = response as? HTTPURLResponse else {

View File

@ -89,7 +89,6 @@ public enum FileServerAPI {
with: serverPublicKey, with: serverPublicKey,
timeout: timeout timeout: timeout
) )
.subscribe(on: DispatchQueue.global(qos: .userInitiated))
.tryMap { _, response -> Data in .tryMap { _, response -> Data in
guard let response: Data = response else { throw HTTPError.parsingFailed } guard let response: Data = response else { throw HTTPError.parsingFailed }

View File

@ -94,9 +94,20 @@ public enum AttachmentDownloadJob: JobExecutor {
else { throw AttachmentDownloadError.invalidUrl } else { throw AttachmentDownloadError.invalidUrl }
return Storage.shared return Storage.shared
.readPublisher { db in try OpenGroup.fetchOne(db, id: threadId) } .readPublisher { db -> OpenGroupAPI.PreparedSendData<Data>? in
.flatMap { maybeOpenGroup -> AnyPublisher<Data, Error> in try OpenGroup.fetchOne(db, id: threadId)
guard let openGroup: OpenGroup = maybeOpenGroup else { .map { openGroup in
try OpenGroupAPI
.preparedDownloadFile(
db,
fileId: fileId,
from: openGroup.roomToken,
on: openGroup.server
)
}
}
.flatMap { maybePreparedSendData -> AnyPublisher<Data, Error> in
guard let preparedSendData: OpenGroupAPI.PreparedSendData<Data> = maybePreparedSendData else {
return FileServerAPI return FileServerAPI
.download( .download(
fileId, fileId,
@ -105,16 +116,8 @@ public enum AttachmentDownloadJob: JobExecutor {
.eraseToAnyPublisher() .eraseToAnyPublisher()
} }
return Storage.shared return OpenGroupAPI
.readPublisherFlatMap { db in .send(data: preparedSendData)
OpenGroupAPI
.downloadFile(
db,
fileId: fileId,
from: openGroup.roomToken,
on: openGroup.server
)
}
.map { _, data in data } .map { _, data in data }
.eraseToAnyPublisher() .eraseToAnyPublisher()
} }

View File

@ -221,25 +221,29 @@ public extension ConfigurationSyncJob {
return return
} }
// Upsert a config sync job (if there is already an pending one then no need // Upsert a config sync job if needed
// to add another one)
JobRunner.upsert( JobRunner.upsert(
db, db,
job: ConfigurationSyncJob.createOrUpdateIfNeeded(db, publicKey: publicKey) job: ConfigurationSyncJob.createIfNeeded(db, publicKey: publicKey)
) )
} }
@discardableResult static func createOrUpdateIfNeeded(_ db: Database, publicKey: String) -> Job { @discardableResult static func createIfNeeded(_ db: Database, publicKey: String) -> Job? {
// Try to get an existing job (if there is one that's not running) /// The ConfigurationSyncJob will automatically reschedule itself to run again after 3 seconds so if there is an existing
if /// job then there is no need to create another instance
let existingJobs: [Job] = try? Job ///
/// **Note:** Jobs with different `threadId` values can run concurrently
guard
JobRunner
.infoForCurrentlyRunningJobs(of: .configurationSync)
.filter({ _, info in info.threadId == publicKey })
.isEmpty,
(try? Job
.filter(Job.Columns.variant == Job.Variant.configurationSync) .filter(Job.Columns.variant == Job.Variant.configurationSync)
.filter(Job.Columns.threadId == publicKey) .filter(Job.Columns.threadId == publicKey)
.fetchAll(db), .isEmpty(db))
let existingJob: Job = existingJobs.first(where: { !JobRunner.isCurrentlyRunning($0) }) .defaulting(to: false)
{ else { return nil }
return existingJob
}
// Otherwise create a new job // Otherwise create a new job
return Job( return Job(
@ -278,7 +282,7 @@ public extension ConfigurationSyncJob {
Future { resolver in Future { resolver in
ConfigurationSyncJob.run( ConfigurationSyncJob.run(
Job(variant: .configurationSync), Job(variant: .configurationSync),
queue: DispatchQueue.global(qos: .userInitiated), queue: .global(qos: .userInitiated),
success: { _, _ in resolver(Result.success(())) }, success: { _, _ in resolver(Result.success(())) },
failure: { _, error, _ in resolver(Result.failure(error ?? HTTPError.generic)) }, failure: { _, error, _ in resolver(Result.failure(error ?? HTTPError.generic)) },
deferred: { _ in } deferred: { _ in }

View File

@ -55,6 +55,7 @@ public enum GroupLeavingJob: JobExecutor {
) )
} }
.flatMap { MessageSender.sendImmediate(preparedSendData: $0) } .flatMap { MessageSender.sendImmediate(preparedSendData: $0) }
.subscribe(on: queue)
.receive(on: queue) .receive(on: queue)
.sinkUntilComplete( .sinkUntilComplete(
receiveCompletion: { result in receiveCompletion: { result in

View File

@ -189,6 +189,7 @@ public enum MessageSendJob: JobExecutor {
} }
.map { sendData in sendData.with(fileIds: messageFileIds) } .map { sendData in sendData.with(fileIds: messageFileIds) }
.flatMap { MessageSender.sendImmediate(preparedSendData: $0) } .flatMap { MessageSender.sendImmediate(preparedSendData: $0) }
.subscribe(on: queue)
.receive(on: queue) .receive(on: queue)
.sinkUntilComplete( .sinkUntilComplete(
receiveCompletion: { result in receiveCompletion: { result in

View File

@ -49,6 +49,7 @@ public enum SendReadReceiptsJob: JobExecutor {
) )
} }
.flatMap { MessageSender.sendImmediate(preparedSendData: $0) } .flatMap { MessageSender.sendImmediate(preparedSendData: $0) }
.subscribe(on: queue)
.receive(on: queue) .receive(on: queue)
.sinkUntilComplete( .sinkUntilComplete(
receiveCompletion: { result in receiveCompletion: { result in

File diff suppressed because it is too large Load Diff

View File

@ -296,8 +296,6 @@ public final class OpenGroupManager {
) )
} }
} }
.subscribe(on: OpenGroupAPI.workQueue)
.receive(on: OpenGroupAPI.workQueue)
.flatMap { response -> Future<Void, Error> in .flatMap { response -> Future<Void, Error> in
Future<Void, Error> { resolver in Future<Void, Error> { resolver in
dependencies.storage.write { db in dependencies.storage.write { db in
@ -347,7 +345,7 @@ public final class OpenGroupManager {
_ db: Database, _ db: Database,
openGroupId: String, openGroupId: String,
calledFromConfigHandling: Bool, calledFromConfigHandling: Bool,
dependencies: OGMDependencies = OGMDependencies() using dependencies: OGMDependencies = OGMDependencies()
) { ) {
let server: String? = try? OpenGroup let server: String? = try? OpenGroup
.select(.server) .select(.server)
@ -907,26 +905,28 @@ public final class OpenGroupManager {
} }
/// This method specifies if the given capability is supported on a specified Open Group /// This method specifies if the given capability is supported on a specified Open Group
public static func isOpenGroupSupport( public static func doesOpenGroupSupport(
_ capability: Capability.Variant, _ db: Database? = nil,
capability: Capability.Variant,
on server: String?, on server: String?,
using dependencies: OGMDependencies = OGMDependencies() using dependencies: OGMDependencies = OGMDependencies()
) -> Bool { ) -> Bool {
guard let server: String = server else { return false } guard let server: String = server else { return false }
guard let db: Database = db else {
return dependencies.storage
.read { db in doesOpenGroupSupport(db, capability: capability, on: server, using: dependencies) }
.defaulting(to: false)
}
return dependencies.storage let capabilities: [Capability.Variant] = (try? Capability
.read { db in .select(.variant)
let capabilities: [Capability.Variant] = (try? Capability .filter(Capability.Columns.openGroupServer == server)
.select(.variant) .filter(Capability.Columns.isMissing == false)
.filter(Capability.Columns.openGroupServer == server) .asRequest(of: Capability.Variant.self)
.filter(Capability.Columns.isMissing == false) .fetchAll(db))
.asRequest(of: Capability.Variant.self) .defaulting(to: [])
.fetchAll(db))
.defaulting(to: [])
return capabilities.contains(capability) return capabilities.contains(capability)
}
.defaulting(to: false)
} }
/// This method specifies if the given publicKey is a moderator or an admin within a specified Open Group /// This method specifies if the given publicKey is a moderator or an admin within a specified Open Group
@ -1012,7 +1012,10 @@ public final class OpenGroupManager {
} }
@discardableResult public static func getDefaultRoomsIfNeeded( @discardableResult public static func getDefaultRoomsIfNeeded(
using dependencies: OGMDependencies = OGMDependencies() using dependencies: OGMDependencies = OGMDependencies(
subscribeQueue: OpenGroupAPI.workQueue,
receiveQueue: OpenGroupAPI.workQueue
)
) -> AnyPublisher<[OpenGroupAPI.Room], Error> { ) -> AnyPublisher<[OpenGroupAPI.Room], Error> {
// Note: If we already have a 'defaultRoomsPromise' then there is no need to get it again // Note: If we already have a 'defaultRoomsPromise' then there is no need to get it again
if let existingPublisher: AnyPublisher<[OpenGroupAPI.Room], Error> = dependencies.cache.defaultRoomsPublisher { if let existingPublisher: AnyPublisher<[OpenGroupAPI.Room], Error> = dependencies.cache.defaultRoomsPublisher {
@ -1028,8 +1031,8 @@ public final class OpenGroupManager {
using: dependencies using: dependencies
) )
} }
.subscribe(on: OpenGroupAPI.workQueue) .subscribe(on: dependencies.subscribeQueue, immediatelyIfMain: true)
.receive(on: OpenGroupAPI.workQueue) .receive(on: dependencies.receiveQueue, immediatelyIfMain: true)
.retry(8) .retry(8)
.map { response in .map { response in
dependencies.storage.writeAsync { db in dependencies.storage.writeAsync { db in
@ -1077,7 +1080,6 @@ public final class OpenGroupManager {
on: OpenGroupAPI.defaultServer, on: OpenGroupAPI.defaultServer,
using: dependencies using: dependencies
) )
.sinkUntilComplete()
} }
} }
@ -1107,13 +1109,13 @@ public final class OpenGroupManager {
return publisher return publisher
} }
public static func roomImage( @discardableResult public static func roomImage(
_ db: Database, _ db: Database,
fileId: String, fileId: String,
for roomToken: String, for roomToken: String,
on server: String, on server: String,
using dependencies: OGMDependencies = OGMDependencies( using dependencies: OGMDependencies = OGMDependencies(
queue: DispatchQueue.global(qos: .background) subscribeQueue: .global(qos: .background)
) )
) -> AnyPublisher<Data, Error> { ) -> AnyPublisher<Data, Error> {
// Normally the image for a given group is stored with the group thread, so it's only // Normally the image for a given group is stored with the group thread, so it's only
@ -1149,37 +1151,63 @@ public final class OpenGroupManager {
return publisher return publisher
} }
// Trigger the download on a background queue let sendData: OpenGroupAPI.PreparedSendData<Data>
let publisher: AnyPublisher<Data, Error> = OpenGroupAPI
.downloadFile( do {
db, sendData = try OpenGroupAPI
fileId: fileId, .preparedDownloadFile(
from: roomToken, db,
on: server, fileId: fileId,
using: dependencies from: roomToken,
) on: server,
.map { _, imageData in using: dependencies
if server.lowercased() == OpenGroupAPI.defaultServer { )
dependencies.storage.write { db in }
_ = try OpenGroup catch {
.filter(id: threadId) return Fail(error: error)
.updateAll(db, OpenGroup.Columns.imageData.set(to: imageData)) .eraseToAnyPublisher()
} }
dependencies.standardUserDefaults[.lastOpenGroupImageUpdate] = now
// Defer the actual download and run it on a separate thread to avoid blocking the calling thread
let publisher: AnyPublisher<Data, Error> = Deferred {
Future { resolver in
dependencies.subscribeQueue.async {
// Hold on to the publisher until it has completed at least once
OpenGroupAPI
.send(
data: sendData,
using: dependencies
)
.sinkUntilComplete(
receiveCompletion: { result in
switch result {
case .finished: break
case .failure(let error): resolver(Result.failure(error))
}
},
receiveValue: { _, imageData in
if server.lowercased() == OpenGroupAPI.defaultServer {
dependencies.storage.write { db in
_ = try OpenGroup
.filter(id: threadId)
.updateAll(db, OpenGroup.Columns.imageData.set(to: imageData))
}
dependencies.standardUserDefaults[.lastOpenGroupImageUpdate] = now
}
resolver(Result.success(imageData))
}
)
} }
return imageData
} }
.shareReplay(1) }
.eraseToAnyPublisher() .shareReplay(1)
.eraseToAnyPublisher()
dependencies.mutableCache.mutate { cache in dependencies.mutableCache.mutate { cache in
cache.groupImagePublishers[threadId] = publisher cache.groupImagePublishers[threadId] = publisher
} }
// Hold on to the publisher until it has completed at least once
publisher.sinkUntilComplete()
return publisher return publisher
} }
} }
@ -1198,7 +1226,8 @@ extension OpenGroupManager {
public var cache: OGMCacheType { return mutableCache.wrappedValue } public var cache: OGMCacheType { return mutableCache.wrappedValue }
public init( public init(
queue: DispatchQueue? = nil, subscribeQueue: DispatchQueue? = nil,
receiveQueue: DispatchQueue? = nil,
cache: Atomic<OGMCacheType>? = nil, cache: Atomic<OGMCacheType>? = nil,
onionApi: OnionRequestAPIType.Type? = nil, onionApi: OnionRequestAPIType.Type? = nil,
generalCache: Atomic<GeneralCacheType>? = nil, generalCache: Atomic<GeneralCacheType>? = nil,
@ -1218,7 +1247,8 @@ extension OpenGroupManager {
_mutableCache = Atomic(cache) _mutableCache = Atomic(cache)
super.init( super.init(
queue: queue, subscribeQueue: subscribeQueue,
receiveQueue: receiveQueue,
onionApi: onionApi, onionApi: onionApi,
generalCache: generalCache, generalCache: generalCache,
storage: storage, storage: storage,

View File

@ -7,6 +7,7 @@ public enum OpenGroupAPIError: LocalizedError {
case signingFailed case signingFailed
case noPublicKey case noPublicKey
case invalidEmoji case invalidEmoji
case invalidPreparedData
public var errorDescription: String? { public var errorDescription: String? {
switch self { switch self {
@ -14,6 +15,7 @@ public enum OpenGroupAPIError: LocalizedError {
case .signingFailed: return "Couldn't sign message." case .signingFailed: return "Couldn't sign message."
case .noPublicKey: return "Couldn't find server public key." case .noPublicKey: return "Couldn't find server public key."
case .invalidEmoji: return "The emoji is invalid." case .invalidEmoji: return "The emoji is invalid."
case .invalidPreparedData: return "Invalid PreparedSendData provided."
} }
} }
} }

View File

@ -0,0 +1,125 @@
// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved.
import Foundation
import Combine
import SessionUtilitiesKit
public extension OpenGroupAPI {
struct PreparedSendData<R> {
internal let request: URLRequest
internal let endpoint: Endpoint
internal let server: String
internal let publicKey: String
internal let originalType: Decodable.Type
internal let responseType: R.Type
internal let timeout: TimeInterval
internal let responseConverter: ((ResponseInfoType, Any) throws -> R)
internal init(
request: URLRequest,
endpoint: Endpoint,
server: String,
publicKey: String,
responseType: R.Type,
timeout: TimeInterval
) where R: Decodable {
self.request = request
self.endpoint = endpoint
self.server = server
self.publicKey = publicKey
self.originalType = responseType
self.responseType = responseType
self.timeout = timeout
self.responseConverter = { _, response in
guard let validResponse: R = response as? R else { throw HTTPError.invalidResponse }
return validResponse
}
}
private init<U: Decodable>(
request: URLRequest,
endpoint: Endpoint,
server: String,
publicKey: String,
originalType: U.Type,
responseType: R.Type,
timeout: TimeInterval,
responseConverter: @escaping (ResponseInfoType, Any) throws -> R
) {
self.request = request
self.endpoint = endpoint
self.server = server
self.publicKey = publicKey
self.originalType = originalType
self.responseType = responseType
self.timeout = timeout
self.responseConverter = responseConverter
}
}
}
public extension OpenGroupAPI.PreparedSendData {
func map<O>(transform: @escaping (ResponseInfoType, R) throws -> O) -> OpenGroupAPI.PreparedSendData<O> {
return OpenGroupAPI.PreparedSendData(
request: request,
endpoint: endpoint,
server: server,
publicKey: publicKey,
originalType: originalType,
responseType: O.self,
timeout: timeout,
responseConverter: { info, response in
let validResponse: R = try responseConverter(info, response)
return try transform(info, validResponse)
}
)
}
}
// MARK: - Convenience
public extension Publisher where Output == (ResponseInfoType, Data?), Failure == Error {
func decoded<R>(
with preparedData: OpenGroupAPI.PreparedSendData<R>,
using dependencies: Dependencies = Dependencies()
) -> AnyPublisher<(ResponseInfoType, R), Error> {
self
.tryMap { responseInfo, maybeData -> (ResponseInfoType, R) in
// Depending on the 'originalType' we need to process the response differently
let targetData: Any = try {
switch preparedData.originalType {
case is NoResponse.Type: return NoResponse()
case is Optional<Data>.Type: return maybeData as Any
case is Data.Type: return try maybeData ?? { throw HTTPError.parsingFailed }()
case is _OptionalProtocol.Type:
guard let data: Data = maybeData else { return maybeData as Any }
return try preparedData.originalType.decoded(from: data, using: dependencies)
default:
guard let data: Data = maybeData else { throw HTTPError.parsingFailed }
return try preparedData.originalType.decoded(from: data, using: dependencies)
}
}()
// Generate and return the converted data
let convertedData: R = try preparedData.responseConverter(responseInfo, targetData)
return (responseInfo, convertedData)
}
.eraseToAnyPublisher()
}
}
// MARK: - _OptionalProtocol
/// This protocol should only be used within this file and is used to distinguish between `Any.Type` and `Optional<Any>.Type` as
/// it seems that `is Optional<Any>.Type` doesn't work nicely but this protocol works nicely as long as the case is under any explicit
/// `Optional<T>` handling that we need
private protocol _OptionalProtocol {}
extension Optional: _OptionalProtocol {}

View File

@ -1,4 +1,5 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. // Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import Foundation import Foundation
import GRDB import GRDB
import Sodium import Sodium
@ -57,7 +58,8 @@ public class SMKDependencies: SSKDependencies {
// MARK: - Initialization // MARK: - Initialization
public init( public init(
queue: DispatchQueue? = nil, subscribeQueue: DispatchQueue? = nil,
receiveQueue: DispatchQueue? = nil,
onionApi: OnionRequestAPIType.Type? = nil, onionApi: OnionRequestAPIType.Type? = nil,
generalCache: Atomic<GeneralCacheType>? = nil, generalCache: Atomic<GeneralCacheType>? = nil,
storage: Storage? = nil, storage: Storage? = nil,
@ -83,7 +85,8 @@ public class SMKDependencies: SSKDependencies {
_nonceGenerator24 = Atomic(nonceGenerator24) _nonceGenerator24 = Atomic(nonceGenerator24)
super.init( super.init(
queue: queue, subscribeQueue: subscribeQueue,
receiveQueue: receiveQueue,
onionApi: onionApi, onionApi: onionApi,
generalCache: generalCache, generalCache: generalCache,
storage: storage, storage: storage,

View File

@ -64,12 +64,14 @@ extension MessageReceiver {
let thread: SessionThread = try SessionThread let thread: SessionThread = try SessionThread
.fetchOrCreate(db, id: sender, variant: .contact, shouldBeVisible: nil) .fetchOrCreate(db, id: sender, variant: .contact, shouldBeVisible: nil)
Environment.shared?.notificationsManager.wrappedValue? if !interaction.wasRead {
.notifyUser( Environment.shared?.notificationsManager.wrappedValue?
db, .notifyUser(
forIncomingCall: interaction, db,
in: thread forIncomingCall: interaction,
) in: thread
)
}
} }
return return
} }
@ -79,12 +81,14 @@ extension MessageReceiver {
let thread: SessionThread = try SessionThread let thread: SessionThread = try SessionThread
.fetchOrCreate(db, id: sender, variant: .contact, shouldBeVisible: nil) .fetchOrCreate(db, id: sender, variant: .contact, shouldBeVisible: nil)
Environment.shared?.notificationsManager.wrappedValue? if !interaction.wasRead {
.notifyUser( Environment.shared?.notificationsManager.wrappedValue?
db, .notifyUser(
forIncomingCall: interaction, db,
in: thread forIncomingCall: interaction,
) in: thread
)
}
// Trigger the missed call UI if needed // Trigger the missed call UI if needed
NotificationCenter.default.post( NotificationCenter.default.post(
@ -196,6 +200,10 @@ extension MessageReceiver {
SNLog("[Calls] Sending end call message because there is an ongoing call.") SNLog("[Calls] Sending end call message because there is an ongoing call.")
let messageSentTimestamp: Int64 = (
message.sentTimestamp.map { Int64($0) } ??
SnodeAPI.currentOffsetTimestampMs()
)
_ = try Interaction( _ = try Interaction(
serverHash: message.serverHash, serverHash: message.serverHash,
messageUuid: message.uuid, messageUuid: message.uuid,
@ -203,9 +211,13 @@ extension MessageReceiver {
authorId: caller, authorId: caller,
variant: .infoCall, variant: .infoCall,
body: String(data: messageInfoData, encoding: .utf8), body: String(data: messageInfoData, encoding: .utf8),
timestampMs: ( timestampMs: messageSentTimestamp,
message.sentTimestamp.map { Int64($0) } ?? wasRead: SessionUtil.timestampAlreadyRead(
SnodeAPI.currentOffsetTimestampMs() threadId: thread.id,
threadVariant: thread.variant,
timestampMs: (messageSentTimestamp * 1000),
userPublicKey: getUserHexEncodedPublicKey(db),
openGroup: nil
) )
) )
.inserted(db) .inserted(db)
@ -227,6 +239,7 @@ extension MessageReceiver {
interactionId: nil // Explicitly nil as it's a separate message from above interactionId: nil // Explicitly nil as it's a separate message from above
) )
) )
.subscribe(on: DispatchQueue.global(qos: .userInitiated))
.sinkUntilComplete() .sinkUntilComplete()
} }
@ -246,9 +259,10 @@ extension MessageReceiver {
!thread.isMessageRequest(db) !thread.isMessageRequest(db)
else { return nil } else { return nil }
let currentUserPublicKey: String = getUserHexEncodedPublicKey(db)
let messageInfo: CallMessage.MessageInfo = CallMessage.MessageInfo( let messageInfo: CallMessage.MessageInfo = CallMessage.MessageInfo(
state: state.defaulting( state: state.defaulting(
to: (sender == getUserHexEncodedPublicKey(db) ? to: (sender == currentUserPublicKey ?
.outgoing : .outgoing :
.incoming .incoming
) )
@ -268,7 +282,14 @@ extension MessageReceiver {
authorId: sender, authorId: sender,
variant: .infoCall, variant: .infoCall,
body: String(data: messageInfoData, encoding: .utf8), body: String(data: messageInfoData, encoding: .utf8),
timestampMs: timestampMs timestampMs: timestampMs,
wasRead: SessionUtil.timestampAlreadyRead(
threadId: thread.id,
threadVariant: thread.variant,
timestampMs: (timestampMs * 1000),
userPublicKey: currentUserPublicKey,
openGroup: nil
)
).inserted(db) ).inserted(db)
} }
} }

View File

@ -3,6 +3,7 @@
import Foundation import Foundation
import GRDB import GRDB
import SessionSnodeKit import SessionSnodeKit
import SessionUtilitiesKit
extension MessageReceiver { extension MessageReceiver {
internal static func handleDataExtractionNotification( internal static func handleDataExtractionNotification(
@ -17,6 +18,10 @@ extension MessageReceiver {
let messageKind: DataExtractionNotification.Kind = message.kind let messageKind: DataExtractionNotification.Kind = message.kind
else { throw MessageReceiverError.invalidMessage } else { throw MessageReceiverError.invalidMessage }
let timestampMs: Int64 = (
message.sentTimestamp.map { Int64($0) } ??
SnodeAPI.currentOffsetTimestampMs()
)
_ = try Interaction( _ = try Interaction(
serverHash: message.serverHash, serverHash: message.serverHash,
threadId: threadId, threadId: threadId,
@ -27,9 +32,13 @@ extension MessageReceiver {
case .mediaSaved: return .infoMediaSavedNotification case .mediaSaved: return .infoMediaSavedNotification
} }
}(), }(),
timestampMs: ( timestampMs: timestampMs,
message.sentTimestamp.map { Int64($0) } ?? wasRead: SessionUtil.timestampAlreadyRead(
SnodeAPI.currentOffsetTimestampMs() threadId: threadId,
threadVariant: threadVariant,
timestampMs: (timestampMs * 1000),
userPublicKey: getUserHexEncodedPublicKey(db),
openGroup: nil
) )
).inserted(db) ).inserted(db)
} }

View File

@ -71,18 +71,26 @@ extension MessageReceiver {
} }
// Add an info message for the user // Add an info message for the user
let currentUserPublicKey: String = getUserHexEncodedPublicKey(db)
_ = try Interaction( _ = try Interaction(
serverHash: nil, // Intentionally null so sync messages are seen as duplicates serverHash: nil, // Intentionally null so sync messages are seen as duplicates
threadId: threadId, threadId: threadId,
authorId: sender, authorId: sender,
variant: .infoDisappearingMessagesUpdate, variant: .infoDisappearingMessagesUpdate,
body: config.messageInfoString( body: config.messageInfoString(
with: (sender != getUserHexEncodedPublicKey(db) ? with: (sender != currentUserPublicKey ?
Profile.displayName(db, id: sender) : Profile.displayName(db, id: sender) :
nil nil
) )
), ),
timestampMs: timestampMs timestampMs: timestampMs,
wasRead: SessionUtil.timestampAlreadyRead(
threadId: threadId,
threadVariant: threadVariant,
timestampMs: (timestampMs * 1000),
userPublicKey: currentUserPublicKey,
openGroup: nil
)
).inserted(db) ).inserted(db)
// Only save the updated config if we can perform the change // Only save the updated config if we can perform the change

View File

@ -48,6 +48,7 @@ extension MessageReceiver {
publicKey: author, publicKey: author,
serverHashes: [serverHash] serverHashes: [serverHash]
) )
.subscribe(on: DispatchQueue.global(qos: .background))
.sinkUntilComplete() .sinkUntilComplete()
} }

View File

@ -117,7 +117,8 @@ extension MessageReceiver {
message: message, message: message,
associatedWithProto: proto, associatedWithProto: proto,
sender: sender, sender: sender,
messageSentTimestamp: messageSentTimestamp messageSentTimestamp: messageSentTimestamp,
openGroup: maybeOpenGroup
) { ) {
return interactionId return interactionId
} }
@ -323,7 +324,7 @@ extension MessageReceiver {
} }
// Notify the user if needed // Notify the user if needed
guard variant == .standardIncoming else { return interactionId } guard variant == .standardIncoming && !interaction.wasRead else { return interactionId }
// Use the same identifier for notifications when in backgroud polling to prevent spam // Use the same identifier for notifications when in backgroud polling to prevent spam
Environment.shared?.notificationsManager.wrappedValue? Environment.shared?.notificationsManager.wrappedValue?
@ -342,7 +343,8 @@ extension MessageReceiver {
message: VisibleMessage, message: VisibleMessage,
associatedWithProto proto: SNProtoContent, associatedWithProto proto: SNProtoContent,
sender: String, sender: String,
messageSentTimestamp: TimeInterval messageSentTimestamp: TimeInterval,
openGroup: OpenGroup?
) throws -> Int64? { ) throws -> Int64? {
guard guard
let reaction: VisibleMessage.VMReaction = message.reaction, let reaction: VisibleMessage.VMReaction = message.reaction,
@ -370,17 +372,28 @@ extension MessageReceiver {
switch reaction.kind { switch reaction.kind {
case .react: case .react:
let timestampMs: Int64 = Int64(messageSentTimestamp * 1000)
let currentUserPublicKey: String = getUserHexEncodedPublicKey(db)
let reaction: Reaction = try Reaction( let reaction: Reaction = try Reaction(
interactionId: interactionId, interactionId: interactionId,
serverHash: message.serverHash, serverHash: message.serverHash,
timestampMs: Int64(messageSentTimestamp * 1000), timestampMs: timestampMs,
authorId: sender, authorId: sender,
emoji: reaction.emoji, emoji: reaction.emoji,
count: 1, count: 1,
sortId: sortId sortId: sortId
).inserted(db) ).inserted(db)
let timestampAlreadyRead: Bool = SessionUtil.timestampAlreadyRead(
threadId: thread.id,
threadVariant: thread.variant,
timestampMs: timestampMs,
userPublicKey: currentUserPublicKey,
openGroup: openGroup
)
if sender != getUserHexEncodedPublicKey(db) { // Don't notify if the reaction was added before the lastest read timestamp for
// the conversation
if sender != currentUserPublicKey && !timestampAlreadyRead {
Environment.shared?.notificationsManager.wrappedValue? Environment.shared?.notificationsManager.wrappedValue?
.notifyUser( .notifyUser(
db, db,

View File

@ -108,10 +108,7 @@ extension MessageSender {
) )
} }
public static func performUploadsIfNeeded( public static func performUploadsIfNeeded(preparedSendData: PreparedSendData) -> AnyPublisher<PreparedSendData, Error> {
queue: DispatchQueue,
preparedSendData: PreparedSendData
) -> AnyPublisher<PreparedSendData, Error> {
// We need an interactionId in order for a message to have uploads // We need an interactionId in order for a message to have uploads
guard let interactionId: Int64 = preparedSendData.interactionId else { guard let interactionId: Int64 = preparedSendData.interactionId else {
return Just(preparedSendData) return Just(preparedSendData)

View File

@ -643,7 +643,6 @@ public final class MessageSender {
snodeMessage, snodeMessage,
in: namespace in: namespace
) )
.subscribe(on: DispatchQueue.global(qos: .default))
.flatMap { response -> AnyPublisher<Void, Error> in .flatMap { response -> AnyPublisher<Void, Error> in
let updatedMessage: Message = message let updatedMessage: Message = message
updatedMessage.serverHash = response.1.hash updatedMessage.serverHash = response.1.hash
@ -703,7 +702,7 @@ public final class MessageSender {
Future<Void, Error> { resolver in Future<Void, Error> { resolver in
NotifyPushServerJob.run( NotifyPushServerJob.run(
job, job,
queue: DispatchQueue.global(qos: .default), queue: .global(qos: .default),
success: { _, _ in resolver(Result.success(())) }, success: { _, _ in resolver(Result.success(())) },
failure: { _, _, _ in failure: { _, _, _ in
// Always fulfill because the notify PN server job isn't critical. // Always fulfill because the notify PN server job isn't critical.
@ -760,9 +759,9 @@ public final class MessageSender {
// Send the result // Send the result
return dependencies.storage return dependencies.storage
.readPublisherFlatMap { db in .readPublisher { db in
OpenGroupAPI try OpenGroupAPI
.send( .preparedSend(
db, db,
plaintext: plaintext, plaintext: plaintext,
to: roomToken, to: roomToken,
@ -773,7 +772,7 @@ public final class MessageSender {
using: dependencies using: dependencies
) )
} }
.subscribe(on: DispatchQueue.global(qos: .default)) .flatMap { OpenGroupAPI.send(data: $0, using: dependencies) }
.flatMap { (responseInfo, responseData) -> AnyPublisher<Void, Error> in .flatMap { (responseInfo, responseData) -> AnyPublisher<Void, Error> in
let serverTimestampMs: UInt64? = responseData.posted.map { UInt64(floor($0 * 1000)) } let serverTimestampMs: UInt64? = responseData.posted.map { UInt64(floor($0 * 1000)) }
let updatedMessage: Message = message let updatedMessage: Message = message
@ -828,9 +827,9 @@ public final class MessageSender {
// Send the result // Send the result
return dependencies.storage return dependencies.storage
.readPublisherFlatMap { db in .readPublisher { db in
return OpenGroupAPI try OpenGroupAPI
.send( .preparedSend(
db, db,
ciphertext: ciphertext, ciphertext: ciphertext,
toInboxFor: recipientBlindedPublicKey, toInboxFor: recipientBlindedPublicKey,
@ -838,7 +837,7 @@ public final class MessageSender {
using: dependencies using: dependencies
) )
} }
.subscribe(on: DispatchQueue.global(qos: .default)) .flatMap { OpenGroupAPI.send(data: $0, using: dependencies) }
.flatMap { (responseInfo, responseData) -> AnyPublisher<Void, Error> in .flatMap { (responseInfo, responseData) -> AnyPublisher<Void, Error> in
let updatedMessage: Message = message let updatedMessage: Message = message
updatedMessage.openGroupServerMessageId = UInt64(responseData.id) updatedMessage.openGroupServerMessageId = UInt64(responseData.id)

View File

@ -92,12 +92,16 @@ public final class ClosedGroupPoller: Poller {
.eraseToAnyPublisher() .eraseToAnyPublisher()
} }
override func handlePollError(_ error: Error, for publicKey: String) { override func handlePollError(
_ error: Error,
for publicKey: String,
using dependencies: SMKDependencies = SMKDependencies()
) {
SNLog("Polling failed for closed group with public key: \(publicKey) due to error: \(error).") SNLog("Polling failed for closed group with public key: \(publicKey) due to error: \(error).")
// Try to restart the poller from scratch // Try to restart the poller from scratch
Threading.pollerQueue.async { [weak self] in Threading.pollerQueue.async { [weak self] in
self?.setUpPolling(for: publicKey) self?.setUpPolling(for: publicKey, using: dependencies)
} }
} }
} }

View File

@ -100,7 +100,11 @@ public final class CurrentUserPoller: Poller {
.eraseToAnyPublisher() .eraseToAnyPublisher()
} }
override func handlePollError(_ error: Error, for publicKey: String) { override func handlePollError(
_ error: Error,
for publicKey: String,
using dependencies: SMKDependencies = SMKDependencies()
) {
if UserDefaults.sharedLokiProject?[.isMainAppActive] != true { if UserDefaults.sharedLokiProject?[.isMainAppActive] != true {
// Do nothing when an error gets throws right after returning from the background (happens frequently) // Do nothing when an error gets throws right after returning from the background (happens frequently)
} }
@ -115,7 +119,7 @@ public final class CurrentUserPoller: Poller {
// Try to restart the poller from scratch // Try to restart the poller from scratch
Threading.pollerQueue.async { [weak self] in Threading.pollerQueue.async { [weak self] in
self?.setUpPolling(for: publicKey) self?.setUpPolling(for: publicKey, using: dependencies)
} }
} }
} }

View File

@ -18,8 +18,16 @@ extension OpenGroupAPI {
// MARK: - Settings // MARK: - Settings
private static let minPollInterval: TimeInterval = 3 private static let minPollInterval: TimeInterval = 3
private static let maxPollInterval: Double = (60 * 60) private static let maxPollInterval: TimeInterval = (60 * 60)
internal static let maxInactivityPeriod: Double = (14 * 24 * 60 * 60) internal static let maxInactivityPeriod: TimeInterval = (14 * 24 * 60 * 60)
/// If there are hidden rooms that we poll and they fail too many times we want to prune them (as it likely means they no longer
/// exist, and since they are already hidden it's unlikely that the user will notice that we stopped polling for them)
internal static let maxHiddenRoomFailureCount: Int64 = 10
/// When doing a background poll we want to only fetch from rooms which are unlikely to timeout, in order to do this we exclude
/// any rooms which have failed more than this threashold
public static let maxRoomFailureCountForBackgroundPoll: Int64 = 15
// MARK: - Lifecycle // MARK: - Lifecycle
@ -41,7 +49,12 @@ extension OpenGroupAPI {
// MARK: - Polling // MARK: - Polling
private func pollRecursively(using dependencies: OpenGroupManager.OGMDependencies = OpenGroupManager.OGMDependencies()) { private func pollRecursively(
using dependencies: OpenGroupManager.OGMDependencies = OpenGroupManager.OGMDependencies(
subscribeQueue: Threading.pollerQueue,
receiveQueue: OpenGroupAPI.workQueue
)
) {
guard hasStarted else { return } guard hasStarted else { return }
let minPollFailureCount: TimeInterval = Storage.shared let minPollFailureCount: TimeInterval = Storage.shared
@ -59,6 +72,8 @@ extension OpenGroupAPI {
// Wait until the last poll completes before polling again ensuring we don't poll any faster than // Wait until the last poll completes before polling again ensuring we don't poll any faster than
// the 'nextPollInterval' value // the 'nextPollInterval' value
poll(using: dependencies) poll(using: dependencies)
.subscribe(on: dependencies.subscribeQueue, immediatelyIfMain: true)
.receive(on: dependencies.receiveQueue, immediatelyIfMain: true)
.sinkUntilComplete( .sinkUntilComplete(
receiveCompletion: { [weak self] _ in receiveCompletion: { [weak self] _ in
let currentTime: TimeInterval = Date().timeIntervalSince1970 let currentTime: TimeInterval = Date().timeIntervalSince1970
@ -129,8 +144,6 @@ extension OpenGroupAPI {
.map { response in (failureCount, response) } .map { response in (failureCount, response) }
.eraseToAnyPublisher() .eraseToAnyPublisher()
} }
.subscribe(on: Threading.pollerQueue)
.receive(on: Threading.pollerQueue)
.handleEvents( .handleEvents(
receiveOutput: { [weak self] failureCount, response in receiveOutput: { [weak self] failureCount, response in
guard !calledFromBackgroundPoller || isBackgroundPollerValid() else { guard !calledFromBackgroundPoller || isBackgroundPollerValid() else {
@ -179,7 +192,8 @@ extension OpenGroupAPI {
calledFromBackgroundPoller: calledFromBackgroundPoller, calledFromBackgroundPoller: calledFromBackgroundPoller,
isBackgroundPollerValid: isBackgroundPollerValid, isBackgroundPollerValid: isBackgroundPollerValid,
isPostCapabilitiesRetry: isPostCapabilitiesRetry, isPostCapabilitiesRetry: isPostCapabilitiesRetry,
error: error error: error,
using: dependencies
) )
.handleEvents( .handleEvents(
receiveOutput: { [weak self] didHandleError in receiveOutput: { [weak self] didHandleError in
@ -232,7 +246,7 @@ extension OpenGroupAPI {
/// If the polling has failed 10+ times then try to prune any invalid rooms that /// If the polling has failed 10+ times then try to prune any invalid rooms that
/// aren't visible (they would have been added via config messages and will /// aren't visible (they would have been added via config messages and will
/// likely always fail but the user has no way to delete them) /// likely always fail but the user has no way to delete them)
guard pollFailureCount > 10 else { return } guard pollFailureCount > Poller.maxHiddenRoomFailureCount else { return }
prunedIds = roomsAreVisible prunedIds = roomsAreVisible
.filter { !$0.shouldBeVisible } .filter { !$0.shouldBeVisible }
@ -247,19 +261,20 @@ extension OpenGroupAPI {
/// not be in an invalid state on other devices - one of the other devices /// not be in an invalid state on other devices - one of the other devices
/// will eventually trigger a new config update which will re-add this room /// will eventually trigger a new config update which will re-add this room
/// and hopefully at that time it'll work again /// and hopefully at that time it'll work again
calledFromConfigHandling: true calledFromConfigHandling: true,
using: dependencies
) )
} }
} }
SNLog("Open group polling failed due to error: \(error). Setting failure count to \(pollFailureCount).") SNLog("Open group polling to \(server) failed due to error: \(error). Setting failure count to \(pollFailureCount).")
// Add a note to the logs that this happened // Add a note to the logs that this happened
if !prunedIds.isEmpty { if !prunedIds.isEmpty {
let rooms: String = prunedIds let rooms: String = prunedIds
.compactMap { $0.components(separatedBy: server).last } .compactMap { $0.components(separatedBy: server).last }
.joined(separator: ", ") .joined(separator: ", ")
SNLog("Hidden open group failure count surpassed 10, removed hidden rooms \(rooms).") SNLog("Hidden open group failure count surpassed \(Poller.maxHiddenRoomFailureCount), removed hidden rooms \(rooms).")
} }
} }
@ -299,16 +314,15 @@ extension OpenGroupAPI {
} }
return dependencies.storage return dependencies.storage
.readPublisherFlatMap { db in .readPublisher { db in
OpenGroupAPI.capabilities( try OpenGroupAPI.preparedCapabilities(
db, db,
server: server, server: server,
forceBlinded: true, forceBlinded: true,
using: dependencies using: dependencies
) )
} }
.subscribe(on: OpenGroupAPI.workQueue) .flatMap { OpenGroupAPI.send(data: $0, using: dependencies) }
.receive(on: OpenGroupAPI.workQueue)
.flatMap { [weak self] _, responseBody -> AnyPublisher<Void, Error> in .flatMap { [weak self] _, responseBody -> AnyPublisher<Void, Error> in
guard let strongSelf = self, isBackgroundPollerValid() else { guard let strongSelf = self, isBackgroundPollerValid() else {
return Just(()) return Just(())

View File

@ -59,7 +59,7 @@ public class Poller {
preconditionFailure("abstract class - override in subclass") preconditionFailure("abstract class - override in subclass")
} }
internal func handlePollError(_ error: Error, for publicKey: String) { internal func handlePollError(_ error: Error, for publicKey: String, using dependencies: SMKDependencies) {
preconditionFailure("abstract class - override in subclass") preconditionFailure("abstract class - override in subclass")
} }
@ -81,37 +81,46 @@ public class Poller {
/// We want to initially trigger a poll against the target service node and then run the recursive polling, /// We want to initially trigger a poll against the target service node and then run the recursive polling,
/// if an error is thrown during the poll then this should automatically restart the polling /// if an error is thrown during the poll then this should automatically restart the polling
internal func setUpPolling(for publicKey: String) { internal func setUpPolling(
for publicKey: String,
using dependencies: SMKDependencies = SMKDependencies(
subscribeQueue: Threading.pollerQueue,
receiveQueue: Threading.pollerQueue
)
) {
guard isPolling.wrappedValue[publicKey] == true else { return } guard isPolling.wrappedValue[publicKey] == true else { return }
let namespaces: [SnodeAPI.Namespace] = self.namespaces let namespaces: [SnodeAPI.Namespace] = self.namespaces
getSnodeForPolling(for: publicKey) getSnodeForPolling(for: publicKey)
.subscribe(on: Threading.pollerQueue) .subscribe(on: dependencies.subscribeQueue, immediatelyIfMain: true)
.flatMap { snode -> AnyPublisher<[Message], Error> in .flatMap { snode -> AnyPublisher<[Message], Error> in
Poller.poll( Poller.poll(
namespaces: namespaces, namespaces: namespaces,
from: snode, from: snode,
for: publicKey, for: publicKey,
on: Threading.pollerQueue, poller: self,
poller: self using: dependencies
) )
} }
.receive(on: Threading.pollerQueue) .receive(on: dependencies.receiveQueue, immediatelyIfMain: true)
.sinkUntilComplete( .sinkUntilComplete(
receiveCompletion: { [weak self] result in receiveCompletion: { [weak self] result in
switch result { switch result {
case .finished: self?.pollRecursively(for: publicKey) case .finished: self?.pollRecursively(for: publicKey, using: dependencies)
case .failure(let error): case .failure(let error):
guard self?.isPolling.wrappedValue[publicKey] == true else { return } guard self?.isPolling.wrappedValue[publicKey] == true else { return }
self?.handlePollError(error, for: publicKey) self?.handlePollError(error, for: publicKey, using: dependencies)
} }
} }
) )
} }
private func pollRecursively(for publicKey: String) { private func pollRecursively(
for publicKey: String,
using dependencies: SMKDependencies = SMKDependencies()
) {
guard isPolling.wrappedValue[publicKey] == true else { return } guard isPolling.wrappedValue[publicKey] == true else { return }
let namespaces: [SnodeAPI.Namespace] = self.namespaces let namespaces: [SnodeAPI.Namespace] = self.namespaces
@ -125,21 +134,21 @@ public class Poller {
timer.invalidate() timer.invalidate()
self?.getSnodeForPolling(for: publicKey) self?.getSnodeForPolling(for: publicKey)
.subscribe(on: Threading.pollerQueue) .subscribe(on: dependencies.subscribeQueue, immediatelyIfMain: true)
.flatMap { snode -> AnyPublisher<[Message], Error> in .flatMap { snode -> AnyPublisher<[Message], Error> in
Poller.poll( Poller.poll(
namespaces: namespaces, namespaces: namespaces,
from: snode, from: snode,
for: publicKey, for: publicKey,
on: Threading.pollerQueue, poller: self,
poller: self using: dependencies
) )
} }
.receive(on: Threading.pollerQueue) .receive(on: dependencies.receiveQueue, immediatelyIfMain: true)
.sinkUntilComplete( .sinkUntilComplete(
receiveCompletion: { result in receiveCompletion: { result in
switch result { switch result {
case .failure(let error): self?.handlePollError(error, for: publicKey) case .failure(let error): self?.handlePollError(error, for: publicKey, using: dependencies)
case .finished: case .finished:
let maxNodePollCount: UInt = (self?.maxNodePollCount ?? 0) let maxNodePollCount: UInt = (self?.maxNodePollCount ?? 0)
@ -161,7 +170,7 @@ public class Poller {
timer.invalidate() timer.invalidate()
self?.pollCount.mutate { $0[publicKey] = 0 } self?.pollCount.mutate { $0[publicKey] = 0 }
self?.setUpPolling(for: publicKey) self?.setUpPolling(for: publicKey, using: dependencies)
} }
} }
return return
@ -169,7 +178,7 @@ public class Poller {
} }
// Otherwise just loop // Otherwise just loop
self?.pollRecursively(for: publicKey) self?.pollRecursively(for: publicKey, using: dependencies)
} }
} }
) )
@ -186,10 +195,12 @@ public class Poller {
namespaces: [SnodeAPI.Namespace], namespaces: [SnodeAPI.Namespace],
from snode: Snode, from snode: Snode,
for publicKey: String, for publicKey: String,
on queue: DispatchQueue,
calledFromBackgroundPoller: Bool = false, calledFromBackgroundPoller: Bool = false,
isBackgroundPollValid: @escaping (() -> Bool) = { true }, isBackgroundPollValid: @escaping (() -> Bool) = { true },
poller: Poller? = nil poller: Poller? = nil,
using dependencies: SMKDependencies = SMKDependencies(
receiveQueue: Threading.pollerQueue
)
) -> AnyPublisher<[Message], Error> { ) -> AnyPublisher<[Message], Error> {
// If the polling has been cancelled then don't continue // If the polling has been cancelled then don't continue
guard guard
@ -213,7 +224,8 @@ public class Poller {
namespaces: namespaces, namespaces: namespaces,
refreshingConfigHashes: configHashes, refreshingConfigHashes: configHashes,
from: snode, from: snode,
associatedWith: publicKey associatedWith: publicKey,
using: dependencies
) )
.flatMap { namespacedResults -> AnyPublisher<[Message], Error> in .flatMap { namespacedResults -> AnyPublisher<[Message], Error> in
guard guard
@ -391,7 +403,7 @@ public class Poller {
// Note: In the background we just want jobs to fail silently // Note: In the background we just want jobs to fail silently
ConfigMessageReceiveJob.run( ConfigMessageReceiveJob.run(
job, job,
queue: queue, queue: dependencies.receiveQueue,
success: { _, _ in resolver(Result.success(())) }, success: { _, _ in resolver(Result.success(())) },
failure: { _, _, _ in resolver(Result.success(())) }, failure: { _, _, _ in resolver(Result.success(())) },
deferred: { _ in resolver(Result.success(())) } deferred: { _ in resolver(Result.success(())) }
@ -411,7 +423,7 @@ public class Poller {
// Note: In the background we just want jobs to fail silently // Note: In the background we just want jobs to fail silently
MessageReceiveJob.run( MessageReceiveJob.run(
job, job,
queue: queue, queue: dependencies.receiveQueue,
success: { _, _ in resolver(Result.success(())) }, success: { _, _ in resolver(Result.success(())) },
failure: { _, _, _ in resolver(Result.success(())) }, failure: { _, _, _ in resolver(Result.success(())) },
deferred: { _ in resolver(Result.success(())) } deferred: { _ in resolver(Result.success(())) }

View File

@ -328,7 +328,7 @@ public extension SessionUtil {
return false return false
} }
return (oneToOne.last_read > timestampMs) return (oneToOne.last_read >= timestampMs)
case .legacyGroup: case .legacyGroup:
var cThreadId: [CChar] = threadId.cArray.nullTerminated() var cThreadId: [CChar] = threadId.cArray.nullTerminated()
@ -338,7 +338,7 @@ public extension SessionUtil {
return false return false
} }
return (legacyGroup.last_read > timestampMs) return (legacyGroup.last_read >= timestampMs)
case .community: case .community:
guard let openGroup: OpenGroup = openGroup else { return false } guard let openGroup: OpenGroup = openGroup else { return false }
@ -351,7 +351,7 @@ public extension SessionUtil {
return false return false
} }
return (convoCommunity.last_read > timestampMs) return (convoCommunity.last_read >= timestampMs)
case .group: return false case .group: return false
} }

View File

@ -355,7 +355,6 @@ public enum SessionUtil {
) )
let seqNo: Int64 = cPushData.pointee.seqno let seqNo: Int64 = cPushData.pointee.seqno
cPushData.deallocate() cPushData.deallocate()
SNLog("[libSession - DEBUG] Push data for \(variant) config data, size: \(configCountInfo), bytes: \(pushData.count)")
return OutgoingConfResult( return OutgoingConfResult(
message: SharedConfigMessage( message: SharedConfigMessage(

View File

@ -56,6 +56,15 @@ public struct MessageViewModel: FetchableRecordWithRowId, Decodable, Equatable,
case typingIndicator case typingIndicator
case dateHeader case dateHeader
case unreadMarker case unreadMarker
/// A number of the `CellType` entries are dynamically added to the dataset after processing, this flag indicates
/// whether the given type is one of them
public var isPostProcessed: Bool {
switch self {
case .typingIndicator, .dateHeader, .unreadMarker: return true
default: return false
}
}
} }
public var differenceIdentifier: Int64 { id } public var differenceIdentifier: Int64 { id }
@ -74,6 +83,7 @@ public struct MessageViewModel: FetchableRecordWithRowId, Decodable, Equatable,
public let rowId: Int64 public let rowId: Int64
public let id: Int64 public let id: Int64
public let openGroupServerMessageId: Int64?
public let variant: Interaction.Variant public let variant: Interaction.Variant
public let timestampMs: Int64 public let timestampMs: Int64
public let receivedAtTimestampMs: Int64 public let receivedAtTimestampMs: Int64
@ -171,6 +181,7 @@ public struct MessageViewModel: FetchableRecordWithRowId, Decodable, Equatable,
threadContactNameInternal: self.threadContactNameInternal, threadContactNameInternal: self.threadContactNameInternal,
rowId: self.rowId, rowId: self.rowId,
id: self.id, id: self.id,
openGroupServerMessageId: self.openGroupServerMessageId,
variant: self.variant, variant: self.variant,
timestampMs: self.timestampMs, timestampMs: self.timestampMs,
receivedAtTimestampMs: self.receivedAtTimestampMs, receivedAtTimestampMs: self.receivedAtTimestampMs,
@ -335,6 +346,7 @@ public struct MessageViewModel: FetchableRecordWithRowId, Decodable, Equatable,
threadContactNameInternal: self.threadContactNameInternal, threadContactNameInternal: self.threadContactNameInternal,
rowId: self.rowId, rowId: self.rowId,
id: self.id, id: self.id,
openGroupServerMessageId: self.openGroupServerMessageId,
variant: self.variant, variant: self.variant,
timestampMs: self.timestampMs, timestampMs: self.timestampMs,
receivedAtTimestampMs: self.receivedAtTimestampMs, receivedAtTimestampMs: self.receivedAtTimestampMs,
@ -516,7 +528,7 @@ public extension MessageViewModel {
static let genericId: Int64 = -1 static let genericId: Int64 = -1
static let typingIndicatorId: Int64 = -2 static let typingIndicatorId: Int64 = -2
// Note: This init method is only used system-created cells or empty states /// This init method is only used for system-created cells or empty states
init( init(
variant: Interaction.Variant = .standardOutgoing, variant: Interaction.Variant = .standardOutgoing,
timestampMs: Int64 = Int64.max, timestampMs: Int64 = Int64.max,
@ -546,6 +558,7 @@ public extension MessageViewModel {
}() }()
self.rowId = targetId self.rowId = targetId
self.id = targetId self.id = targetId
self.openGroupServerMessageId = nil
self.variant = variant self.variant = variant
self.timestampMs = timestampMs self.timestampMs = timestampMs
self.receivedAtTimestampMs = receivedAtTimestampMs self.receivedAtTimestampMs = receivedAtTimestampMs
@ -567,11 +580,11 @@ public extension MessageViewModel {
self.linkPreview = nil self.linkPreview = nil
self.linkPreviewAttachment = nil self.linkPreviewAttachment = nil
self.currentUserPublicKey = "" self.currentUserPublicKey = ""
self.attachments = nil
self.reactionInfo = nil
// Post-Query Processing Data // Post-Query Processing Data
self.attachments = nil
self.reactionInfo = nil
self.cellType = cellType self.cellType = cellType
self.authorName = "" self.authorName = ""
self.senderName = nil self.senderName = nil
@ -587,6 +600,84 @@ public extension MessageViewModel {
self.isLastOutgoing = isLastOutgoing self.isLastOutgoing = isLastOutgoing
self.currentUserBlindedPublicKey = nil self.currentUserBlindedPublicKey = nil
} }
/// This init method is only used for optimistic outgoing messages
init(
threadId: String,
threadVariant: SessionThread.Variant,
threadHasDisappearingMessagesEnabled: Bool,
threadOpenGroupServer: String?,
threadOpenGroupPublicKey: String?,
threadContactNameInternal: String,
timestampMs: Int64,
receivedAtTimestampMs: Int64,
authorId: String,
authorNameInternal: String,
body: String?,
expiresStartedAtMs: Double?,
expiresInSeconds: TimeInterval?,
isSenderOpenGroupModerator: Bool,
currentUserProfile: Profile,
quote: Quote?,
quoteAttachment: Attachment?,
linkPreview: LinkPreview?,
linkPreviewAttachment: Attachment?,
attachments: [Attachment]?
) {
self.threadId = threadId
self.threadVariant = threadVariant
self.threadIsTrusted = false
self.threadHasDisappearingMessagesEnabled = threadHasDisappearingMessagesEnabled
self.threadOpenGroupServer = threadOpenGroupServer
self.threadOpenGroupPublicKey = threadOpenGroupPublicKey
self.threadContactNameInternal = threadContactNameInternal
// Interaction Info
self.rowId = -1
self.id = -1
self.openGroupServerMessageId = nil
self.variant = .standardOutgoing
self.timestampMs = timestampMs
self.receivedAtTimestampMs = receivedAtTimestampMs
self.authorId = authorId
self.authorNameInternal = authorNameInternal
self.body = body
self.rawBody = body
self.expiresStartedAtMs = expiresStartedAtMs
self.expiresInSeconds = expiresInSeconds
self.state = .sending
self.hasAtLeastOneReadReceipt = false
self.mostRecentFailureText = nil
self.isSenderOpenGroupModerator = isSenderOpenGroupModerator
self.isTypingIndicator = false
self.profile = currentUserProfile
self.quote = quote
self.quoteAttachment = quoteAttachment
self.linkPreview = linkPreview
self.linkPreviewAttachment = linkPreviewAttachment
self.currentUserPublicKey = currentUserProfile.id
self.attachments = attachments
self.reactionInfo = nil
// Post-Query Processing Data
self.cellType = .textOnlyMessage
self.authorName = ""
self.senderName = nil
self.canHaveProfile = false
self.shouldShowProfile = false
self.shouldShowDateHeader = false
self.containsOnlyEmoji = nil
self.glyphCount = nil
self.previousVariant = nil
self.positionInCluster = .middle
self.isOnlyMessageInCluster = true
self.isLast = false
self.isLastOutgoing = false
self.currentUserBlindedPublicKey = nil
}
} }
// MARK: - Convenience // MARK: - Convenience
@ -688,7 +779,7 @@ public extension MessageViewModel {
let interactionAttachmentAttachmentIdColumn: SQL = SQL(stringLiteral: InteractionAttachment.Columns.attachmentId.name) let interactionAttachmentAttachmentIdColumn: SQL = SQL(stringLiteral: InteractionAttachment.Columns.attachmentId.name)
let interactionAttachmentAlbumIndexColumn: SQL = SQL(stringLiteral: InteractionAttachment.Columns.albumIndex.name) let interactionAttachmentAlbumIndexColumn: SQL = SQL(stringLiteral: InteractionAttachment.Columns.albumIndex.name)
let numColumnsBeforeLinkedRecords: Int = 21 let numColumnsBeforeLinkedRecords: Int = 22
let finalGroupSQL: SQL = (groupSQL ?? "") let finalGroupSQL: SQL = (groupSQL ?? "")
let request: SQLRequest<ViewModel> = """ let request: SQLRequest<ViewModel> = """
SELECT SELECT
@ -704,6 +795,7 @@ public extension MessageViewModel {
\(interaction.alias[Column.rowID]) AS \(ViewModel.rowIdKey), \(interaction.alias[Column.rowID]) AS \(ViewModel.rowIdKey),
\(interaction[.id]), \(interaction[.id]),
\(interaction[.openGroupServerMessageId]),
\(interaction[.variant]), \(interaction[.variant]),
\(interaction[.timestampMs]), \(interaction[.timestampMs]),
\(interaction[.receivedAtTimestampMs]), \(interaction[.receivedAtTimestampMs]),
@ -977,7 +1069,7 @@ public extension MessageViewModel.ReactionInfo {
items: pagedRowIdsWithNoReactions items: pagedRowIdsWithNoReactions
.compactMap { rowId -> ViewModel? in updatedPagedDataCache.data[rowId] } .compactMap { rowId -> ViewModel? in updatedPagedDataCache.data[rowId] }
.filter { viewModel -> Bool in (viewModel.reactionInfo?.isEmpty == false) } .filter { viewModel -> Bool in (viewModel.reactionInfo?.isEmpty == false) }
.map { viewModel -> ViewModel in viewModel.with(reactionInfo: nil) } .map { viewModel -> ViewModel in viewModel.with(reactionInfo: []) }
) )
return updatedPagedDataCache return updatedPagedDataCache

View File

@ -33,6 +33,7 @@ public struct SessionThreadViewModel: FetchableRecordWithRowId, Decodable, Equat
public static let threadWasMarkedUnreadKey: SQL = SQL(stringLiteral: CodingKeys.threadWasMarkedUnread.stringValue) public static let threadWasMarkedUnreadKey: SQL = SQL(stringLiteral: CodingKeys.threadWasMarkedUnread.stringValue)
public static let threadUnreadCountKey: SQL = SQL(stringLiteral: CodingKeys.threadUnreadCount.stringValue) public static let threadUnreadCountKey: SQL = SQL(stringLiteral: CodingKeys.threadUnreadCount.stringValue)
public static let threadUnreadMentionCountKey: SQL = SQL(stringLiteral: CodingKeys.threadUnreadMentionCount.stringValue) public static let threadUnreadMentionCountKey: SQL = SQL(stringLiteral: CodingKeys.threadUnreadMentionCount.stringValue)
public static let disappearingMessagesConfigurationKey: SQL = SQL(stringLiteral: CodingKeys.disappearingMessagesConfiguration.stringValue)
public static let contactProfileKey: SQL = SQL(stringLiteral: CodingKeys.contactProfile.stringValue) public static let contactProfileKey: SQL = SQL(stringLiteral: CodingKeys.contactProfile.stringValue)
public static let closedGroupNameKey: SQL = SQL(stringLiteral: CodingKeys.closedGroupName.stringValue) public static let closedGroupNameKey: SQL = SQL(stringLiteral: CodingKeys.closedGroupName.stringValue)
public static let closedGroupUserCountKey: SQL = SQL(stringLiteral: CodingKeys.closedGroupUserCount.stringValue) public static let closedGroupUserCountKey: SQL = SQL(stringLiteral: CodingKeys.closedGroupUserCount.stringValue)
@ -66,6 +67,7 @@ public struct SessionThreadViewModel: FetchableRecordWithRowId, Decodable, Equat
public static let threadUnreadMentionCountString: String = CodingKeys.threadUnreadMentionCount.stringValue public static let threadUnreadMentionCountString: String = CodingKeys.threadUnreadMentionCount.stringValue
public static let closedGroupUserCountString: String = CodingKeys.closedGroupUserCount.stringValue public static let closedGroupUserCountString: String = CodingKeys.closedGroupUserCount.stringValue
public static let openGroupUserCountString: String = CodingKeys.openGroupUserCount.stringValue public static let openGroupUserCountString: String = CodingKeys.openGroupUserCount.stringValue
public static let disappearingMessagesConfigurationString: String = CodingKeys.disappearingMessagesConfiguration.stringValue
public static let contactProfileString: String = CodingKeys.contactProfile.stringValue public static let contactProfileString: String = CodingKeys.contactProfile.stringValue
public static let closedGroupProfileFrontString: String = CodingKeys.closedGroupProfileFront.stringValue public static let closedGroupProfileFrontString: String = CodingKeys.closedGroupProfileFront.stringValue
public static let closedGroupProfileBackString: String = CodingKeys.closedGroupProfileBack.stringValue public static let closedGroupProfileBackString: String = CodingKeys.closedGroupProfileBack.stringValue
@ -116,6 +118,8 @@ public struct SessionThreadViewModel: FetchableRecordWithRowId, Decodable, Equat
// Thread display info // Thread display info
public let disappearingMessagesConfiguration: DisappearingMessagesConfiguration?
private let contactProfile: Profile? private let contactProfile: Profile?
private let closedGroupProfileFront: Profile? private let closedGroupProfileFront: Profile?
private let closedGroupProfileBack: Profile? private let closedGroupProfileBack: Profile?
@ -343,7 +347,8 @@ public extension SessionThreadViewModel {
contactProfile: Profile? = nil, contactProfile: Profile? = nil,
currentUserIsClosedGroupMember: Bool? = nil, currentUserIsClosedGroupMember: Bool? = nil,
openGroupPermissions: OpenGroup.Permissions? = nil, openGroupPermissions: OpenGroup.Permissions? = nil,
unreadCount: UInt = 0 unreadCount: UInt = 0,
disappearingMessagesConfiguration: DisappearingMessagesConfiguration? = nil
) { ) {
self.rowId = -1 self.rowId = -1
self.threadId = threadId self.threadId = threadId
@ -368,6 +373,8 @@ public extension SessionThreadViewModel {
// Thread display info // Thread display info
self.disappearingMessagesConfiguration = disappearingMessagesConfiguration
self.contactProfile = contactProfile self.contactProfile = contactProfile
self.closedGroupProfileFront = nil self.closedGroupProfileFront = nil
self.closedGroupProfileBack = nil self.closedGroupProfileBack = nil
@ -430,6 +437,7 @@ public extension SessionThreadViewModel {
threadWasMarkedUnread: self.threadWasMarkedUnread, threadWasMarkedUnread: self.threadWasMarkedUnread,
threadUnreadCount: self.threadUnreadCount, threadUnreadCount: self.threadUnreadCount,
threadUnreadMentionCount: self.threadUnreadMentionCount, threadUnreadMentionCount: self.threadUnreadMentionCount,
disappearingMessagesConfiguration: self.disappearingMessagesConfiguration,
contactProfile: self.contactProfile, contactProfile: self.contactProfile,
closedGroupProfileFront: self.closedGroupProfileFront, closedGroupProfileFront: self.closedGroupProfileFront,
closedGroupProfileBack: self.closedGroupProfileBack, closedGroupProfileBack: self.closedGroupProfileBack,
@ -486,6 +494,7 @@ public extension SessionThreadViewModel {
threadWasMarkedUnread: self.threadWasMarkedUnread, threadWasMarkedUnread: self.threadWasMarkedUnread,
threadUnreadCount: self.threadUnreadCount, threadUnreadCount: self.threadUnreadCount,
threadUnreadMentionCount: self.threadUnreadMentionCount, threadUnreadMentionCount: self.threadUnreadMentionCount,
disappearingMessagesConfiguration: self.disappearingMessagesConfiguration,
contactProfile: self.contactProfile, contactProfile: self.contactProfile,
closedGroupProfileFront: self.closedGroupProfileFront, closedGroupProfileFront: self.closedGroupProfileFront,
closedGroupProfileBack: self.closedGroupProfileBack, closedGroupProfileBack: self.closedGroupProfileBack,
@ -839,6 +848,7 @@ public extension SessionThreadViewModel {
/// but including this warning just in case there is a discrepancy) /// but including this warning just in case there is a discrepancy)
static func conversationQuery(threadId: String, userPublicKey: String) -> AdaptedFetchRequest<SQLRequest<SessionThreadViewModel>> { static func conversationQuery(threadId: String, userPublicKey: String) -> AdaptedFetchRequest<SQLRequest<SessionThreadViewModel>> {
let thread: TypedTableAlias<SessionThread> = TypedTableAlias() let thread: TypedTableAlias<SessionThread> = TypedTableAlias()
let disappearingMessagesConfiguration: TypedTableAlias<DisappearingMessagesConfiguration> = TypedTableAlias()
let contact: TypedTableAlias<Contact> = TypedTableAlias() let contact: TypedTableAlias<Contact> = TypedTableAlias()
let closedGroup: TypedTableAlias<ClosedGroup> = TypedTableAlias() let closedGroup: TypedTableAlias<ClosedGroup> = TypedTableAlias()
let groupMember: TypedTableAlias<GroupMember> = TypedTableAlias() let groupMember: TypedTableAlias<GroupMember> = TypedTableAlias()
@ -883,6 +893,8 @@ public extension SessionThreadViewModel {
\(thread[.markedAsUnread]) AS \(ViewModel.threadWasMarkedUnreadKey), \(thread[.markedAsUnread]) AS \(ViewModel.threadWasMarkedUnreadKey),
\(aggregateInteractionLiteral).\(ViewModel.threadUnreadCountKey), \(aggregateInteractionLiteral).\(ViewModel.threadUnreadCountKey),
\(ViewModel.disappearingMessagesConfigurationKey).*,
\(ViewModel.contactProfileKey).*, \(ViewModel.contactProfileKey).*,
\(closedGroup[.name]) AS \(ViewModel.closedGroupNameKey), \(closedGroup[.name]) AS \(ViewModel.closedGroupNameKey),
@ -911,6 +923,7 @@ public extension SessionThreadViewModel {
\(SQL("\(userPublicKey)")) AS \(ViewModel.currentUserPublicKeyKey) \(SQL("\(userPublicKey)")) AS \(ViewModel.currentUserPublicKeyKey)
FROM \(SessionThread.self) FROM \(SessionThread.self)
LEFT JOIN \(DisappearingMessagesConfiguration.self) ON \(disappearingMessagesConfiguration[.threadId]) = \(thread[.id])
LEFT JOIN \(Contact.self) ON \(contact[.id]) = \(thread[.id]) LEFT JOIN \(Contact.self) ON \(contact[.id]) = \(thread[.id])
LEFT JOIN ( LEFT JOIN (
SELECT SELECT
@ -945,11 +958,13 @@ public extension SessionThreadViewModel {
return request.adapted { db in return request.adapted { db in
let adapters = try splittingRowAdapters(columnCounts: [ let adapters = try splittingRowAdapters(columnCounts: [
numColumnsBeforeProfiles, numColumnsBeforeProfiles,
DisappearingMessagesConfiguration.numberOfSelectedColumns(db),
Profile.numberOfSelectedColumns(db) Profile.numberOfSelectedColumns(db)
]) ])
return ScopeAdapter([ return ScopeAdapter([
ViewModel.contactProfileString: adapters[1] ViewModel.disappearingMessagesConfigurationString: adapters[1],
ViewModel.contactProfileString: adapters[2]
]) ])
} }
} }

View File

@ -51,7 +51,10 @@ public struct ProfileManager {
} }
if let profilePictureUrl: String = profile.profilePictureUrl, !profilePictureUrl.isEmpty { if let profilePictureUrl: String = profile.profilePictureUrl, !profilePictureUrl.isEmpty {
downloadAvatar(for: profile) // FIXME: Refactor avatar downloading to be a proper Job so we can avoid this
JobRunner.afterBlockingQueue {
ProfileManager.downloadAvatar(for: profile)
}
} }
return nil return nil
@ -78,7 +81,10 @@ public struct ProfileManager {
completion: { _, _ in completion: { _, _ in
// Try to re-download the avatar if it has a URL // Try to re-download the avatar if it has a URL
if let profilePictureUrl: String = profile.profilePictureUrl, !profilePictureUrl.isEmpty { if let profilePictureUrl: String = profile.profilePictureUrl, !profilePictureUrl.isEmpty {
downloadAvatar(for: profile) // FIXME: Refactor avatar downloading to be a proper Job so we can avoid this
JobRunner.afterBlockingQueue {
ProfileManager.downloadAvatar(for: profile)
}
} }
} }
) )
@ -214,7 +220,8 @@ public struct ProfileManager {
FileServerAPI FileServerAPI
.download(fileId, useOldServer: useOldServer) .download(fileId, useOldServer: useOldServer)
.receive(on: DispatchQueue.global(qos: .default)) .subscribe(on: DispatchQueue.global(qos: .background))
.receive(on: DispatchQueue.global(qos: .background))
.sinkUntilComplete( .sinkUntilComplete(
receiveCompletion: { _ in receiveCompletion: { _ in
currentAvatarDownloads.mutate { $0.remove(profile.id) } currentAvatarDownloads.mutate { $0.remove(profile.id) }
@ -451,6 +458,7 @@ public struct ProfileManager {
// Upload the avatar to the FileServer // Upload the avatar to the FileServer
FileServerAPI FileServerAPI
.upload(encryptedAvatarData) .upload(encryptedAvatarData)
.subscribe(on: DispatchQueue.global(qos: .userInitiated))
.receive(on: queue) .receive(on: queue)
.sinkUntilComplete( .sinkUntilComplete(
receiveCompletion: { result in receiveCompletion: { result in
@ -590,7 +598,12 @@ public struct ProfileManager {
db.afterNextTransactionNestedOnce(dedupeId: dedupeIdentifier) { db in db.afterNextTransactionNestedOnce(dedupeId: dedupeIdentifier) { db in
// Need to refetch to ensure the db changes have occurred // Need to refetch to ensure the db changes have occurred
ProfileManager.downloadAvatar(for: Profile.fetchOrCreate(db, id: publicKey)) let targetProfile: Profile = Profile.fetchOrCreate(db, id: publicKey)
// FIXME: Refactor avatar downloading to be a proper Job so we can avoid this
JobRunner.afterBlockingQueue {
ProfileManager.downloadAvatar(for: targetProfile)
}
} }
} }
} }

View File

@ -1,6 +1,5 @@
import Foundation import Foundation
internal enum Threading { public enum Threading {
public static let pollerQueue = DispatchQueue(label: "SessionMessagingKit.pollerQueue")
internal static let pollerQueue = DispatchQueue(label: "SessionMessagingKit.pollerQueue")
} }

File diff suppressed because it is too large Load Diff

View File

@ -116,7 +116,8 @@ class OpenGroupManagerSpec: QuickSpec {
mockNonce24Generator = MockNonce24Generator() mockNonce24Generator = MockNonce24Generator()
mockUserDefaults = MockUserDefaults() mockUserDefaults = MockUserDefaults()
dependencies = OpenGroupManager.OGMDependencies( dependencies = OpenGroupManager.OGMDependencies(
queue: DispatchQueue.main, subscribeQueue: DispatchQueue.main,
receiveQueue: DispatchQueue.main,
cache: Atomic(mockOGMCache), cache: Atomic(mockOGMCache),
onionApi: TestCapabilitiesAndRoomApi.self, onionApi: TestCapabilitiesAndRoomApi.self,
generalCache: Atomic(mockGeneralCache), generalCache: Atomic(mockGeneralCache),
@ -991,7 +992,7 @@ class OpenGroupManagerSpec: QuickSpec {
db, db,
openGroupId: OpenGroup.idFor(roomToken: "testRoom", server: "testServer"), openGroupId: OpenGroup.idFor(roomToken: "testRoom", server: "testServer"),
calledFromConfigHandling: true, // Don't trigger SessionUtil logic calledFromConfigHandling: true, // Don't trigger SessionUtil logic
dependencies: dependencies using: dependencies
) )
} }
@ -1006,7 +1007,7 @@ class OpenGroupManagerSpec: QuickSpec {
db, db,
openGroupId: OpenGroup.idFor(roomToken: "testRoom", server: "testServer"), openGroupId: OpenGroup.idFor(roomToken: "testRoom", server: "testServer"),
calledFromConfigHandling: true, // Don't trigger SessionUtil logic calledFromConfigHandling: true, // Don't trigger SessionUtil logic
dependencies: dependencies using: dependencies
) )
} }
@ -1024,7 +1025,7 @@ class OpenGroupManagerSpec: QuickSpec {
db, db,
openGroupId: OpenGroup.idFor(roomToken: "testRoom", server: "testServer"), openGroupId: OpenGroup.idFor(roomToken: "testRoom", server: "testServer"),
calledFromConfigHandling: true, // Don't trigger SessionUtil logic calledFromConfigHandling: true, // Don't trigger SessionUtil logic
dependencies: dependencies using: dependencies
) )
} }
@ -1038,7 +1039,7 @@ class OpenGroupManagerSpec: QuickSpec {
db, db,
openGroupId: OpenGroup.idFor(roomToken: "testRoom", server: "testServer"), openGroupId: OpenGroup.idFor(roomToken: "testRoom", server: "testServer"),
calledFromConfigHandling: true, // Don't trigger SessionUtil logic calledFromConfigHandling: true, // Don't trigger SessionUtil logic
dependencies: dependencies using: dependencies
) )
} }
@ -1077,7 +1078,7 @@ class OpenGroupManagerSpec: QuickSpec {
db, db,
openGroupId: OpenGroup.idFor(roomToken: "testRoom", server: "testServer"), openGroupId: OpenGroup.idFor(roomToken: "testRoom", server: "testServer"),
calledFromConfigHandling: true, // Don't trigger SessionUtil logic calledFromConfigHandling: true, // Don't trigger SessionUtil logic
dependencies: dependencies using: dependencies
) )
} }
@ -1130,7 +1131,7 @@ class OpenGroupManagerSpec: QuickSpec {
db, db,
openGroupId: OpenGroup.idFor(roomToken: "testRoom", server: OpenGroupAPI.defaultServer), openGroupId: OpenGroup.idFor(roomToken: "testRoom", server: OpenGroupAPI.defaultServer),
calledFromConfigHandling: true, // Don't trigger SessionUtil logic calledFromConfigHandling: true, // Don't trigger SessionUtil logic
dependencies: dependencies using: dependencies
) )
} }
@ -1145,7 +1146,7 @@ class OpenGroupManagerSpec: QuickSpec {
db, db,
openGroupId: OpenGroup.idFor(roomToken: "testRoom", server: OpenGroupAPI.defaultServer), openGroupId: OpenGroup.idFor(roomToken: "testRoom", server: OpenGroupAPI.defaultServer),
calledFromConfigHandling: true, // Don't trigger SessionUtil logic calledFromConfigHandling: true, // Don't trigger SessionUtil logic
dependencies: dependencies using: dependencies
) )
} }

View File

@ -136,7 +136,14 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension
guard case .preOffer = callMessage.kind else { return self.completeSilenty() } guard case .preOffer = callMessage.kind else { return self.completeSilenty() }
if !db[.areCallsEnabled] { if !db[.areCallsEnabled] {
if let sender: String = callMessage.sender, let interaction: Interaction = try MessageReceiver.insertCallInfoMessage(db, for: callMessage, state: .permissionDenied) { if
let sender: String = callMessage.sender,
let interaction: Interaction = try MessageReceiver.insertCallInfoMessage(
db,
for: callMessage,
state: .permissionDenied
)
{
let thread: SessionThread = try SessionThread let thread: SessionThread = try SessionThread
.fetchOrCreate( .fetchOrCreate(
db, db,
@ -145,12 +152,15 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension
shouldBeVisible: nil shouldBeVisible: nil
) )
Environment.shared?.notificationsManager.wrappedValue? // Notify the user if the call message wasn't already read
.notifyUser( if !interaction.wasRead {
db, Environment.shared?.notificationsManager.wrappedValue?
forIncomingCall: interaction, .notifyUser(
in: thread db,
) forIncomingCall: interaction,
in: thread
)
}
} }
break break
} }

View File

@ -153,7 +153,8 @@ final class ThreadPickerVC: UIViewController, UITableViewDataSource, UITableView
tableView.deselectRow(at: indexPath, animated: true) tableView.deselectRow(at: indexPath, animated: true)
ShareNavController.attachmentPrepPublisher? ShareNavController.attachmentPrepPublisher?
.receiveOnMain(immediately: true) .subscribe(on: DispatchQueue.global(qos: .userInitiated))
.receive(on: DispatchQueue.main, immediatelyIfMain: true)
.sinkUntilComplete( .sinkUntilComplete(
receiveValue: { [weak self] attachments in receiveValue: { [weak self] attachments in
guard let strongSelf = self else { return } guard let strongSelf = self else { return }
@ -232,18 +233,20 @@ final class ThreadPickerVC: UIViewController, UITableViewDataSource, UITableView
try LinkPreview( try LinkPreview(
url: linkPreviewDraft.urlString, url: linkPreviewDraft.urlString,
title: linkPreviewDraft.title, title: linkPreviewDraft.title,
attachmentId: LinkPreview.saveAttachmentIfPossible( attachmentId: LinkPreview
db, .generateAttachmentIfPossible(
imageData: linkPreviewDraft.jpegImageData, imageData: linkPreviewDraft.jpegImageData,
mimeType: OWSMimeTypeImageJpeg mimeType: OWSMimeTypeImageJpeg
) )?
.inserted(db)
.id
).insert(db) ).insert(db)
} }
// Prepare any attachments // Prepare any attachments
try Attachment.prepare( try Attachment.process(
db, db,
attachments: finalAttachments, data: Attachment.prepare(attachments: finalAttachments),
for: interactionId for: interactionId
) )
@ -257,13 +260,9 @@ final class ThreadPickerVC: UIViewController, UITableViewDataSource, UITableView
) )
} }
.subscribe(on: DispatchQueue.global(qos: .userInitiated)) .subscribe(on: DispatchQueue.global(qos: .userInitiated))
.flatMap { .flatMap { MessageSender.performUploadsIfNeeded(preparedSendData: $0) }
MessageSender.performUploadsIfNeeded(
queue: DispatchQueue.global(qos: .userInitiated),
preparedSendData: $0
)
}
.flatMap { MessageSender.sendImmediate(preparedSendData: $0) } .flatMap { MessageSender.sendImmediate(preparedSendData: $0) }
.subscribe(on: DispatchQueue.global(qos: .userInitiated))
.receive(on: DispatchQueue.main) .receive(on: DispatchQueue.main)
.sinkUntilComplete( .sinkUntilComplete(
receiveCompletion: { [weak self] result in receiveCompletion: { [weak self] result in

View File

@ -29,6 +29,7 @@ public class ThreadPickerViewModel {
.fetchAll(db) .fetchAll(db)
} }
.removeDuplicates() .removeDuplicates()
.handleEvents(didFail: { SNLog("[ThreadPickerViewModel] Observation failed with error: \($0)") })
// MARK: - Functions // MARK: - Functions

View File

@ -56,7 +56,7 @@ public enum GetSnodePoolJob: JobExecutor {
public static func run() { public static func run() {
GetSnodePoolJob.run( GetSnodePoolJob.run(
Job(variant: .getSnodePool), Job(variant: .getSnodePool),
queue: DispatchQueue.global(qos: .background), queue: .global(qos: .background),
success: { _, _ in }, success: { _, _ in },
failure: { _, _, _ in }, failure: { _, _, _ in },
deferred: { _ in } deferred: { _ in }

View File

@ -0,0 +1,16 @@
// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved.
import Foundation
import SessionUtilitiesKit
extension SnodeAPI {
public struct GetStatsResponse: Codable {
private enum CodingKeys: String, CodingKey {
case versionString = "version"
}
let versionString: String?
var version: Version? { versionString.map { Version.from($0) } }
}
}

View File

@ -71,18 +71,12 @@ public enum OnionRequestAPI: OnionRequestAPIType {
let timeout: TimeInterval = 3 // Use a shorter timeout for testing let timeout: TimeInterval = 3 // Use a shorter timeout for testing
return HTTP.execute(.get, url, timeout: timeout) return HTTP.execute(.get, url, timeout: timeout)
.subscribe(on: DispatchQueue.global(qos: .userInitiated)) .decoded(as: SnodeAPI.GetStatsResponse.self)
.tryMap { responseData -> Void in .tryMap { response -> Void in
// TODO: Remove JSON usage guard let version: Version = response.version else { throw OnionRequestAPIError.missingSnodeVersion }
guard let responseJson: JSON = try? JSONSerialization.jsonObject(with: responseData, options: [ .fragmentsAllowed ]) as? JSON else { guard version >= Version(major: 2, minor: 0, patch: 7) else {
throw HTTPError.invalidJSON SNLog("Unsupported snode version: \(version.stringValue).")
} throw OnionRequestAPIError.unsupportedSnodeVersion(version.stringValue)
guard let version = responseJson["version"] as? String else {
throw OnionRequestAPIError.missingSnodeVersion
}
guard version >= "2.0.7" else {
SNLog("Unsupported snode version: \(version).")
throw OnionRequestAPIError.unsupportedSnodeVersion(version)
} }
return () return ()
@ -154,63 +148,82 @@ public enum OnionRequestAPI: OnionRequestAPIType {
return existingBuildPathsPublisher return existingBuildPathsPublisher
} }
SNLog("Building onion request paths.") return buildPathsPublisher.mutate { result in
DispatchQueue.main.async { /// It was possible for multiple threads to call this at the same time resulting in duplicate promises getting created, while
NotificationCenter.default.post(name: .buildingPaths, object: nil) /// this should no longer be possible (as the `wrappedValue` should now properly be blocked) this is a sanity check
} /// to make sure we don't create an additional promise when one already exists
let reusableGuardSnodes = reusablePaths.map { $0[0] } if let previouslyBlockedPublisher: AnyPublisher<[[Snode]], Error> = result {
let publisher: AnyPublisher<[[Snode]], Error> = getGuardSnodes(reusing: reusableGuardSnodes) return previouslyBlockedPublisher
.flatMap { guardSnodes -> AnyPublisher<[[Snode]], Error> in }
var unusedSnodes = SnodeAPI.snodePool.wrappedValue
.subtracting(guardSnodes) SNLog("Building onion request paths.")
.subtracting(reusablePaths.flatMap { $0 }) DispatchQueue.main.async {
let reusableGuardSnodeCount = UInt(reusableGuardSnodes.count) NotificationCenter.default.post(name: .buildingPaths, object: nil)
let pathSnodeCount = (targetGuardSnodeCount - reusableGuardSnodeCount) * pathSize - (targetGuardSnodeCount - reusableGuardSnodeCount) }
guard unusedSnodes.count >= pathSnodeCount else { /// Need to include the post-request code and a `shareReplay` within the publisher otherwise it can still be executed
return Fail<[[Snode]], Error>(error: OnionRequestAPIError.insufficientSnodes) /// multiple times as a result of multiple subscribers
.eraseToAnyPublisher() let reusableGuardSnodes = reusablePaths.map { $0[0] }
} let publisher: AnyPublisher<[[Snode]], Error> = getGuardSnodes(reusing: reusableGuardSnodes)
.flatMap { (guardSnodes: Set<Snode>) -> AnyPublisher<[[Snode]], Error> in
// Don't test path snodes as this would reveal the user's IP to them var unusedSnodes: Set<Snode> = SnodeAPI.snodePool.wrappedValue
return Just( .subtracting(guardSnodes)
guardSnodes .subtracting(reusablePaths.flatMap { $0 })
let reusableGuardSnodeCount: UInt = UInt(reusableGuardSnodes.count)
let pathSnodeCount: UInt = (targetGuardSnodeCount - reusableGuardSnodeCount) * pathSize - (targetGuardSnodeCount - reusableGuardSnodeCount)
guard unusedSnodes.count >= pathSnodeCount else {
return Fail<[[Snode]], Error>(error: OnionRequestAPIError.insufficientSnodes)
.eraseToAnyPublisher()
}
// Don't test path snodes as this would reveal the user's IP to them
let paths: [[Snode]] = guardSnodes
.subtracting(reusableGuardSnodes) .subtracting(reusableGuardSnodes)
.map { guardSnode in .map { (guardSnode: Snode) in
let result = [ guardSnode ] + (0..<(pathSize - 1)).map { _ in let result: [Snode] = [guardSnode]
// randomElement() uses the system's default random generator, which is cryptographically secure .appending(
let pathSnode = unusedSnodes.randomElement()! // Safe because of the pathSnodeCount check above contentsOf: (0..<(pathSize - 1))
unusedSnodes.remove(pathSnode) // All used snodes should be unique .map { _ in
return pathSnode // randomElement() uses the system's default random generator,
} // which is cryptographically secure
let pathSnode: Snode = unusedSnodes.randomElement()! // Safe because of the pathSnodeCount check above
unusedSnodes.remove(pathSnode) // All used snodes should be unique
return pathSnode
}
)
SNLog("Built new onion request path: \(result.prettifiedDescription).") SNLog("Built new onion request path: \(result.prettifiedDescription).")
return result return result
} }
return Just(paths)
.setFailureType(to: Error.self)
.eraseToAnyPublisher()
}
.handleEvents(
receiveOutput: { output in
OnionRequestAPI.paths = (output + reusablePaths)
Storage.shared.write { db in
SNLog("Persisting onion request paths to database.")
try? output.save(db)
}
DispatchQueue.main.async {
NotificationCenter.default.post(name: .pathsBuilt, object: nil)
}
},
receiveCompletion: { _ in buildPathsPublisher.mutate { $0 = nil } }
) )
.setFailureType(to: Error.self) .shareReplay(1)
.eraseToAnyPublisher() .eraseToAnyPublisher()
}
.handleEvents( /// Actually assign the atomic value
receiveOutput: { output in result = publisher
OnionRequestAPI.paths = (output + reusablePaths)
return publisher
Storage.shared.write { db in }
SNLog("Persisting onion request paths to database.")
try? output.save(db)
}
DispatchQueue.main.async {
NotificationCenter.default.post(name: .pathsBuilt, object: nil)
}
},
receiveCompletion: { _ in buildPathsPublisher.mutate { $0 = nil } }
)
.eraseToAnyPublisher()
buildPathsPublisher.mutate { $0 = publisher }
return publisher
} }
/// Returns a `Path` to be used for building an onion request. Builds new paths as needed. /// Returns a `Path` to be used for building an onion request. Builds new paths as needed.
@ -245,6 +258,7 @@ public enum OnionRequestAPI: OnionRequestAPIType {
if let snode = snode { if let snode = snode {
if let path = paths.first(where: { !$0.contains(snode) }) { if let path = paths.first(where: { !$0.contains(snode) }) {
buildPaths(reusing: paths) // Re-build paths in the background buildPaths(reusing: paths) // Re-build paths in the background
.subscribe(on: DispatchQueue.global(qos: .background))
.sink(receiveCompletion: { _ in cancellable = [] }, receiveValue: { _ in }) .sink(receiveCompletion: { _ in cancellable = [] }, receiveValue: { _ in })
.store(in: &cancellable) .store(in: &cancellable)
@ -269,6 +283,7 @@ public enum OnionRequestAPI: OnionRequestAPIType {
} }
else { else {
buildPaths(reusing: paths) // Re-build paths in the background buildPaths(reusing: paths) // Re-build paths in the background
.subscribe(on: DispatchQueue.global(qos: .background))
.sink(receiveCompletion: { _ in cancellable = [] }, receiveValue: { _ in }) .sink(receiveCompletion: { _ in cancellable = [] }, receiveValue: { _ in })
.store(in: &cancellable) .store(in: &cancellable)
@ -480,7 +495,6 @@ public enum OnionRequestAPI: OnionRequestAPIType {
var guardSnode: Snode? var guardSnode: Snode?
return buildOnion(around: payload, targetedAt: destination) return buildOnion(around: payload, targetedAt: destination)
.subscribe(on: Threading.workQueue)
.flatMap { intermediate -> AnyPublisher<(ResponseInfoType, Data?), Error> in .flatMap { intermediate -> AnyPublisher<(ResponseInfoType, Data?), Error> in
guardSnode = intermediate.guardSnode guardSnode = intermediate.guardSnode
let url = "\(guardSnode!.address):\(guardSnode!.port)/onion_req/v2" let url = "\(guardSnode!.address):\(guardSnode!.port)/onion_req/v2"

Some files were not shown because too many files have changed in this diff Show More