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:
parent
5db254303a
commit
53a5db0ea5
|
@ -1 +1 @@
|
||||||
Subproject commit 49c78682a6f4546c8773113f3e201244f0b1e65a
|
Subproject commit e0b994201a016cc5bf9065526a0ceb4291f60d5a
|
|
@ -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)",
|
||||||
|
|
|
@ -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
|
||||||
}()
|
}()
|
||||||
|
|
|
@ -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,
|
||||||
|
quoteModel: snInputView.quoteDraftInfo?.model
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
func sendMessage(hasPermissionToSendSeed: Bool = false) {
|
func sendMessage(
|
||||||
|
text: String,
|
||||||
|
attachments: [SignalAttachment] = [],
|
||||||
|
linkPreviewDraft: LinkPreviewDraft? = nil,
|
||||||
|
quoteModel: QuotedReplyModel? = nil,
|
||||||
|
hasPermissionToSendSeed: Bool = false
|
||||||
|
) {
|
||||||
guard !showBlockedModalIfNeeded() else { return }
|
guard !showBlockedModalIfNeeded() else { return }
|
||||||
|
|
||||||
let text = replaceMentions(in: snInputView.text.trimmingCharacters(in: .whitespacesAndNewlines))
|
// Handle attachment errors if applicable
|
||||||
|
if let failedAttachment: SignalAttachment = attachments.first(where: { $0.hasError }) {
|
||||||
guard !text.isEmpty else { return }
|
return showErrorAlert(for: failedAttachment)
|
||||||
|
|
||||||
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
|
let processedText: String = replaceMentions(in: text.trimmingCharacters(in: .whitespacesAndNewlines))
|
||||||
DispatchQueue.main.async { [weak self] in
|
|
||||||
self?.snInputView.text = ""
|
|
||||||
self?.snInputView.quoteDraftInfo = nil
|
|
||||||
|
|
||||||
self?.resetMentions()
|
// If we have no content then do nothing
|
||||||
}
|
guard !processedText.isEmpty || !attachments.isEmpty else { return }
|
||||||
|
|
||||||
// Note: 'shouldBeVisible' is set to true the first time a thread is saved so we can
|
if processedText.contains(mnemonic) && !viewModel.threadData.threadIsNoteToSelf && !hasPermissionToSendSeed {
|
||||||
// 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) {
|
|
||||||
guard !showBlockedModalIfNeeded() else { return }
|
|
||||||
|
|
||||||
for attachment in attachments {
|
|
||||||
if attachment.hasError {
|
|
||||||
return showErrorAlert(for: attachment, onDismiss: onComplete)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let text = replaceMentions(in: snInputView.text.trimmingCharacters(in: .whitespacesAndNewlines))
|
|
||||||
|
|
||||||
if text.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,20 +1153,13 @@ 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))
|
||||||
|
.flatMap { sendData, pendingChange in
|
||||||
|
OpenGroupAPI.send(data: sendData)
|
||||||
.handleEvents(
|
.handleEvents(
|
||||||
receiveOutput: { response, pendingChange in
|
receiveOutput: { _, response in
|
||||||
OpenGroupManager
|
OpenGroupManager
|
||||||
.updatePendingChange(
|
.updatePendingChange(
|
||||||
pendingChange,
|
pendingChange,
|
||||||
|
@ -1253,6 +1167,8 @@ extension ConversationVC:
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
.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,14 +1257,15 @@ 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(
|
|
||||||
|
let sortId: Int64 = Reaction.getSortId(
|
||||||
db,
|
db,
|
||||||
interactionId: cellViewModel.id,
|
interactionId: cellViewModel.id,
|
||||||
emoji: emoji
|
emoji: emoji
|
||||||
|
@ -1332,7 +1280,6 @@ extension ConversationVC:
|
||||||
count: 1,
|
count: 1,
|
||||||
sortId: sortId
|
sortId: sortId
|
||||||
)
|
)
|
||||||
}
|
|
||||||
}()
|
}()
|
||||||
|
|
||||||
// Update the database
|
// Update the database
|
||||||
|
@ -1350,11 +1297,43 @@ 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 {
|
||||||
|
case .community:
|
||||||
guard
|
guard
|
||||||
let openGroup: OpenGroup = try? OpenGroup.fetchOne(db, id: cellViewModel.threadId),
|
let serverMessageId: Int64 = cellViewModel.openGroupServerMessageId,
|
||||||
OpenGroupManager.isOpenGroupSupport(.reactions, on: openGroup.server)
|
let openGroupServer: String = cellViewModel.threadOpenGroupServer,
|
||||||
else {
|
let openGroupRoom: String = openGroupRoom,
|
||||||
|
let pendingChange: OpenGroupAPI.PendingChange = pendingChange,
|
||||||
|
OpenGroupManager.doesOpenGroupSupport(db, capability: .reactions, on: openGroupServer)
|
||||||
|
else { throw MessageSenderError.invalidMessage }
|
||||||
|
|
||||||
|
let sendData: OpenGroupAPI.PreparedSendData<Int64?> = try {
|
||||||
|
guard !remove else {
|
||||||
|
return try OpenGroupAPI
|
||||||
|
.preparedReactionDelete(
|
||||||
|
db,
|
||||||
|
emoji: emoji,
|
||||||
|
id: serverMessageId,
|
||||||
|
in: openGroupRoom,
|
||||||
|
on: openGroupServer
|
||||||
|
)
|
||||||
|
.map { _, response in response.seqNo }
|
||||||
|
}
|
||||||
|
|
||||||
|
return try OpenGroupAPI
|
||||||
|
.preparedReactionAdd(
|
||||||
|
db,
|
||||||
|
emoji: emoji,
|
||||||
|
id: serverMessageId,
|
||||||
|
in: openGroupRoom,
|
||||||
|
on: openGroupServer
|
||||||
|
)
|
||||||
|
.map { _, response in response.seqNo }
|
||||||
|
}()
|
||||||
|
|
||||||
|
return (nil, (pendingReaction, pendingChange, sendData))
|
||||||
|
|
||||||
|
default:
|
||||||
let sendData: MessageSender.PreparedSendData = try MessageSender.preparedSendData(
|
let sendData: MessageSender.PreparedSendData = try MessageSender.preparedSendData(
|
||||||
db,
|
db,
|
||||||
message: VisibleMessage(
|
message: VisibleMessage(
|
||||||
|
@ -1381,64 +1360,22 @@ extension ConversationVC:
|
||||||
interactionId: cellViewModel.id
|
interactionId: cellViewModel.id
|
||||||
)
|
)
|
||||||
|
|
||||||
return Just(sendData)
|
return (sendData, nil)
|
||||||
.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()
|
|
||||||
}
|
}
|
||||||
}()
|
}
|
||||||
|
.tryFlatMap { messageSendData, openGroupInfo -> AnyPublisher<Void, Error> in
|
||||||
|
switch (messageSendData, openGroupInfo) {
|
||||||
|
case (.some(let sendData), _):
|
||||||
|
return MessageSender.sendImmediate(preparedSendData: sendData)
|
||||||
|
|
||||||
return request
|
case (_, .some(let info)):
|
||||||
|
return OpenGroupAPI.send(data: info.sendData)
|
||||||
.handleEvents(
|
.handleEvents(
|
||||||
receiveOutput: { seqNo in
|
receiveOutput: { _, seqNo in
|
||||||
OpenGroupManager
|
OpenGroupManager
|
||||||
.updatePendingChange(
|
.updatePendingChange(
|
||||||
pendingChange,
|
info.pendingChange,
|
||||||
seqNo: seqNo
|
seqNo: seqNo
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
@ -1446,27 +1383,20 @@ extension ConversationVC:
|
||||||
switch result {
|
switch result {
|
||||||
case .finished: break
|
case .finished: break
|
||||||
case .failure:
|
case .failure:
|
||||||
OpenGroupManager.removePendingChange(pendingChange)
|
OpenGroupManager.removePendingChange(info.pendingChange)
|
||||||
|
|
||||||
self?.handleReactionSentFailure(
|
self?.handleReactionSentFailure(
|
||||||
pendingReaction,
|
info.pendingReaction,
|
||||||
remove: remove
|
remove: remove
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
.map { _ in nil }
|
.map { _ in () }
|
||||||
.eraseToAnyPublisher()
|
.eraseToAnyPublisher()
|
||||||
}
|
|
||||||
.subscribe(on: DispatchQueue.global(qos: .userInitiated))
|
|
||||||
.flatMap { maybeSendData -> AnyPublisher<Void, Error> in
|
|
||||||
guard let sendData: MessageSender.PreparedSendData = maybeSendData else {
|
|
||||||
return Just(())
|
|
||||||
.setFailureType(to: Error.self)
|
|
||||||
.eraseToAnyPublisher()
|
|
||||||
}
|
|
||||||
|
|
||||||
return MessageSender.sendImmediate(preparedSendData: sendData)
|
default: throw MessageSenderError.invalidMessage
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.sinkUntilComplete()
|
.sinkUntilComplete()
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
try OpenGroupAPI.preparedMessageDelete(
|
||||||
db,
|
db,
|
||||||
id: openGroupServerMessageId,
|
id: openGroupServerMessageId,
|
||||||
in: openGroup.roomToken,
|
in: openGroup.roomToken,
|
||||||
on: openGroup.server
|
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)
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
public func collapseReactions(for interactionId: Int64) {
|
/// **MUST** have the same logic as in the 'PagedDataObserver.onChangeUnsorted' above
|
||||||
reactionExpandedInteractionIds.remove(interactionId)
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Record an association between an `optimisticMessageId` and a specific `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 {
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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]
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)")
|
||||||
|
|
||||||
|
|
|
@ -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] {
|
||||||
|
|
|
@ -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 }
|
||||||
|
|
|
@ -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
|
||||||
|
@ -47,6 +48,18 @@ extension MediaDismissAnimationController: UIViewControllerAnimatedTransitioning
|
||||||
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 {
|
||||||
transitionContext.completeTransition(false)
|
transitionContext.completeTransition(false)
|
||||||
|
@ -65,6 +78,19 @@ extension MediaDismissAnimationController: UIViewControllerAnimatedTransitioning
|
||||||
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 {
|
||||||
transitionContext.completeTransition(false)
|
transitionContext.completeTransition(false)
|
||||||
|
|
|
@ -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
|
||||||
|
@ -35,6 +36,18 @@ extension MediaZoomAnimationController: UIViewControllerAnimatedTransitioning {
|
||||||
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 {
|
||||||
transitionContext.completeTransition(false)
|
transitionContext.completeTransition(false)
|
||||||
|
@ -52,6 +65,18 @@ extension MediaZoomAnimationController: UIViewControllerAnimatedTransitioning {
|
||||||
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 {
|
||||||
transitionContext.completeTransition(false)
|
transitionContext.completeTransition(false)
|
||||||
|
|
|
@ -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,13 +353,25 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
|
||||||
self?.showFailedMigrationAlert(calledFrom: lifecycleMethod, error: error)
|
self?.showFailedMigrationAlert(calledFrom: lifecycleMethod, error: error)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Only offer the 'Restore' option if the user hasn't already tried to restore
|
||||||
|
if !isRestoreError {
|
||||||
alert.addAction(UIAlertAction(title: "vc_restore_title".localized(), style: .destructive) { _ in
|
alert.addAction(UIAlertAction(title: "vc_restore_title".localized(), style: .destructive) { _ in
|
||||||
|
if SUKLegacy.hasLegacyDatabaseFile {
|
||||||
// Remove the legacy database and any message hashes that have been migrated to the new DB
|
// Remove the legacy database and any message hashes that have been migrated to the new DB
|
||||||
try? SUKLegacy.deleteLegacyDatabaseFilesAndKey()
|
try? SUKLegacy.deleteLegacyDatabaseFilesAndKey()
|
||||||
|
|
||||||
Storage.shared.write { db in
|
Storage.shared.write { db in
|
||||||
try SnodeReceivedMessageInfo.deleteAll(db)
|
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)
|
// The re-run the migration (should succeed since there is no data)
|
||||||
AppSetup.runPostSetupMigrations(
|
AppSetup.runPostSetupMigrations(
|
||||||
|
@ -366,7 +383,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
|
||||||
},
|
},
|
||||||
migrationsCompletion: { [weak self] result, needsConfigSync in
|
migrationsCompletion: { [weak self] result, needsConfigSync in
|
||||||
if case .failure(let error) = result {
|
if case .failure(let error) = result {
|
||||||
self?.showFailedMigrationAlert(calledFrom: lifecycleMethod, error: error)
|
self?.showFailedMigrationAlert(calledFrom: lifecycleMethod, error: error, isRestoreError: true)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -374,6 +391,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
}
|
||||||
|
|
||||||
alert.addAction(UIAlertAction(title: "Close", style: .default) { _ in
|
alert.addAction(UIAlertAction(title: "Close", style: .default) { _ in
|
||||||
DDLog.flushLog()
|
DDLog.flushLog()
|
||||||
|
@ -612,13 +630,18 @@ 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
|
||||||
|
/// `JobRunner.blockingQueue` to complete we can have more confidence that paths won't fail to build incorrectly
|
||||||
|
JobRunner.afterBlockingQueue { [weak self] in
|
||||||
|
self?.poller.start()
|
||||||
|
|
||||||
guard shouldStartGroupPollers else { return }
|
guard shouldStartGroupPollers else { return }
|
||||||
|
|
||||||
ClosedGroupPoller.shared.start()
|
ClosedGroupPoller.shared.start()
|
||||||
OpenGroupManager.shared.startPolling()
|
OpenGroupManager.shared.startPolling()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public func stopPollers(shouldStopUserPoller: Bool = true) {
|
public func stopPollers(shouldStopUserPoller: Bool = true) {
|
||||||
if shouldStopUserPoller {
|
if shouldStopUserPoller {
|
||||||
|
|
|
@ -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.";
|
||||||
|
|
|
@ -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.";
|
||||||
|
|
|
@ -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.";
|
||||||
|
|
|
@ -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" = "به نظر می رسد کلمات کافی وارد نکرده اید. لطفاً عبارت بازیابی خود را بررسی کنید و دوباره امتحان کنید.";
|
||||||
|
|
|
@ -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.";
|
||||||
|
|
|
@ -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.";
|
||||||
|
|
|
@ -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.";
|
||||||
|
|
|
@ -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.";
|
||||||
|
|
|
@ -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.";
|
||||||
|
|
|
@ -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.";
|
||||||
|
|
|
@ -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.";
|
||||||
|
|
|
@ -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.";
|
||||||
|
|
|
@ -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.";
|
||||||
|
|
|
@ -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.";
|
||||||
|
|
|
@ -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.";
|
||||||
|
|
|
@ -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.";
|
||||||
|
|
|
@ -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.";
|
||||||
|
|
|
@ -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.";
|
||||||
|
|
|
@ -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.";
|
||||||
|
|
|
@ -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.";
|
||||||
|
|
|
@ -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.";
|
||||||
|
|
|
@ -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.";
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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")
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -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() {
|
||||||
|
|
|
@ -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")
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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 () }
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
guard let attachment: Attachment = maybeAttachment else { return }
|
public static func process(
|
||||||
|
_ db: Database,
|
||||||
|
data: PreparedData?,
|
||||||
|
for interactionId: Int64?
|
||||||
|
) throws {
|
||||||
|
guard
|
||||||
|
let data: PreparedData = data,
|
||||||
|
let interactionId: Int64 = interactionId
|
||||||
|
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))
|
||||||
|
|
||||||
|
// We need database access for OpenGroup uploads so generate prepared data
|
||||||
|
let preparedSendData: OpenGroupAPI.PreparedSendData<FileUploadResponse>? = try {
|
||||||
switch destination {
|
switch destination {
|
||||||
case .openGroup(let openGroup):
|
case .openGroup(let openGroup):
|
||||||
return OpenGroupAPI
|
return try OpenGroupAPI
|
||||||
.uploadFile(
|
.preparedUploadFile(
|
||||||
db,
|
db,
|
||||||
bytes: data.bytes,
|
bytes: data.bytes,
|
||||||
to: openGroup.roomToken,
|
to: openGroup.roomToken,
|
||||||
on: openGroup.server
|
on: openGroup.server
|
||||||
)
|
)
|
||||||
.map { _, response -> (String, Data?, Data?) in
|
|
||||||
(
|
default: return nil
|
||||||
response.id,
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
return (
|
||||||
|
preparedSendData,
|
||||||
|
nil,
|
||||||
(destination.shouldEncrypt ? encryptionKey as Data : nil),
|
(destination.shouldEncrypt ? encryptionKey as Data : nil),
|
||||||
(destination.shouldEncrypt ? digest as Data : nil)
|
(destination.shouldEncrypt ? digest as Data : nil)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
.eraseToAnyPublisher()
|
.flatMap { preparedSendData, existingFileId, encryptionKey, digest -> AnyPublisher<(String?, Data?, Data?), Error> in
|
||||||
|
// No need to upload if the file was already uploaded
|
||||||
case .fileServer:
|
if let fileId: String = existingFileId {
|
||||||
/// **Note:** FileServer uploads don't need database access so
|
return Just((fileId, encryptionKey, digest))
|
||||||
return Just((
|
|
||||||
nil,
|
|
||||||
(destination.shouldEncrypt ? encryptionKey as Data : nil),
|
|
||||||
(destination.shouldEncrypt ? digest as Data : nil)
|
|
||||||
))
|
|
||||||
.setFailureType(to: Error.self)
|
.setFailureType(to: Error.self)
|
||||||
.eraseToAnyPublisher()
|
.eraseToAnyPublisher()
|
||||||
}
|
}
|
||||||
}
|
|
||||||
.flatMap { maybeFileId, encryptionKey, digest -> AnyPublisher<(String?, Data?, Data?), Error> in
|
|
||||||
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:
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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 }
|
||||||
|
|
||||||
|
|
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 }
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
@ -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,15 +905,19 @@ 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
|
return dependencies.storage
|
||||||
.read { db in
|
.read { db in doesOpenGroupSupport(db, capability: capability, on: server, using: dependencies) }
|
||||||
|
.defaulting(to: false)
|
||||||
|
}
|
||||||
|
|
||||||
let capabilities: [Capability.Variant] = (try? Capability
|
let capabilities: [Capability.Variant] = (try? Capability
|
||||||
.select(.variant)
|
.select(.variant)
|
||||||
.filter(Capability.Columns.openGroupServer == server)
|
.filter(Capability.Columns.openGroupServer == server)
|
||||||
|
@ -926,8 +928,6 @@ public final class OpenGroupManager {
|
||||||
|
|
||||||
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
|
||||||
public static func isUserModeratorOrAdmin(
|
public static func isUserModeratorOrAdmin(
|
||||||
|
@ -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,16 +1151,41 @@ 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 {
|
||||||
|
sendData = try OpenGroupAPI
|
||||||
|
.preparedDownloadFile(
|
||||||
db,
|
db,
|
||||||
fileId: fileId,
|
fileId: fileId,
|
||||||
from: roomToken,
|
from: roomToken,
|
||||||
on: server,
|
on: server,
|
||||||
using: dependencies
|
using: dependencies
|
||||||
)
|
)
|
||||||
.map { _, imageData in
|
}
|
||||||
|
catch {
|
||||||
|
return Fail(error: error)
|
||||||
|
.eraseToAnyPublisher()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 {
|
if server.lowercased() == OpenGroupAPI.defaultServer {
|
||||||
dependencies.storage.write { db in
|
dependencies.storage.write { db in
|
||||||
_ = try OpenGroup
|
_ = try OpenGroup
|
||||||
|
@ -1168,7 +1195,11 @@ public final class OpenGroupManager {
|
||||||
dependencies.standardUserDefaults[.lastOpenGroupImageUpdate] = now
|
dependencies.standardUserDefaults[.lastOpenGroupImageUpdate] = now
|
||||||
}
|
}
|
||||||
|
|
||||||
return imageData
|
resolver(Result.success(imageData))
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.shareReplay(1)
|
.shareReplay(1)
|
||||||
.eraseToAnyPublisher()
|
.eraseToAnyPublisher()
|
||||||
|
@ -1177,9 +1208,6 @@ public final class OpenGroupManager {
|
||||||
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,
|
||||||
|
|
|
@ -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."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 {}
|
|
@ -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,
|
||||||
|
|
|
@ -64,6 +64,7 @@ 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)
|
||||||
|
|
||||||
|
if !interaction.wasRead {
|
||||||
Environment.shared?.notificationsManager.wrappedValue?
|
Environment.shared?.notificationsManager.wrappedValue?
|
||||||
.notifyUser(
|
.notifyUser(
|
||||||
db,
|
db,
|
||||||
|
@ -71,6 +72,7 @@ extension MessageReceiver {
|
||||||
in: thread
|
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)
|
||||||
|
|
||||||
|
if !interaction.wasRead {
|
||||||
Environment.shared?.notificationsManager.wrappedValue?
|
Environment.shared?.notificationsManager.wrappedValue?
|
||||||
.notifyUser(
|
.notifyUser(
|
||||||
db,
|
db,
|
||||||
forIncomingCall: interaction,
|
forIncomingCall: interaction,
|
||||||
in: thread
|
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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -48,6 +48,7 @@ extension MessageReceiver {
|
||||||
publicKey: author,
|
publicKey: author,
|
||||||
serverHashes: [serverHash]
|
serverHashes: [serverHash]
|
||||||
)
|
)
|
||||||
|
.subscribe(on: DispatchQueue.global(qos: .background))
|
||||||
.sinkUntilComplete()
|
.sinkUntilComplete()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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(())
|
||||||
|
|
|
@ -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(())) }
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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()
|
||||||
|
@ -884,6 +894,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),
|
||||||
\(closedGroupUserCountTableLiteral).\(ViewModel.closedGroupUserCountKey) AS \(ViewModel.closedGroupUserCountKey),
|
\(closedGroupUserCountTableLiteral).\(ViewModel.closedGroupUserCountKey) AS \(ViewModel.closedGroupUserCountKey),
|
||||||
|
@ -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]
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
@ -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
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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,6 +152,8 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension
|
||||||
shouldBeVisible: nil
|
shouldBeVisible: nil
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Notify the user if the call message wasn't already read
|
||||||
|
if !interaction.wasRead {
|
||||||
Environment.shared?.notificationsManager.wrappedValue?
|
Environment.shared?.notificationsManager.wrappedValue?
|
||||||
.notifyUser(
|
.notifyUser(
|
||||||
db,
|
db,
|
||||||
|
@ -152,6 +161,7 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension
|
||||||
in: thread
|
in: thread
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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 }
|
||||||
|
|
|
@ -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) } }
|
||||||
|
}
|
||||||
|
}
|
|
@ -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,18 +148,29 @@ public enum OnionRequestAPI: OnionRequestAPIType {
|
||||||
return existingBuildPathsPublisher
|
return existingBuildPathsPublisher
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return buildPathsPublisher.mutate { result in
|
||||||
|
/// It was possible for multiple threads to call this at the same time resulting in duplicate promises getting created, while
|
||||||
|
/// 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
|
||||||
|
if let previouslyBlockedPublisher: AnyPublisher<[[Snode]], Error> = result {
|
||||||
|
return previouslyBlockedPublisher
|
||||||
|
}
|
||||||
|
|
||||||
SNLog("Building onion request paths.")
|
SNLog("Building onion request paths.")
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
NotificationCenter.default.post(name: .buildingPaths, object: nil)
|
NotificationCenter.default.post(name: .buildingPaths, object: nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Need to include the post-request code and a `shareReplay` within the publisher otherwise it can still be executed
|
||||||
|
/// multiple times as a result of multiple subscribers
|
||||||
let reusableGuardSnodes = reusablePaths.map { $0[0] }
|
let reusableGuardSnodes = reusablePaths.map { $0[0] }
|
||||||
let publisher: AnyPublisher<[[Snode]], Error> = getGuardSnodes(reusing: reusableGuardSnodes)
|
let publisher: AnyPublisher<[[Snode]], Error> = getGuardSnodes(reusing: reusableGuardSnodes)
|
||||||
.flatMap { guardSnodes -> AnyPublisher<[[Snode]], Error> in
|
.flatMap { (guardSnodes: Set<Snode>) -> AnyPublisher<[[Snode]], Error> in
|
||||||
var unusedSnodes = SnodeAPI.snodePool.wrappedValue
|
var unusedSnodes: Set<Snode> = SnodeAPI.snodePool.wrappedValue
|
||||||
.subtracting(guardSnodes)
|
.subtracting(guardSnodes)
|
||||||
.subtracting(reusablePaths.flatMap { $0 })
|
.subtracting(reusablePaths.flatMap { $0 })
|
||||||
let reusableGuardSnodeCount = UInt(reusableGuardSnodes.count)
|
let reusableGuardSnodeCount: UInt = UInt(reusableGuardSnodes.count)
|
||||||
let pathSnodeCount = (targetGuardSnodeCount - reusableGuardSnodeCount) * pathSize - (targetGuardSnodeCount - reusableGuardSnodeCount)
|
let pathSnodeCount: UInt = (targetGuardSnodeCount - reusableGuardSnodeCount) * pathSize - (targetGuardSnodeCount - reusableGuardSnodeCount)
|
||||||
|
|
||||||
guard unusedSnodes.count >= pathSnodeCount else {
|
guard unusedSnodes.count >= pathSnodeCount else {
|
||||||
return Fail<[[Snode]], Error>(error: OnionRequestAPIError.insufficientSnodes)
|
return Fail<[[Snode]], Error>(error: OnionRequestAPIError.insufficientSnodes)
|
||||||
|
@ -173,21 +178,26 @@ public enum OnionRequestAPI: OnionRequestAPIType {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Don't test path snodes as this would reveal the user's IP to them
|
// Don't test path snodes as this would reveal the user's IP to them
|
||||||
return Just(
|
let paths: [[Snode]] = guardSnodes
|
||||||
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))
|
||||||
|
.map { _ in
|
||||||
|
// 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
|
unusedSnodes.remove(pathSnode) // All used snodes should be unique
|
||||||
return pathSnode
|
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)
|
.setFailureType(to: Error.self)
|
||||||
.eraseToAnyPublisher()
|
.eraseToAnyPublisher()
|
||||||
}
|
}
|
||||||
|
@ -206,12 +216,15 @@ public enum OnionRequestAPI: OnionRequestAPIType {
|
||||||
},
|
},
|
||||||
receiveCompletion: { _ in buildPathsPublisher.mutate { $0 = nil } }
|
receiveCompletion: { _ in buildPathsPublisher.mutate { $0 = nil } }
|
||||||
)
|
)
|
||||||
|
.shareReplay(1)
|
||||||
.eraseToAnyPublisher()
|
.eraseToAnyPublisher()
|
||||||
|
|
||||||
buildPathsPublisher.mutate { $0 = publisher }
|
/// Actually assign the atomic value
|
||||||
|
result = publisher
|
||||||
|
|
||||||
return 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.
|
||||||
internal static func getPath(excluding snode: Snode?) -> AnyPublisher<[Snode], Error> {
|
internal static func getPath(excluding snode: Snode?) -> AnyPublisher<[Snode], Error> {
|
||||||
|
@ -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
Loading…
Reference in New Issue