mirror of
https://github.com/oxen-io/session-ios.git
synced 2023-12-13 21:30:14 +01:00
A few bugs fixes and some optimisations
Fixed a bug where notifications could incorrectly appear for messages in the current thread Fixed a bug where swiping left/right on images in the MediaDetailViewController could load images from other threads Fixed a bug where the unread count wouldn't appear correctly when opening a conversation Fixed a bug where the unread count on the conversation cell could get truncated Fixed a bug where notifications weren't working correctly when the app is in the foreground Fixed a bug where we weren't clearing the 'received X new messages' count when in the foreground Fixed a bug where outgoing messages could get marked as read in a very specific case Updated the "group notification" logic to only apply to Open Groups (and always doing it rather than just in the background) Added a placeholder person icon when you have a closed group with a single member Added a couple of indexes to improve the HomeVC database query performance (reduce launch time by ~15% in some cases) Added a background task to give message sending the chance to complete when sending the app to the background Removed an unneeded query from the HomeViewModel init (reduce launch time by ~10% in some cases) Optimised one of the queries used to load the data needed for the conversation screen
This commit is contained in:
parent
c8bcd8fb33
commit
7097853d58
39 changed files with 691 additions and 299 deletions
|
@ -302,8 +302,6 @@
|
||||||
C32C5DD2256DD9E5003C73A2 /* LRUCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33FDAFD255A580600E217F9 /* LRUCache.swift */; };
|
C32C5DD2256DD9E5003C73A2 /* LRUCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33FDAFD255A580600E217F9 /* LRUCache.swift */; };
|
||||||
C32C5DDB256DD9FF003C73A2 /* ContentProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33FDB68255A580F00E217F9 /* ContentProxy.swift */; };
|
C32C5DDB256DD9FF003C73A2 /* ContentProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33FDB68255A580F00E217F9 /* ContentProxy.swift */; };
|
||||||
C32C5E0C256DDAFA003C73A2 /* NSRegularExpression+SSK.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33FDA7A255A57FB00E217F9 /* NSRegularExpression+SSK.swift */; };
|
C32C5E0C256DDAFA003C73A2 /* NSRegularExpression+SSK.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33FDA7A255A57FB00E217F9 /* NSRegularExpression+SSK.swift */; };
|
||||||
C32C5FBB256E0206003C73A2 /* OWSBackgroundTask.m in Sources */ = {isa = PBXBuildFile; fileRef = C33FDC1B255A581F00E217F9 /* OWSBackgroundTask.m */; };
|
|
||||||
C32C5FC4256E0209003C73A2 /* OWSBackgroundTask.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDB38255A580B00E217F9 /* OWSBackgroundTask.h */; settings = {ATTRIBUTES = (Public, ); }; };
|
|
||||||
C32C600F256E07F5003C73A2 /* NSUserDefaults+OWS.m in Sources */ = {isa = PBXBuildFile; fileRef = C33FDB77255A581000E217F9 /* NSUserDefaults+OWS.m */; };
|
C32C600F256E07F5003C73A2 /* NSUserDefaults+OWS.m in Sources */ = {isa = PBXBuildFile; fileRef = C33FDB77255A581000E217F9 /* NSUserDefaults+OWS.m */; };
|
||||||
C32C6018256E07F9003C73A2 /* NSUserDefaults+OWS.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDB51255A580D00E217F9 /* NSUserDefaults+OWS.h */; settings = {ATTRIBUTES = (Public, ); }; };
|
C32C6018256E07F9003C73A2 /* NSUserDefaults+OWS.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDB51255A580D00E217F9 /* NSUserDefaults+OWS.h */; settings = {ATTRIBUTES = (Public, ); }; };
|
||||||
C33100082558FF6D00070591 /* NewConversationButtonSet.swift in Sources */ = {isa = PBXBuildFile; fileRef = B83F2B85240C7B8F000A54AB /* NewConversationButtonSet.swift */; };
|
C33100082558FF6D00070591 /* NewConversationButtonSet.swift in Sources */ = {isa = PBXBuildFile; fileRef = B83F2B85240C7B8F000A54AB /* NewConversationButtonSet.swift */; };
|
||||||
|
@ -648,6 +646,7 @@
|
||||||
FD37EA0F28AB3330003AE748 /* _006_FixHiddenModAdminSupport.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD37EA0E28AB3330003AE748 /* _006_FixHiddenModAdminSupport.swift */; };
|
FD37EA0F28AB3330003AE748 /* _006_FixHiddenModAdminSupport.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD37EA0E28AB3330003AE748 /* _006_FixHiddenModAdminSupport.swift */; };
|
||||||
FD37EA1128AB34B3003AE748 /* TypedTableAlteration.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD37EA1028AB34B3003AE748 /* TypedTableAlteration.swift */; };
|
FD37EA1128AB34B3003AE748 /* TypedTableAlteration.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD37EA1028AB34B3003AE748 /* TypedTableAlteration.swift */; };
|
||||||
FD37EA1528AB42CB003AE748 /* IdentitySpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD37EA1428AB42CB003AE748 /* IdentitySpec.swift */; };
|
FD37EA1528AB42CB003AE748 /* IdentitySpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD37EA1428AB42CB003AE748 /* IdentitySpec.swift */; };
|
||||||
|
FD37EA1B28ACB51F003AE748 /* _007_HomeQueryOptimisationIndexes.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD37EA1A28ACB51F003AE748 /* _007_HomeQueryOptimisationIndexes.swift */; };
|
||||||
FD3AABE928306BBD00E5099A /* ThreadPickerViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD3AABE828306BBD00E5099A /* ThreadPickerViewModel.swift */; };
|
FD3AABE928306BBD00E5099A /* ThreadPickerViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD3AABE828306BBD00E5099A /* ThreadPickerViewModel.swift */; };
|
||||||
FD3C905C27E3FBEF00CD579F /* BatchRequestInfoSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD3C905B27E3FBEF00CD579F /* BatchRequestInfoSpec.swift */; };
|
FD3C905C27E3FBEF00CD579F /* BatchRequestInfoSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD3C905B27E3FBEF00CD579F /* BatchRequestInfoSpec.swift */; };
|
||||||
FD3C906027E410F700CD579F /* FileUploadResponseSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD3C905F27E410F700CD579F /* FileUploadResponseSpec.swift */; };
|
FD3C906027E410F700CD579F /* FileUploadResponseSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD3C905F27E410F700CD579F /* FileUploadResponseSpec.swift */; };
|
||||||
|
@ -661,6 +660,8 @@
|
||||||
FD3E0C84283B5835002A425C /* SessionThreadViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD3E0C83283B5835002A425C /* SessionThreadViewModel.swift */; };
|
FD3E0C84283B5835002A425C /* SessionThreadViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD3E0C83283B5835002A425C /* SessionThreadViewModel.swift */; };
|
||||||
FD42F9A8285064B800A0C77D /* PushNotificationAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33FDBDE255A581900E217F9 /* PushNotificationAPI.swift */; };
|
FD42F9A8285064B800A0C77D /* PushNotificationAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33FDBDE255A581900E217F9 /* PushNotificationAPI.swift */; };
|
||||||
FD4B200E283492210034334B /* InsetLockableTableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD4B200D283492210034334B /* InsetLockableTableView.swift */; };
|
FD4B200E283492210034334B /* InsetLockableTableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD4B200D283492210034334B /* InsetLockableTableView.swift */; };
|
||||||
|
FD52090028AF6153006098F6 /* OWSBackgroundTask.m in Sources */ = {isa = PBXBuildFile; fileRef = C33FDC1B255A581F00E217F9 /* OWSBackgroundTask.m */; };
|
||||||
|
FD52090128AF61BA006098F6 /* OWSBackgroundTask.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDB38255A580B00E217F9 /* OWSBackgroundTask.h */; settings = {ATTRIBUTES = (Public, ); }; };
|
||||||
FD5C72F7284F0E560029977D /* MessageReceiver+ReadReceipts.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD5C72F6284F0E560029977D /* MessageReceiver+ReadReceipts.swift */; };
|
FD5C72F7284F0E560029977D /* MessageReceiver+ReadReceipts.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD5C72F6284F0E560029977D /* MessageReceiver+ReadReceipts.swift */; };
|
||||||
FD5C72F9284F0E880029977D /* MessageReceiver+TypingIndicators.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD5C72F8284F0E880029977D /* MessageReceiver+TypingIndicators.swift */; };
|
FD5C72F9284F0E880029977D /* MessageReceiver+TypingIndicators.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD5C72F8284F0E880029977D /* MessageReceiver+TypingIndicators.swift */; };
|
||||||
FD5C72FB284F0EA10029977D /* MessageReceiver+DataExtractionNotification.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD5C72FA284F0EA10029977D /* MessageReceiver+DataExtractionNotification.swift */; };
|
FD5C72FB284F0EA10029977D /* MessageReceiver+DataExtractionNotification.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD5C72FA284F0EA10029977D /* MessageReceiver+DataExtractionNotification.swift */; };
|
||||||
|
@ -780,7 +781,6 @@
|
||||||
FDCDB8DE2810F73B00352A0C /* Differentiable+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDCDB8DD2810F73B00352A0C /* Differentiable+Utilities.swift */; };
|
FDCDB8DE2810F73B00352A0C /* Differentiable+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDCDB8DD2810F73B00352A0C /* Differentiable+Utilities.swift */; };
|
||||||
FDCDB8E02811007F00352A0C /* HomeViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDCDB8DF2811007F00352A0C /* HomeViewModel.swift */; };
|
FDCDB8E02811007F00352A0C /* HomeViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDCDB8DF2811007F00352A0C /* HomeViewModel.swift */; };
|
||||||
FDCDB8E42817819600352A0C /* (null) in Sources */ = {isa = PBXBuildFile; };
|
FDCDB8E42817819600352A0C /* (null) in Sources */ = {isa = PBXBuildFile; };
|
||||||
FDCDB8F12817ABE600352A0C /* Optional+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDCDB8F02817ABE600352A0C /* Optional+Utilities.swift */; };
|
|
||||||
FDD2506E283711D600198BDA /* DifferenceKit+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDD2506D283711D600198BDA /* DifferenceKit+Utilities.swift */; };
|
FDD2506E283711D600198BDA /* DifferenceKit+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDD2506D283711D600198BDA /* DifferenceKit+Utilities.swift */; };
|
||||||
FDD250702837199200198BDA /* GarbageCollectionJob.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDD2506F2837199200198BDA /* GarbageCollectionJob.swift */; };
|
FDD250702837199200198BDA /* GarbageCollectionJob.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDD2506F2837199200198BDA /* GarbageCollectionJob.swift */; };
|
||||||
FDD250722837234B00198BDA /* MediaGalleryNavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDD250712837234B00198BDA /* MediaGalleryNavigationController.swift */; };
|
FDD250722837234B00198BDA /* MediaGalleryNavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDD250712837234B00198BDA /* MediaGalleryNavigationController.swift */; };
|
||||||
|
@ -1691,6 +1691,7 @@
|
||||||
FD37EA0E28AB3330003AE748 /* _006_FixHiddenModAdminSupport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _006_FixHiddenModAdminSupport.swift; sourceTree = "<group>"; };
|
FD37EA0E28AB3330003AE748 /* _006_FixHiddenModAdminSupport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _006_FixHiddenModAdminSupport.swift; sourceTree = "<group>"; };
|
||||||
FD37EA1028AB34B3003AE748 /* TypedTableAlteration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TypedTableAlteration.swift; sourceTree = "<group>"; };
|
FD37EA1028AB34B3003AE748 /* TypedTableAlteration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TypedTableAlteration.swift; sourceTree = "<group>"; };
|
||||||
FD37EA1428AB42CB003AE748 /* IdentitySpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IdentitySpec.swift; sourceTree = "<group>"; };
|
FD37EA1428AB42CB003AE748 /* IdentitySpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IdentitySpec.swift; sourceTree = "<group>"; };
|
||||||
|
FD37EA1A28ACB51F003AE748 /* _007_HomeQueryOptimisationIndexes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _007_HomeQueryOptimisationIndexes.swift; sourceTree = "<group>"; };
|
||||||
FD3AABE828306BBD00E5099A /* ThreadPickerViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadPickerViewModel.swift; sourceTree = "<group>"; };
|
FD3AABE828306BBD00E5099A /* ThreadPickerViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadPickerViewModel.swift; sourceTree = "<group>"; };
|
||||||
FD3C905B27E3FBEF00CD579F /* BatchRequestInfoSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BatchRequestInfoSpec.swift; sourceTree = "<group>"; };
|
FD3C905B27E3FBEF00CD579F /* BatchRequestInfoSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BatchRequestInfoSpec.swift; sourceTree = "<group>"; };
|
||||||
FD3C905F27E410F700CD579F /* FileUploadResponseSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileUploadResponseSpec.swift; sourceTree = "<group>"; };
|
FD3C905F27E410F700CD579F /* FileUploadResponseSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileUploadResponseSpec.swift; sourceTree = "<group>"; };
|
||||||
|
@ -1816,7 +1817,6 @@
|
||||||
FDC6D75F2862B3F600B04575 /* Dependencies.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Dependencies.swift; sourceTree = "<group>"; };
|
FDC6D75F2862B3F600B04575 /* Dependencies.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Dependencies.swift; sourceTree = "<group>"; };
|
||||||
FDCDB8DD2810F73B00352A0C /* Differentiable+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Differentiable+Utilities.swift"; sourceTree = "<group>"; };
|
FDCDB8DD2810F73B00352A0C /* Differentiable+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Differentiable+Utilities.swift"; sourceTree = "<group>"; };
|
||||||
FDCDB8DF2811007F00352A0C /* HomeViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeViewModel.swift; sourceTree = "<group>"; };
|
FDCDB8DF2811007F00352A0C /* HomeViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeViewModel.swift; sourceTree = "<group>"; };
|
||||||
FDCDB8F02817ABE600352A0C /* Optional+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Optional+Utilities.swift"; sourceTree = "<group>"; };
|
|
||||||
FDD2506D283711D600198BDA /* DifferenceKit+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DifferenceKit+Utilities.swift"; sourceTree = "<group>"; };
|
FDD2506D283711D600198BDA /* DifferenceKit+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DifferenceKit+Utilities.swift"; sourceTree = "<group>"; };
|
||||||
FDD2506F2837199200198BDA /* GarbageCollectionJob.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GarbageCollectionJob.swift; sourceTree = "<group>"; };
|
FDD2506F2837199200198BDA /* GarbageCollectionJob.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GarbageCollectionJob.swift; sourceTree = "<group>"; };
|
||||||
FDD250712837234B00198BDA /* MediaGalleryNavigationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaGalleryNavigationController.swift; sourceTree = "<group>"; };
|
FDD250712837234B00198BDA /* MediaGalleryNavigationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaGalleryNavigationController.swift; sourceTree = "<group>"; };
|
||||||
|
@ -3057,8 +3057,6 @@
|
||||||
C38EF2F5255B6DBC007E1867 /* OWSAudioPlayer.h */,
|
C38EF2F5255B6DBC007E1867 /* OWSAudioPlayer.h */,
|
||||||
C38EF2F7255B6DBC007E1867 /* OWSAudioPlayer.m */,
|
C38EF2F7255B6DBC007E1867 /* OWSAudioPlayer.m */,
|
||||||
C38EF281255B6D84007E1867 /* OWSAudioSession.swift */,
|
C38EF281255B6D84007E1867 /* OWSAudioSession.swift */,
|
||||||
C33FDB38255A580B00E217F9 /* OWSBackgroundTask.h */,
|
|
||||||
C33FDC1B255A581F00E217F9 /* OWSBackgroundTask.m */,
|
|
||||||
FDF0B75D280AAF35004C14C5 /* Preferences.swift */,
|
FDF0B75D280AAF35004C14C5 /* Preferences.swift */,
|
||||||
C38EF2FB255B6DBD007E1867 /* OWSWindowManager.h */,
|
C38EF2FB255B6DBD007E1867 /* OWSWindowManager.h */,
|
||||||
C38EF306255B6DBE007E1867 /* OWSWindowManager.m */,
|
C38EF306255B6DBE007E1867 /* OWSWindowManager.m */,
|
||||||
|
@ -3126,7 +3124,6 @@
|
||||||
B8A582AE258C65D000AFD84C /* Networking */,
|
B8A582AE258C65D000AFD84C /* Networking */,
|
||||||
B8A582AD258C655E00AFD84C /* PromiseKit */,
|
B8A582AD258C655E00AFD84C /* PromiseKit */,
|
||||||
FD09796527F6B0A800936362 /* Utilities */,
|
FD09796527F6B0A800936362 /* Utilities */,
|
||||||
FDCDB8EF2817ABCE00352A0C /* Utilities */,
|
|
||||||
C3D9E43025676D3D0040E4F3 /* Configuration.swift */,
|
C3D9E43025676D3D0040E4F3 /* Configuration.swift */,
|
||||||
);
|
);
|
||||||
path = SessionUtilitiesKit;
|
path = SessionUtilitiesKit;
|
||||||
|
@ -3426,6 +3423,8 @@
|
||||||
FD09797127FAA2F500936362 /* Optional+Utilities.swift */,
|
FD09797127FAA2F500936362 /* Optional+Utilities.swift */,
|
||||||
FD09797C27FBDB2000936362 /* Notification+Utilities.swift */,
|
FD09797C27FBDB2000936362 /* Notification+Utilities.swift */,
|
||||||
FDF222082818D2B0000A4995 /* NSAttributedString+Utilities.swift */,
|
FDF222082818D2B0000A4995 /* NSAttributedString+Utilities.swift */,
|
||||||
|
C33FDB38255A580B00E217F9 /* OWSBackgroundTask.h */,
|
||||||
|
C33FDC1B255A581F00E217F9 /* OWSBackgroundTask.m */,
|
||||||
);
|
);
|
||||||
path = Utilities;
|
path = Utilities;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -3464,6 +3463,7 @@
|
||||||
FDF40CDD2897A1BC006A0CC4 /* _004_RemoveLegacyYDB.swift */,
|
FDF40CDD2897A1BC006A0CC4 /* _004_RemoveLegacyYDB.swift */,
|
||||||
FD37EA0C28AB2A45003AE748 /* _005_FixDeletedMessageReadState.swift */,
|
FD37EA0C28AB2A45003AE748 /* _005_FixDeletedMessageReadState.swift */,
|
||||||
FD37EA0E28AB3330003AE748 /* _006_FixHiddenModAdminSupport.swift */,
|
FD37EA0E28AB3330003AE748 /* _006_FixHiddenModAdminSupport.swift */,
|
||||||
|
FD37EA1A28ACB51F003AE748 /* _007_HomeQueryOptimisationIndexes.swift */,
|
||||||
);
|
);
|
||||||
path = Migrations;
|
path = Migrations;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -3884,14 +3884,6 @@
|
||||||
path = Models;
|
path = Models;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
FDCDB8EF2817ABCE00352A0C /* Utilities */ = {
|
|
||||||
isa = PBXGroup;
|
|
||||||
children = (
|
|
||||||
FDCDB8F02817ABE600352A0C /* Optional+Utilities.swift */,
|
|
||||||
);
|
|
||||||
path = Utilities;
|
|
||||||
sourceTree = "<group>";
|
|
||||||
};
|
|
||||||
FDE7214E287E50D50093DF33 /* Scripts */ = {
|
FDE7214E287E50D50093DF33 /* Scripts */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
@ -4006,6 +3998,7 @@
|
||||||
C3C2A67D255388CC00C340D1 /* SessionUtilitiesKit.h in Headers */,
|
C3C2A67D255388CC00C340D1 /* SessionUtilitiesKit.h in Headers */,
|
||||||
C32C6018256E07F9003C73A2 /* NSUserDefaults+OWS.h in Headers */,
|
C32C6018256E07F9003C73A2 /* NSUserDefaults+OWS.h in Headers */,
|
||||||
B8856D8D256F1502001CE70E /* UIView+OWS.h in Headers */,
|
B8856D8D256F1502001CE70E /* UIView+OWS.h in Headers */,
|
||||||
|
FD52090128AF61BA006098F6 /* OWSBackgroundTask.h in Headers */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
|
@ -4015,7 +4008,6 @@
|
||||||
files = (
|
files = (
|
||||||
C3C2A6F425539DE700C340D1 /* SessionMessagingKit.h in Headers */,
|
C3C2A6F425539DE700C340D1 /* SessionMessagingKit.h in Headers */,
|
||||||
C32C5C46256DCBB2003C73A2 /* AppReadiness.h in Headers */,
|
C32C5C46256DCBB2003C73A2 /* AppReadiness.h in Headers */,
|
||||||
C32C5FC4256E0209003C73A2 /* OWSBackgroundTask.h in Headers */,
|
|
||||||
FD716E732850647900C96BF4 /* NSData+messagePadding.h in Headers */,
|
FD716E732850647900C96BF4 /* NSData+messagePadding.h in Headers */,
|
||||||
B8856D72256F1421001CE70E /* OWSWindowManager.h in Headers */,
|
B8856D72256F1421001CE70E /* OWSWindowManager.h in Headers */,
|
||||||
B8856CF7256F105E001CE70E /* OWSAudioPlayer.h in Headers */,
|
B8856CF7256F105E001CE70E /* OWSAudioPlayer.h in Headers */,
|
||||||
|
@ -5022,7 +5014,6 @@
|
||||||
FDA8EB10280F8238002B68E5 /* Codable+Utilities.swift in Sources */,
|
FDA8EB10280F8238002B68E5 /* Codable+Utilities.swift in Sources */,
|
||||||
C352A36D2557858E00338F3E /* NSTimer+Proxying.m in Sources */,
|
C352A36D2557858E00338F3E /* NSTimer+Proxying.m in Sources */,
|
||||||
FD09797B27FBB25900936362 /* Updatable.swift in Sources */,
|
FD09797B27FBB25900936362 /* Updatable.swift in Sources */,
|
||||||
FDCDB8F12817ABE600352A0C /* Optional+Utilities.swift in Sources */,
|
|
||||||
7B7CB192271508AD0079FF93 /* CallRingTonePlayer.swift in Sources */,
|
7B7CB192271508AD0079FF93 /* CallRingTonePlayer.swift in Sources */,
|
||||||
C3C2ABD22553C6C900C340D1 /* Data+SecureRandom.swift in Sources */,
|
C3C2ABD22553C6C900C340D1 /* Data+SecureRandom.swift in Sources */,
|
||||||
FD848B8B283DC509000E298B /* PagedDatabaseObserver.swift in Sources */,
|
FD848B8B283DC509000E298B /* PagedDatabaseObserver.swift in Sources */,
|
||||||
|
@ -5068,6 +5059,7 @@
|
||||||
FD17D7B827F51ECA00122BE0 /* Migration.swift in Sources */,
|
FD17D7B827F51ECA00122BE0 /* Migration.swift in Sources */,
|
||||||
FD7728982849E8110018502F /* UITableView+ReusableView.swift in Sources */,
|
FD7728982849E8110018502F /* UITableView+ReusableView.swift in Sources */,
|
||||||
7B0EFDEE274F598600FFAAE7 /* TimestampUtils.swift in Sources */,
|
7B0EFDEE274F598600FFAAE7 /* TimestampUtils.swift in Sources */,
|
||||||
|
FD52090028AF6153006098F6 /* OWSBackgroundTask.m in Sources */,
|
||||||
C32C5DDB256DD9FF003C73A2 /* ContentProxy.swift in Sources */,
|
C32C5DDB256DD9FF003C73A2 /* ContentProxy.swift in Sources */,
|
||||||
C3A71F892558BA9F0043A11F /* Mnemonic.swift in Sources */,
|
C3A71F892558BA9F0043A11F /* Mnemonic.swift in Sources */,
|
||||||
B8F5F58325EC94A6003BF8D4 /* Collection+Subscripting.swift in Sources */,
|
B8F5F58325EC94A6003BF8D4 /* Collection+Subscripting.swift in Sources */,
|
||||||
|
@ -5114,6 +5106,7 @@
|
||||||
files = (
|
files = (
|
||||||
FD86585828507B24008B6CF9 /* NSData+messagePadding.m in Sources */,
|
FD86585828507B24008B6CF9 /* NSData+messagePadding.m in Sources */,
|
||||||
FD245C52285065D500B966DD /* SignalAttachment.swift in Sources */,
|
FD245C52285065D500B966DD /* SignalAttachment.swift in Sources */,
|
||||||
|
FD37EA1B28ACB51F003AE748 /* _007_HomeQueryOptimisationIndexes.swift in Sources */,
|
||||||
B8856D08256F10F1001CE70E /* DeviceSleepManager.swift in Sources */,
|
B8856D08256F10F1001CE70E /* DeviceSleepManager.swift in Sources */,
|
||||||
C3471F4C25553AB000297E91 /* MessageReceiver+Decryption.swift in Sources */,
|
C3471F4C25553AB000297E91 /* MessageReceiver+Decryption.swift in Sources */,
|
||||||
FD245C672850665E00B966DD /* AttachmentDownloadJob.swift in Sources */,
|
FD245C672850665E00B966DD /* AttachmentDownloadJob.swift in Sources */,
|
||||||
|
@ -5241,7 +5234,6 @@
|
||||||
FD245C5C2850660A00B966DD /* ConfigurationMessage.swift in Sources */,
|
FD245C5C2850660A00B966DD /* ConfigurationMessage.swift in Sources */,
|
||||||
FD5C7301284F0F7A0029977D /* MessageReceiver+UnsendRequests.swift in Sources */,
|
FD5C7301284F0F7A0029977D /* MessageReceiver+UnsendRequests.swift in Sources */,
|
||||||
C3C2A75F2553A3C500C340D1 /* VisibleMessage+LinkPreview.swift in Sources */,
|
C3C2A75F2553A3C500C340D1 /* VisibleMessage+LinkPreview.swift in Sources */,
|
||||||
C32C5FBB256E0206003C73A2 /* OWSBackgroundTask.m in Sources */,
|
|
||||||
FD245C642850664F00B966DD /* Threading.swift in Sources */,
|
FD245C642850664F00B966DD /* Threading.swift in Sources */,
|
||||||
FD848B8D283E0B26000E298B /* MessageInputTypes.swift in Sources */,
|
FD848B8D283E0B26000E298B /* MessageInputTypes.swift in Sources */,
|
||||||
C32C5C3D256DCBAF003C73A2 /* AppReadiness.m in Sources */,
|
C32C5C3D256DCBAF003C73A2 /* AppReadiness.m in Sources */,
|
||||||
|
|
|
@ -688,6 +688,17 @@ final class ConversationVC: BaseVC, OWSConversationSettingsViewDelegate, Convers
|
||||||
let wasLoadingMore: Bool = self.isLoadingMore
|
let wasLoadingMore: Bool = self.isLoadingMore
|
||||||
let wasOffsetCloseToBottom: Bool = self.isCloseToBottom
|
let wasOffsetCloseToBottom: Bool = self.isCloseToBottom
|
||||||
let numItemsInUpdatedData: [Int] = updatedData.map { $0.elements.count }
|
let numItemsInUpdatedData: [Int] = updatedData.map { $0.elements.count }
|
||||||
|
let didSwapAllContent: Bool = (updatedData
|
||||||
|
.first(where: { $0.model == .messages })?
|
||||||
|
.elements
|
||||||
|
.contains(where: {
|
||||||
|
$0.id == self.viewModel.interactionData
|
||||||
|
.first(where: { $0.model == .messages })?
|
||||||
|
.elements
|
||||||
|
.first?
|
||||||
|
.id
|
||||||
|
}))
|
||||||
|
.defaulting(to: false)
|
||||||
let itemChangeInfo: ItemChangeInfo? = {
|
let itemChangeInfo: ItemChangeInfo? = {
|
||||||
guard
|
guard
|
||||||
isInsert,
|
isInsert,
|
||||||
|
@ -720,7 +731,7 @@ final class ConversationVC: BaseVC, OWSConversationSettingsViewDelegate, Convers
|
||||||
)
|
)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
guard !isInsert || wasLoadingMore || itemChangeInfo?.isInsertAtTop == true else {
|
guard !isInsert || itemChangeInfo?.isInsertAtTop == true else {
|
||||||
self.viewModel.updateInteractionData(updatedData)
|
self.viewModel.updateInteractionData(updatedData)
|
||||||
self.tableView.reloadData()
|
self.tableView.reloadData()
|
||||||
|
|
||||||
|
@ -729,16 +740,27 @@ final class ConversationVC: BaseVC, OWSConversationSettingsViewDelegate, Convers
|
||||||
if let focusedInteractionId: Int64 = self.focusedInteractionId {
|
if let focusedInteractionId: Int64 = self.focusedInteractionId {
|
||||||
// If we had a focusedInteractionId then scroll to it (and hide the search
|
// If we had a focusedInteractionId then scroll to it (and hide the search
|
||||||
// result bar loading indicator)
|
// result bar loading indicator)
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(100)) { [weak self] in
|
let delay: DispatchTime = (didSwapAllContent ?
|
||||||
|
.now() :
|
||||||
|
(.now() + .milliseconds(100))
|
||||||
|
)
|
||||||
|
|
||||||
|
DispatchQueue.main.asyncAfter(deadline: delay) { [weak self] in
|
||||||
self?.searchController.resultsBar.stopLoading()
|
self?.searchController.resultsBar.stopLoading()
|
||||||
self?.scrollToInteractionIfNeeded(
|
self?.scrollToInteractionIfNeeded(
|
||||||
with: focusedInteractionId,
|
with: focusedInteractionId,
|
||||||
isAnimated: true,
|
isAnimated: true,
|
||||||
highlight: (self?.shouldHighlightNextScrollToInteraction == true)
|
highlight: (self?.shouldHighlightNextScrollToInteraction == true)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if wasLoadingMore {
|
||||||
|
// Complete page loading
|
||||||
|
self?.isLoadingMore = false
|
||||||
|
self?.autoLoadNextPageIfNeeded()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if wasOffsetCloseToBottom {
|
}
|
||||||
|
else if wasOffsetCloseToBottom && !wasLoadingMore {
|
||||||
// Scroll to the bottom if an interaction was just inserted and we either
|
// Scroll to the bottom if an interaction was just inserted and we either
|
||||||
// just sent a message or are close enough to the bottom (wait a tiny fraction
|
// just sent a message or are close enough to the bottom (wait a tiny fraction
|
||||||
// to avoid buggy animation behaviour)
|
// to avoid buggy animation behaviour)
|
||||||
|
@ -746,6 +768,11 @@ final class ConversationVC: BaseVC, OWSConversationSettingsViewDelegate, Convers
|
||||||
self?.scrollToBottom(isAnimated: true)
|
self?.scrollToBottom(isAnimated: true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
else if wasLoadingMore {
|
||||||
|
// Complete page loading
|
||||||
|
self.isLoadingMore = false
|
||||||
|
self.autoLoadNextPageIfNeeded()
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -755,7 +782,7 @@ final class ConversationVC: BaseVC, OWSConversationSettingsViewDelegate, Convers
|
||||||
///
|
///
|
||||||
/// Unfortunately the UITableView also does some weird things when updating (where it won't have updated it's internal data until
|
/// Unfortunately the UITableView also does some weird things when updating (where it won't have updated it's internal data until
|
||||||
/// after it performs the next layout); the below code checks a condition on layout and if it passes it calls a closure
|
/// after it performs the next layout); the below code checks a condition on layout and if it passes it calls a closure
|
||||||
if let itemChangeInfo: ItemChangeInfo = itemChangeInfo, (itemChangeInfo.isInsertAtTop || wasLoadingMore) {
|
if let itemChangeInfo: ItemChangeInfo = itemChangeInfo, itemChangeInfo.isInsertAtTop {
|
||||||
let oldCellHeight: CGFloat = self.tableView.rectForRow(at: itemChangeInfo.oldVisibleIndexPath).height
|
let oldCellHeight: CGFloat = self.tableView.rectForRow(at: itemChangeInfo.oldVisibleIndexPath).height
|
||||||
|
|
||||||
// The the user triggered the 'scrollToTop' animation (by tapping in the nav bar) then we
|
// The the user triggered the 'scrollToTop' animation (by tapping in the nav bar) then we
|
||||||
|
@ -812,6 +839,29 @@ final class ConversationVC: BaseVC, OWSConversationSettingsViewDelegate, Convers
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
else if wasLoadingMore {
|
||||||
|
if let focusedInteractionId: Int64 = self.focusedInteractionId {
|
||||||
|
DispatchQueue.main.async { [weak self] in
|
||||||
|
// If we had a focusedInteractionId then scroll to it (and hide the search
|
||||||
|
// result bar loading indicator)
|
||||||
|
self?.searchController.resultsBar.stopLoading()
|
||||||
|
self?.scrollToInteractionIfNeeded(
|
||||||
|
with: focusedInteractionId,
|
||||||
|
isAnimated: true,
|
||||||
|
highlight: (self?.shouldHighlightNextScrollToInteraction == true)
|
||||||
|
)
|
||||||
|
|
||||||
|
// Complete page loading
|
||||||
|
self?.isLoadingMore = false
|
||||||
|
self?.autoLoadNextPageIfNeeded()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Complete page loading
|
||||||
|
self.isLoadingMore = false
|
||||||
|
self.autoLoadNextPageIfNeeded()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
self.tableView.reload(
|
self.tableView.reload(
|
||||||
using: changeset,
|
using: changeset,
|
||||||
|
@ -837,13 +887,13 @@ final class ConversationVC: BaseVC, OWSConversationSettingsViewDelegate, Convers
|
||||||
// the screen will scroll to the bottom instead of the first unread message
|
// the screen will scroll to the bottom instead of the first unread message
|
||||||
if let focusedInteractionId: Int64 = self.viewModel.focusedInteractionId {
|
if let focusedInteractionId: Int64 = self.viewModel.focusedInteractionId {
|
||||||
self.scrollToInteractionIfNeeded(with: focusedInteractionId, isAnimated: false, highlight: true)
|
self.scrollToInteractionIfNeeded(with: focusedInteractionId, isAnimated: false, highlight: true)
|
||||||
self.unreadCountView.alpha = self.scrollButton.alpha
|
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
self.scrollToBottom(isAnimated: false)
|
self.scrollToBottom(isAnimated: false)
|
||||||
}
|
}
|
||||||
|
|
||||||
self.scrollButton.alpha = self.getScrollButtonOpacity()
|
self.scrollButton.alpha = self.getScrollButtonOpacity()
|
||||||
|
self.unreadCountView.alpha = self.scrollButton.alpha
|
||||||
self.hasPerformedInitialScroll = true
|
self.hasPerformedInitialScroll = true
|
||||||
|
|
||||||
// Now that the data has loaded we need to check if either of the "load more" sections are
|
// Now that the data has loaded we need to check if either of the "load more" sections are
|
||||||
|
@ -1018,6 +1068,7 @@ final class ConversationVC: BaseVC, OWSConversationSettingsViewDelegate, Convers
|
||||||
|
|
||||||
let scrollButtonOpacity: CGFloat = (self?.getScrollButtonOpacity() ?? 0)
|
let scrollButtonOpacity: CGFloat = (self?.getScrollButtonOpacity() ?? 0)
|
||||||
self?.scrollButton.alpha = scrollButtonOpacity
|
self?.scrollButton.alpha = scrollButtonOpacity
|
||||||
|
self?.unreadCountView.alpha = scrollButtonOpacity
|
||||||
|
|
||||||
self?.view.setNeedsLayout()
|
self?.view.setNeedsLayout()
|
||||||
self?.view.layoutIfNeeded()
|
self?.view.layoutIfNeeded()
|
||||||
|
@ -1225,6 +1276,7 @@ final class ConversationVC: BaseVC, OWSConversationSettingsViewDelegate, Convers
|
||||||
self.scrollToInteractionIfNeeded(
|
self.scrollToInteractionIfNeeded(
|
||||||
with: lastInteractionId,
|
with: lastInteractionId,
|
||||||
position: .bottom,
|
position: .bottom,
|
||||||
|
isJumpingToLastInteraction: true,
|
||||||
isAnimated: true
|
isAnimated: true
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
@ -1283,7 +1335,7 @@ final class ConversationVC: BaseVC, OWSConversationSettingsViewDelegate, Convers
|
||||||
let contentOffsetY = tableView.contentOffset.y
|
let contentOffsetY = tableView.contentOffset.y
|
||||||
let x = (lastPageTop - ConversationVC.bottomInset - contentOffsetY).clamp(0, .greatestFiniteMagnitude)
|
let x = (lastPageTop - ConversationVC.bottomInset - contentOffsetY).clamp(0, .greatestFiniteMagnitude)
|
||||||
let a = 1 / (ConversationVC.scrollButtonFullVisibilityThreshold - ConversationVC.scrollButtonNoVisibilityThreshold)
|
let a = 1 / (ConversationVC.scrollButtonFullVisibilityThreshold - ConversationVC.scrollButtonNoVisibilityThreshold)
|
||||||
return a * x
|
return max(0, min(1, a * x))
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Search
|
// MARK: - Search
|
||||||
|
@ -1394,6 +1446,7 @@ final class ConversationVC: BaseVC, OWSConversationSettingsViewDelegate, Convers
|
||||||
func scrollToInteractionIfNeeded(
|
func scrollToInteractionIfNeeded(
|
||||||
with interactionId: Int64,
|
with interactionId: Int64,
|
||||||
position: UITableView.ScrollPosition = .middle,
|
position: UITableView.ScrollPosition = .middle,
|
||||||
|
isJumpingToLastInteraction: Bool = false,
|
||||||
isAnimated: Bool = true,
|
isAnimated: Bool = true,
|
||||||
highlight: Bool = false
|
highlight: Bool = false
|
||||||
) {
|
) {
|
||||||
|
@ -1417,11 +1470,19 @@ final class ConversationVC: BaseVC, OWSConversationSettingsViewDelegate, Convers
|
||||||
self.searchController.resultsBar.startLoading()
|
self.searchController.resultsBar.startLoading()
|
||||||
|
|
||||||
DispatchQueue.global(qos: .default).async { [weak self] in
|
DispatchQueue.global(qos: .default).async { [weak self] in
|
||||||
|
if isJumpingToLastInteraction {
|
||||||
|
self?.viewModel.pagedDataObserver?.load(.jumpTo(
|
||||||
|
id: interactionId,
|
||||||
|
paddingForInclusive: 5
|
||||||
|
))
|
||||||
|
}
|
||||||
|
else {
|
||||||
self?.viewModel.pagedDataObserver?.load(.untilInclusive(
|
self?.viewModel.pagedDataObserver?.load(.untilInclusive(
|
||||||
id: interactionId,
|
id: interactionId,
|
||||||
padding: 5
|
padding: 5
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -214,9 +214,7 @@ final class HomeVC: BaseVC, UITableViewDataSource, UITableViewDelegate, NewConve
|
||||||
}
|
}
|
||||||
|
|
||||||
// Onion request path countries cache
|
// Onion request path countries cache
|
||||||
DispatchQueue.global(qos: .utility).sync {
|
IP2Country.shared.populateCacheIfNeededAsync()
|
||||||
let _ = IP2Country.shared.populateCacheIfNeeded()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override func viewWillAppear(_ animated: Bool) {
|
override func viewWillAppear(_ animated: Bool) {
|
||||||
|
|
|
@ -43,8 +43,7 @@ public class HomeViewModel {
|
||||||
// MARK: - Initialization
|
// MARK: - Initialization
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
self.state = Storage.shared.read { db in try HomeViewModel.retrieveState(db) }
|
self.state = State()
|
||||||
.defaulting(to: State())
|
|
||||||
self.pagedDataObserver = nil
|
self.pagedDataObserver = nil
|
||||||
|
|
||||||
// Note: Since this references self we need to finish initializing before setting it, we
|
// Note: Since this references self we need to finish initializing before setting it, we
|
||||||
|
@ -139,14 +138,14 @@ public class HomeViewModel {
|
||||||
}()
|
}()
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
/// **Note:** This `optimisedJoinSQL` value includes the required minimum joins needed for the query
|
/// **Note:** This `optimisedJoinSQL` value includes the required minimum joins needed for the query but differs
|
||||||
|
/// from the JOINs that are actually used for performance reasons as the basic logic can be simpler for where it's used
|
||||||
joinSQL: SessionThreadViewModel.optimisedJoinSQL,
|
joinSQL: SessionThreadViewModel.optimisedJoinSQL,
|
||||||
filterSQL: SessionThreadViewModel.homeFilterSQL(userPublicKey: userPublicKey),
|
filterSQL: SessionThreadViewModel.homeFilterSQL(userPublicKey: userPublicKey),
|
||||||
groupSQL: SessionThreadViewModel.groupSQL,
|
groupSQL: SessionThreadViewModel.groupSQL,
|
||||||
orderSQL: SessionThreadViewModel.homeOrderSQL,
|
orderSQL: SessionThreadViewModel.homeOrderSQL,
|
||||||
dataQuery: SessionThreadViewModel.baseQuery(
|
dataQuery: SessionThreadViewModel.baseQuery(
|
||||||
userPublicKey: userPublicKey,
|
userPublicKey: userPublicKey,
|
||||||
filterSQL: SessionThreadViewModel.homeFilterSQL(userPublicKey: userPublicKey),
|
|
||||||
groupSQL: SessionThreadViewModel.groupSQL,
|
groupSQL: SessionThreadViewModel.groupSQL,
|
||||||
orderSQL: SessionThreadViewModel.homeOrderSQL
|
orderSQL: SessionThreadViewModel.homeOrderSQL
|
||||||
),
|
),
|
||||||
|
@ -194,8 +193,9 @@ public class HomeViewModel {
|
||||||
let hasHiddenMessageRequests: Bool = db[.hasHiddenMessageRequests]
|
let hasHiddenMessageRequests: Bool = db[.hasHiddenMessageRequests]
|
||||||
let userProfile: Profile = Profile.fetchOrCreateCurrentUser(db)
|
let userProfile: Profile = Profile.fetchOrCreateCurrentUser(db)
|
||||||
let unreadMessageRequestThreadCount: Int = try SessionThread
|
let unreadMessageRequestThreadCount: Int = try SessionThread
|
||||||
.unreadMessageRequestsThreadIdQuery(userPublicKey: userProfile.id)
|
.unreadMessageRequestsCountQuery(userPublicKey: userProfile.id)
|
||||||
.fetchCount(db)
|
.fetchOne(db)
|
||||||
|
.defaulting(to: 0)
|
||||||
|
|
||||||
return State(
|
return State(
|
||||||
showViewedSeedBanner: !hasViewedSeed,
|
showViewedSeedBanner: !hasViewedSeed,
|
||||||
|
@ -219,7 +219,8 @@ public class HomeViewModel {
|
||||||
else { return }
|
else { return }
|
||||||
|
|
||||||
/// **MUST** have the same logic as in the 'PagedDataObserver.onChangeUnsorted' above
|
/// **MUST** have the same logic as in the 'PagedDataObserver.onChangeUnsorted' above
|
||||||
let currentData: [SessionThreadViewModel] = self.threadData.flatMap { $0.elements }
|
let currentData: [SessionThreadViewModel] = (self.unobservedThreadDataChanges ?? self.threadData)
|
||||||
|
.flatMap { $0.elements }
|
||||||
let updatedThreadData: [SectionModel] = self.process(data: currentData, for: currentPageInfo)
|
let updatedThreadData: [SectionModel] = self.process(data: currentData, for: currentPageInfo)
|
||||||
|
|
||||||
guard let onThreadChange: (([SectionModel]) -> ()) = self.onThreadChange else {
|
guard let onThreadChange: (([SectionModel]) -> ()) = self.onThreadChange else {
|
||||||
|
|
|
@ -86,14 +86,14 @@ public class MessageRequestsViewModel {
|
||||||
}()
|
}()
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
/// **Note:** This `optimisedJoinSQL` value includes the required minimum joins needed for the query
|
/// **Note:** This `optimisedJoinSQL` value includes the required minimum joins needed for the query but differs
|
||||||
|
/// from the JOINs that are actually used for performance reasons as the basic logic can be simpler for where it's used
|
||||||
joinSQL: SessionThreadViewModel.optimisedJoinSQL,
|
joinSQL: SessionThreadViewModel.optimisedJoinSQL,
|
||||||
filterSQL: SessionThreadViewModel.messageRequestsFilterSQL(userPublicKey: userPublicKey),
|
filterSQL: SessionThreadViewModel.messageRequestsFilterSQL(userPublicKey: userPublicKey),
|
||||||
groupSQL: SessionThreadViewModel.groupSQL,
|
groupSQL: SessionThreadViewModel.groupSQL,
|
||||||
orderSQL: SessionThreadViewModel.messageRequetsOrderSQL,
|
orderSQL: SessionThreadViewModel.messageRequetsOrderSQL,
|
||||||
dataQuery: SessionThreadViewModel.baseQuery(
|
dataQuery: SessionThreadViewModel.baseQuery(
|
||||||
userPublicKey: userPublicKey,
|
userPublicKey: userPublicKey,
|
||||||
filterSQL: SessionThreadViewModel.messageRequestsFilterSQL(userPublicKey: userPublicKey),
|
|
||||||
groupSQL: SessionThreadViewModel.groupSQL,
|
groupSQL: SessionThreadViewModel.groupSQL,
|
||||||
orderSQL: SessionThreadViewModel.messageRequetsOrderSQL
|
orderSQL: SessionThreadViewModel.messageRequetsOrderSQL
|
||||||
),
|
),
|
||||||
|
|
|
@ -367,7 +367,7 @@ public class MediaGalleryViewModel {
|
||||||
.removeDuplicates()
|
.removeDuplicates()
|
||||||
}
|
}
|
||||||
|
|
||||||
@discardableResult public func loadAndCacheAlbumData(for interactionId: Int64) -> [Item] {
|
@discardableResult public func loadAndCacheAlbumData(for interactionId: Int64, in threadId: String) -> [Item] {
|
||||||
typealias AlbumInfo = (albumData: [Item], interactionIdBefore: Int64?, interactionIdAfter: Int64?)
|
typealias AlbumInfo = (albumData: [Item], interactionIdBefore: Int64?, interactionIdAfter: Int64?)
|
||||||
|
|
||||||
// Note: It's possible we already have cached album data for this interaction
|
// Note: It's possible we already have cached album data for this interaction
|
||||||
|
@ -394,13 +394,19 @@ public class MediaGalleryViewModel {
|
||||||
let itemBefore: Item? = try Item
|
let itemBefore: Item? = try Item
|
||||||
.baseQuery(
|
.baseQuery(
|
||||||
orderSQL: Item.galleryReverseOrderSQL,
|
orderSQL: Item.galleryReverseOrderSQL,
|
||||||
customFilters: SQL("\(interaction[.timestampMs]) > \(albumTimestampMs)")
|
customFilters: SQL("""
|
||||||
|
\(interaction[.timestampMs]) > \(albumTimestampMs) AND
|
||||||
|
\(interaction[.threadId]) = \(threadId)
|
||||||
|
""")
|
||||||
)
|
)
|
||||||
.fetchOne(db)
|
.fetchOne(db)
|
||||||
let itemAfter: Item? = try Item
|
let itemAfter: Item? = try Item
|
||||||
.baseQuery(
|
.baseQuery(
|
||||||
orderSQL: Item.galleryOrderSQL,
|
orderSQL: Item.galleryOrderSQL,
|
||||||
customFilters: SQL("\(interaction[.timestampMs]) < \(albumTimestampMs)")
|
customFilters: SQL("""
|
||||||
|
\(interaction[.timestampMs]) < \(albumTimestampMs) AND
|
||||||
|
\(interaction[.threadId]) = \(threadId)
|
||||||
|
""")
|
||||||
)
|
)
|
||||||
.fetchOne(db)
|
.fetchOne(db)
|
||||||
|
|
||||||
|
@ -505,7 +511,7 @@ public class MediaGalleryViewModel {
|
||||||
threadVariant: threadVariant,
|
threadVariant: threadVariant,
|
||||||
isPagedData: false
|
isPagedData: false
|
||||||
)
|
)
|
||||||
viewModel.loadAndCacheAlbumData(for: interactionId)
|
viewModel.loadAndCacheAlbumData(for: interactionId, in: threadId)
|
||||||
viewModel.replaceAlbumObservation(toObservationFor: interactionId)
|
viewModel.replaceAlbumObservation(toObservationFor: interactionId)
|
||||||
|
|
||||||
guard
|
guard
|
||||||
|
|
|
@ -681,10 +681,15 @@ class MediaPageViewController: UIPageViewController, UIPageViewControllerDataSou
|
||||||
}
|
}
|
||||||
|
|
||||||
// Then check if there is an interaction before the current album interaction
|
// Then check if there is an interaction before the current album interaction
|
||||||
guard let interactionIdAfter: Int64 = self.viewModel.interactionIdAfter[interactionId] else { return nil }
|
guard let interactionIdAfter: Int64 = self.viewModel.interactionIdAfter[interactionId] else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// Cache and retrieve the new album items
|
// Cache and retrieve the new album items
|
||||||
let newAlbumItems: [MediaGalleryViewModel.Item] = viewModel.loadAndCacheAlbumData(for: interactionIdAfter)
|
let newAlbumItems: [MediaGalleryViewModel.Item] = viewModel.loadAndCacheAlbumData(
|
||||||
|
for: interactionIdAfter,
|
||||||
|
in: self.viewModel.threadId
|
||||||
|
)
|
||||||
|
|
||||||
guard
|
guard
|
||||||
!newAlbumItems.isEmpty,
|
!newAlbumItems.isEmpty,
|
||||||
|
@ -723,10 +728,15 @@ class MediaPageViewController: UIPageViewController, UIPageViewControllerDataSou
|
||||||
}
|
}
|
||||||
|
|
||||||
// Then check if there is an interaction before the current album interaction
|
// Then check if there is an interaction before the current album interaction
|
||||||
guard let interactionIdBefore: Int64 = self.viewModel.interactionIdBefore[interactionId] else { return nil }
|
guard let interactionIdBefore: Int64 = self.viewModel.interactionIdBefore[interactionId] else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// Cache and retrieve the new album items
|
// Cache and retrieve the new album items
|
||||||
let newAlbumItems: [MediaGalleryViewModel.Item] = viewModel.loadAndCacheAlbumData(for: interactionIdBefore)
|
let newAlbumItems: [MediaGalleryViewModel.Item] = viewModel.loadAndCacheAlbumData(
|
||||||
|
for: interactionIdBefore,
|
||||||
|
in: self.viewModel.threadId
|
||||||
|
)
|
||||||
|
|
||||||
guard
|
guard
|
||||||
!newAlbumItems.isEmpty,
|
!newAlbumItems.isEmpty,
|
||||||
|
|
|
@ -133,11 +133,12 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
|
||||||
// NOTE: Fix an edge case where user taps on the callkit notification
|
// NOTE: Fix an edge case where user taps on the callkit notification
|
||||||
// but answers the call on another device
|
// but answers the call on another device
|
||||||
stopPollers(shouldStopUserPoller: !self.hasIncomingCallWaiting())
|
stopPollers(shouldStopUserPoller: !self.hasIncomingCallWaiting())
|
||||||
JobRunner.stopAndClearPendingJobs()
|
|
||||||
|
|
||||||
// Suspend database
|
// Stop all jobs except for message sending and when completed suspend the database
|
||||||
|
JobRunner.stopAndClearPendingJobs(exceptForVariant: .messageSend) {
|
||||||
NotificationCenter.default.post(name: Database.suspendNotification, object: self)
|
NotificationCenter.default.post(name: Database.suspendNotification, object: self)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func applicationDidReceiveMemoryWarning(_ application: UIApplication) {
|
func applicationDidReceiveMemoryWarning(_ application: UIApplication) {
|
||||||
Logger.info("applicationDidReceiveMemoryWarning")
|
Logger.info("applicationDidReceiveMemoryWarning")
|
||||||
|
|
|
@ -44,7 +44,6 @@
|
||||||
#import <SessionUtilitiesKit/NSData+Image.h>
|
#import <SessionUtilitiesKit/NSData+Image.h>
|
||||||
#import <SessionUtilitiesKit/NSNotificationCenter+OWS.h>
|
#import <SessionUtilitiesKit/NSNotificationCenter+OWS.h>
|
||||||
#import <SessionUtilitiesKit/NSString+SSK.h>
|
#import <SessionUtilitiesKit/NSString+SSK.h>
|
||||||
#import <SessionMessagingKit/OWSBackgroundTask.h>
|
|
||||||
#import <SignalUtilitiesKit/OWSDispatch.h>
|
#import <SignalUtilitiesKit/OWSDispatch.h>
|
||||||
#import <SignalUtilitiesKit/OWSError.h>
|
#import <SignalUtilitiesKit/OWSError.h>
|
||||||
#import <SessionUtilitiesKit/OWSFileSystem.h>
|
#import <SessionUtilitiesKit/OWSFileSystem.h>
|
||||||
|
|
|
@ -82,10 +82,6 @@ extension AppNotificationAction {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delay notification of incoming messages when it's a background polling to
|
|
||||||
// avoid too many notifications fired at the same time
|
|
||||||
let kNotificationDelayForBackgroumdPoll: TimeInterval = 5
|
|
||||||
|
|
||||||
let kAudioNotificationsThrottleCount = 2
|
let kAudioNotificationsThrottleCount = 2
|
||||||
let kAudioNotificationsThrottleInterval: TimeInterval = 5
|
let kAudioNotificationsThrottleInterval: TimeInterval = 5
|
||||||
|
|
||||||
|
@ -93,14 +89,48 @@ protocol NotificationPresenterAdaptee: AnyObject {
|
||||||
|
|
||||||
func registerNotificationSettings() -> Promise<Void>
|
func registerNotificationSettings() -> Promise<Void>
|
||||||
|
|
||||||
func notify(category: AppNotificationCategory, title: String?, body: String, userInfo: [AnyHashable: Any], sound: Preferences.Sound?)
|
func notify(
|
||||||
func notify(category: AppNotificationCategory, title: String?, body: String, userInfo: [AnyHashable: Any], sound: Preferences.Sound?, replacingIdentifier: String?)
|
category: AppNotificationCategory,
|
||||||
|
title: String?,
|
||||||
|
body: String,
|
||||||
|
userInfo: [AnyHashable: Any],
|
||||||
|
previewType: Preferences.NotificationPreviewType,
|
||||||
|
sound: Preferences.Sound?,
|
||||||
|
threadVariant: SessionThread.Variant,
|
||||||
|
threadName: String,
|
||||||
|
replacingIdentifier: String?
|
||||||
|
)
|
||||||
|
|
||||||
func cancelNotifications(threadId: String)
|
func cancelNotifications(threadId: String)
|
||||||
func cancelNotifications(identifiers: [String])
|
func cancelNotifications(identifiers: [String])
|
||||||
func clearAllNotifications()
|
func clearAllNotifications()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension NotificationPresenterAdaptee {
|
||||||
|
func notify(
|
||||||
|
category: AppNotificationCategory,
|
||||||
|
title: String?,
|
||||||
|
body: String,
|
||||||
|
userInfo: [AnyHashable: Any],
|
||||||
|
previewType: Preferences.NotificationPreviewType,
|
||||||
|
sound: Preferences.Sound?,
|
||||||
|
threadVariant: SessionThread.Variant,
|
||||||
|
threadName: String
|
||||||
|
) {
|
||||||
|
notify(
|
||||||
|
category: category,
|
||||||
|
title: title,
|
||||||
|
body: body,
|
||||||
|
userInfo: userInfo,
|
||||||
|
previewType: previewType,
|
||||||
|
sound: sound,
|
||||||
|
threadVariant: threadVariant,
|
||||||
|
threadName: threadName,
|
||||||
|
replacingIdentifier: nil
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@objc(OWSNotificationPresenter)
|
@objc(OWSNotificationPresenter)
|
||||||
public class NotificationPresenter: NSObject, NotificationsProtocol {
|
public class NotificationPresenter: NSObject, NotificationsProtocol {
|
||||||
|
|
||||||
|
@ -141,7 +171,7 @@ public class NotificationPresenter: NSObject, NotificationsProtocol {
|
||||||
return adaptee.registerNotificationSettings()
|
return adaptee.registerNotificationSettings()
|
||||||
}
|
}
|
||||||
|
|
||||||
public func notifyUser(_ db: Database, for interaction: Interaction, in thread: SessionThread, isBackgroundPoll: Bool) {
|
public func notifyUser(_ db: Database, for interaction: Interaction, in thread: SessionThread) {
|
||||||
let isMessageRequest: Bool = thread.isMessageRequest(db, includeNonVisible: true)
|
let isMessageRequest: Bool = thread.isMessageRequest(db, includeNonVisible: true)
|
||||||
|
|
||||||
// Ensure we should be showing a notification for the thread
|
// Ensure we should be showing a notification for the thread
|
||||||
|
@ -149,7 +179,10 @@ public class NotificationPresenter: NSObject, NotificationsProtocol {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
let identifier: String = interaction.notificationIdentifier(isBackgroundPoll: isBackgroundPoll)
|
// Try to group notifications for interactions from open groups
|
||||||
|
let identifier: String = interaction.notificationIdentifier(
|
||||||
|
shouldGroupMessagesForThread: (thread.variant == .openGroup)
|
||||||
|
)
|
||||||
|
|
||||||
// While batch processing, some of the necessary changes have not been commited.
|
// While batch processing, some of the necessary changes have not been commited.
|
||||||
let rawMessageText = interaction.previewText(db)
|
let rawMessageText = interaction.previewText(db)
|
||||||
|
@ -166,19 +199,7 @@ public class NotificationPresenter: NSObject, NotificationsProtocol {
|
||||||
let senderName = Profile.displayName(db, id: interaction.authorId, threadVariant: thread.variant)
|
let senderName = Profile.displayName(db, id: interaction.authorId, threadVariant: thread.variant)
|
||||||
let previewType: Preferences.NotificationPreviewType = db[.preferencesNotificationPreviewType]
|
let previewType: Preferences.NotificationPreviewType = db[.preferencesNotificationPreviewType]
|
||||||
.defaulting(to: .nameAndPreview)
|
.defaulting(to: .nameAndPreview)
|
||||||
|
let groupName: String = SessionThread.displayName(
|
||||||
switch previewType {
|
|
||||||
case .noNameNoPreview:
|
|
||||||
notificationTitle = "Session"
|
|
||||||
|
|
||||||
case .nameNoPreview, .nameAndPreview:
|
|
||||||
switch thread.variant {
|
|
||||||
case .contact:
|
|
||||||
notificationTitle = (isMessageRequest ? "Session" : senderName)
|
|
||||||
|
|
||||||
case .closedGroup, .openGroup:
|
|
||||||
let groupName: String = SessionThread
|
|
||||||
.displayName(
|
|
||||||
threadId: thread.id,
|
threadId: thread.id,
|
||||||
variant: thread.variant,
|
variant: thread.variant,
|
||||||
closedGroupName: try? thread.closedGroup
|
closedGroupName: try? thread.closedGroup
|
||||||
|
@ -191,13 +212,21 @@ public class NotificationPresenter: NSObject, NotificationsProtocol {
|
||||||
.fetchOne(db)
|
.fetchOne(db)
|
||||||
)
|
)
|
||||||
|
|
||||||
notificationTitle = (isBackgroundPoll ? groupName :
|
switch previewType {
|
||||||
String(
|
case .noNameNoPreview:
|
||||||
|
notificationTitle = "Session"
|
||||||
|
|
||||||
|
case .nameNoPreview, .nameAndPreview:
|
||||||
|
switch thread.variant {
|
||||||
|
case .contact:
|
||||||
|
notificationTitle = (isMessageRequest ? "Session" : senderName)
|
||||||
|
|
||||||
|
case .closedGroup, .openGroup:
|
||||||
|
notificationTitle = String(
|
||||||
format: NotificationStrings.incomingGroupMessageTitleFormat,
|
format: NotificationStrings.incomingGroupMessageTitleFormat,
|
||||||
senderName,
|
senderName,
|
||||||
groupName
|
groupName
|
||||||
)
|
)
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -243,9 +272,12 @@ public class NotificationPresenter: NSObject, NotificationsProtocol {
|
||||||
self.adaptee.notify(
|
self.adaptee.notify(
|
||||||
category: category,
|
category: category,
|
||||||
title: notificationTitle,
|
title: notificationTitle,
|
||||||
body: notificationBody ?? "",
|
body: (notificationBody ?? ""),
|
||||||
userInfo: userInfo,
|
userInfo: userInfo,
|
||||||
|
previewType: previewType,
|
||||||
sound: sound,
|
sound: sound,
|
||||||
|
threadVariant: thread.variant,
|
||||||
|
threadName: groupName,
|
||||||
replacingIdentifier: identifier
|
replacingIdentifier: identifier
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -268,23 +300,26 @@ public class NotificationPresenter: NSObject, NotificationsProtocol {
|
||||||
guard messageInfo.state == .missed || messageInfo.state == .permissionDenied else { return }
|
guard messageInfo.state == .missed || messageInfo.state == .permissionDenied else { return }
|
||||||
|
|
||||||
let category = AppNotificationCategory.errorMessage
|
let category = AppNotificationCategory.errorMessage
|
||||||
|
let previewType: Preferences.NotificationPreviewType = db[.preferencesNotificationPreviewType]
|
||||||
|
.defaulting(to: .nameAndPreview)
|
||||||
|
|
||||||
let userInfo = [
|
let userInfo = [
|
||||||
AppNotificationUserInfoKey.threadId: thread.id
|
AppNotificationUserInfoKey.threadId: thread.id
|
||||||
]
|
]
|
||||||
|
|
||||||
let notificationTitle = interaction.previewText(db)
|
let notificationTitle: String = interaction.previewText(db)
|
||||||
var notificationBody: String?
|
let threadName: String = SessionThread.displayName(
|
||||||
|
|
||||||
if messageInfo.state == .permissionDenied {
|
|
||||||
notificationBody = String(
|
|
||||||
format: "modal_call_missed_tips_explanation".localized(),
|
|
||||||
SessionThread.displayName(
|
|
||||||
threadId: thread.id,
|
threadId: thread.id,
|
||||||
variant: thread.variant,
|
variant: thread.variant,
|
||||||
closedGroupName: nil, // Not supported
|
closedGroupName: nil, // Not supported
|
||||||
openGroupName: nil // Not supported
|
openGroupName: nil // Not supported
|
||||||
)
|
)
|
||||||
|
var notificationBody: String?
|
||||||
|
|
||||||
|
if messageInfo.state == .permissionDenied {
|
||||||
|
notificationBody = String(
|
||||||
|
format: "modal_call_missed_tips_explanation".localized(),
|
||||||
|
threadName
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -294,9 +329,12 @@ public class NotificationPresenter: NSObject, NotificationsProtocol {
|
||||||
self.adaptee.notify(
|
self.adaptee.notify(
|
||||||
category: category,
|
category: category,
|
||||||
title: notificationTitle,
|
title: notificationTitle,
|
||||||
body: notificationBody ?? "",
|
body: (notificationBody ?? ""),
|
||||||
userInfo: userInfo,
|
userInfo: userInfo,
|
||||||
|
previewType: previewType,
|
||||||
sound: sound,
|
sound: sound,
|
||||||
|
threadVariant: thread.variant,
|
||||||
|
threadName: threadName,
|
||||||
replacingIdentifier: UUID().uuidString
|
replacingIdentifier: UUID().uuidString
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -306,11 +344,7 @@ public class NotificationPresenter: NSObject, NotificationsProtocol {
|
||||||
let notificationTitle: String?
|
let notificationTitle: String?
|
||||||
let previewType: Preferences.NotificationPreviewType = db[.preferencesNotificationPreviewType]
|
let previewType: Preferences.NotificationPreviewType = db[.preferencesNotificationPreviewType]
|
||||||
.defaulting(to: .nameAndPreview)
|
.defaulting(to: .nameAndPreview)
|
||||||
|
let threadName: String = SessionThread.displayName(
|
||||||
switch previewType {
|
|
||||||
case .noNameNoPreview: notificationTitle = nil
|
|
||||||
case .nameNoPreview, .nameAndPreview:
|
|
||||||
notificationTitle = SessionThread.displayName(
|
|
||||||
threadId: thread.id,
|
threadId: thread.id,
|
||||||
variant: thread.variant,
|
variant: thread.variant,
|
||||||
closedGroupName: try? thread.closedGroup
|
closedGroupName: try? thread.closedGroup
|
||||||
|
@ -324,6 +358,10 @@ public class NotificationPresenter: NSObject, NotificationsProtocol {
|
||||||
isNoteToSelf: (thread.isNoteToSelf(db) == true),
|
isNoteToSelf: (thread.isNoteToSelf(db) == true),
|
||||||
profile: try? Profile.fetchOne(db, id: thread.id)
|
profile: try? Profile.fetchOne(db, id: thread.id)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
switch previewType {
|
||||||
|
case .noNameNoPreview: notificationTitle = nil
|
||||||
|
case .nameNoPreview, .nameAndPreview: notificationTitle = threadName
|
||||||
}
|
}
|
||||||
|
|
||||||
let notificationBody = NotificationStrings.failedToSendBody
|
let notificationBody = NotificationStrings.failedToSendBody
|
||||||
|
@ -340,7 +378,10 @@ public class NotificationPresenter: NSObject, NotificationsProtocol {
|
||||||
title: notificationTitle,
|
title: notificationTitle,
|
||||||
body: notificationBody,
|
body: notificationBody,
|
||||||
userInfo: userInfo,
|
userInfo: userInfo,
|
||||||
sound: sound
|
previewType: previewType,
|
||||||
|
sound: sound,
|
||||||
|
threadVariant: thread.variant,
|
||||||
|
threadName: threadName
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -57,8 +57,9 @@ class UserNotificationPresenterAdaptee: NSObject, UNUserNotificationCenterDelega
|
||||||
|
|
||||||
override init() {
|
override init() {
|
||||||
self.notificationCenter = UNUserNotificationCenter.current()
|
self.notificationCenter = UNUserNotificationCenter.current()
|
||||||
|
|
||||||
super.init()
|
super.init()
|
||||||
notificationCenter.delegate = self
|
|
||||||
SwiftSingletons.register(self)
|
SwiftSingletons.register(self)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -86,29 +87,37 @@ extension UserNotificationPresenterAdaptee: NotificationPresenterAdaptee {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func notify(category: AppNotificationCategory, title: String?, body: String, userInfo: [AnyHashable: Any], sound: Preferences.Sound?) {
|
func notify(
|
||||||
AssertIsOnMainThread()
|
category: AppNotificationCategory,
|
||||||
notify(category: category, title: title, body: body, userInfo: userInfo, sound: sound, replacingIdentifier: nil)
|
title: String?,
|
||||||
}
|
body: String,
|
||||||
|
userInfo: [AnyHashable: Any],
|
||||||
func notify(category: AppNotificationCategory, title: String?, body: String, userInfo: [AnyHashable: Any], sound: Preferences.Sound?, replacingIdentifier: String?) {
|
previewType: Preferences.NotificationPreviewType,
|
||||||
|
sound: Preferences.Sound?,
|
||||||
|
threadVariant: SessionThread.Variant,
|
||||||
|
threadName: String,
|
||||||
|
replacingIdentifier: String?
|
||||||
|
) {
|
||||||
AssertIsOnMainThread()
|
AssertIsOnMainThread()
|
||||||
|
|
||||||
|
let threadIdentifier: String? = (userInfo[AppNotificationUserInfoKey.threadId] as? String)
|
||||||
let content = UNMutableNotificationContent()
|
let content = UNMutableNotificationContent()
|
||||||
content.categoryIdentifier = category.identifier
|
content.categoryIdentifier = category.identifier
|
||||||
content.userInfo = userInfo
|
content.userInfo = userInfo
|
||||||
let isReplacingNotification = replacingIdentifier != nil
|
content.threadIdentifier = (threadIdentifier ?? content.threadIdentifier)
|
||||||
var isBackgroudPoll = false
|
|
||||||
if let threadIdentifier = userInfo[AppNotificationUserInfoKey.threadId] as? String {
|
let shouldGroupNotification: Bool = (
|
||||||
content.threadIdentifier = threadIdentifier
|
threadVariant == .openGroup &&
|
||||||
isBackgroudPoll = replacingIdentifier == threadIdentifier
|
replacingIdentifier == threadIdentifier
|
||||||
}
|
)
|
||||||
let isAppActive = UIApplication.shared.applicationState == .active
|
let isAppActive = UIApplication.shared.applicationState == .active
|
||||||
if let sound = sound, sound != .none {
|
if let sound = sound, sound != .none {
|
||||||
content.sound = sound.notificationSound(isQuiet: isAppActive)
|
content.sound = sound.notificationSound(isQuiet: isAppActive)
|
||||||
}
|
}
|
||||||
|
|
||||||
let notificationIdentifier = isReplacingNotification ? replacingIdentifier! : UUID().uuidString
|
let notificationIdentifier: String = (replacingIdentifier ?? UUID().uuidString)
|
||||||
|
let isReplacingNotification: Bool = (notifications[notificationIdentifier] != nil)
|
||||||
|
var trigger: UNNotificationTrigger?
|
||||||
|
|
||||||
if shouldPresentNotification(category: category, userInfo: userInfo) {
|
if shouldPresentNotification(category: category, userInfo: userInfo) {
|
||||||
if let displayableTitle = title?.filterForDisplay {
|
if let displayableTitle = title?.filterForDisplay {
|
||||||
|
@ -117,30 +126,50 @@ extension UserNotificationPresenterAdaptee: NotificationPresenterAdaptee {
|
||||||
if let displayableBody = body.filterForDisplay {
|
if let displayableBody = body.filterForDisplay {
|
||||||
content.body = displayableBody
|
content.body = displayableBody
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
|
if shouldGroupNotification {
|
||||||
|
trigger = UNTimeIntervalNotificationTrigger(
|
||||||
|
timeInterval: Notifications.delayForGroupedNotifications,
|
||||||
|
repeats: false
|
||||||
|
)
|
||||||
|
|
||||||
|
let numberExistingNotifications: Int? = notifications[notificationIdentifier]?
|
||||||
|
.content
|
||||||
|
.userInfo[AppNotificationUserInfoKey.threadNotificationCounter]
|
||||||
|
.asType(Int.self)
|
||||||
|
var numberOfNotifications: Int = (numberExistingNotifications ?? 1)
|
||||||
|
|
||||||
|
if numberExistingNotifications != nil {
|
||||||
|
numberOfNotifications += 1 // Add one for the current notification
|
||||||
|
|
||||||
|
content.title = (previewType == .noNameNoPreview ?
|
||||||
|
content.title :
|
||||||
|
threadName
|
||||||
|
)
|
||||||
|
content.body = String(
|
||||||
|
format: NotificationStrings.incomingCollapsedMessagesBody,
|
||||||
|
"\(numberOfNotifications)"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
content.userInfo[AppNotificationUserInfoKey.threadNotificationCounter] = numberOfNotifications
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
// Play sound and vibrate, but without a `body` no banner will show.
|
// Play sound and vibrate, but without a `body` no banner will show.
|
||||||
Logger.debug("supressing notification body")
|
Logger.debug("supressing notification body")
|
||||||
}
|
}
|
||||||
|
|
||||||
let trigger: UNNotificationTrigger?
|
let request = UNNotificationRequest(
|
||||||
if isBackgroudPoll {
|
identifier: notificationIdentifier,
|
||||||
trigger = UNTimeIntervalNotificationTrigger(timeInterval: kNotificationDelayForBackgroumdPoll, repeats: false)
|
content: content,
|
||||||
let numberOfNotifications: Int
|
trigger: trigger
|
||||||
if let lastRequest = notifications[notificationIdentifier], let counter = lastRequest.content.userInfo[AppNotificationUserInfoKey.threadNotificationCounter] as? Int {
|
)
|
||||||
numberOfNotifications = counter + 1
|
|
||||||
content.body = String(format: NotificationStrings.incomingCollapsedMessagesBody, "\(numberOfNotifications)")
|
|
||||||
} else {
|
|
||||||
numberOfNotifications = 1
|
|
||||||
}
|
|
||||||
content.userInfo[AppNotificationUserInfoKey.threadNotificationCounter] = numberOfNotifications
|
|
||||||
} else {
|
|
||||||
trigger = nil
|
|
||||||
}
|
|
||||||
|
|
||||||
let request = UNNotificationRequest(identifier: notificationIdentifier, content: content, trigger: trigger)
|
|
||||||
|
|
||||||
Logger.debug("presenting notification with identifier: \(notificationIdentifier)")
|
Logger.debug("presenting notification with identifier: \(notificationIdentifier)")
|
||||||
|
|
||||||
if isReplacingNotification { cancelNotifications(identifiers: [notificationIdentifier]) }
|
if isReplacingNotification { cancelNotifications(identifiers: [notificationIdentifier]) }
|
||||||
|
|
||||||
notificationCenter.add(request)
|
notificationCenter.add(request)
|
||||||
notifications[notificationIdentifier] = request
|
notifications[notificationIdentifier] = request
|
||||||
}
|
}
|
||||||
|
|
|
@ -164,12 +164,14 @@ public final class FullConversationCell: UITableViewCell {
|
||||||
|
|
||||||
// Unread count view
|
// Unread count view
|
||||||
unreadCountView.addSubview(unreadCountLabel)
|
unreadCountView.addSubview(unreadCountLabel)
|
||||||
|
unreadCountLabel.setCompressionResistanceHigh()
|
||||||
unreadCountLabel.pin([ VerticalEdge.top, VerticalEdge.bottom ], to: unreadCountView)
|
unreadCountLabel.pin([ VerticalEdge.top, VerticalEdge.bottom ], to: unreadCountView)
|
||||||
unreadCountView.pin(.leading, to: .leading, of: unreadCountLabel, withInset: -4)
|
unreadCountView.pin(.leading, to: .leading, of: unreadCountLabel, withInset: -4)
|
||||||
unreadCountView.pin(.trailing, to: .trailing, of: unreadCountLabel, withInset: 4)
|
unreadCountView.pin(.trailing, to: .trailing, of: unreadCountLabel, withInset: 4)
|
||||||
|
|
||||||
// Has mention view
|
// Has mention view
|
||||||
hasMentionView.addSubview(hasMentionLabel)
|
hasMentionView.addSubview(hasMentionLabel)
|
||||||
|
hasMentionLabel.setCompressionResistanceHigh()
|
||||||
hasMentionLabel.pin(to: hasMentionView)
|
hasMentionLabel.pin(to: hasMentionView)
|
||||||
|
|
||||||
// Label stack view
|
// Label stack view
|
||||||
|
|
|
@ -34,7 +34,7 @@ public final class BackgroundPoller {
|
||||||
poller.stop()
|
poller.stop()
|
||||||
|
|
||||||
return poller.poll(
|
return poller.poll(
|
||||||
isBackgroundPoll: true,
|
calledFromBackgroundPoller: true,
|
||||||
isBackgroundPollerValid: { BackgroundPoller.isValid },
|
isBackgroundPollerValid: { BackgroundPoller.isValid },
|
||||||
isPostCapabilitiesRetry: false
|
isPostCapabilitiesRetry: false
|
||||||
)
|
)
|
||||||
|
@ -82,7 +82,7 @@ public final class BackgroundPoller {
|
||||||
groupPublicKey,
|
groupPublicKey,
|
||||||
on: DispatchQueue.main,
|
on: DispatchQueue.main,
|
||||||
maxRetryCount: 0,
|
maxRetryCount: 0,
|
||||||
isBackgroundPoll: true,
|
calledFromBackgroundPoller: true,
|
||||||
isBackgroundPollValid: { BackgroundPoller.isValid }
|
isBackgroundPollValid: { BackgroundPoller.isValid }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -134,7 +134,7 @@ public final class BackgroundPoller {
|
||||||
threadId: threadId,
|
threadId: threadId,
|
||||||
details: MessageReceiveJob.Details(
|
details: MessageReceiveJob.Details(
|
||||||
messages: threadMessages.map { $0.messageInfo },
|
messages: threadMessages.map { $0.messageInfo },
|
||||||
isBackgroundPoll: true
|
calledFromBackgroundPoller: true
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -50,8 +50,8 @@ final class IP2Country {
|
||||||
|
|
||||||
@objc func populateCacheIfNeededAsync() {
|
@objc func populateCacheIfNeededAsync() {
|
||||||
// This has to be sync since the `countryNamesCache` dict doesn't like async access
|
// This has to be sync since the `countryNamesCache` dict doesn't like async access
|
||||||
IP2Country.workQueue.sync {
|
IP2Country.workQueue.sync { [weak self] in
|
||||||
let _ = self.populateCacheIfNeeded()
|
_ = self?.populateCacheIfNeeded()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -18,7 +18,8 @@ public enum SNMessagingKit { // Just to make the external API nice
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
_005_FixDeletedMessageReadState.self,
|
_005_FixDeletedMessageReadState.self,
|
||||||
_006_FixHiddenModAdminSupport.self
|
_006_FixHiddenModAdminSupport.self,
|
||||||
|
_007_HomeQueryOptimisationIndexes.self
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
|
@ -1253,7 +1253,7 @@ enum _003_YDBToGRDBMigration: Migration {
|
||||||
threadId: processedMessage.threadId,
|
threadId: processedMessage.threadId,
|
||||||
details: MessageReceiveJob.Details(
|
details: MessageReceiveJob.Details(
|
||||||
messages: [processedMessage.messageInfo],
|
messages: [processedMessage.messageInfo],
|
||||||
isBackgroundPoll: legacyJob.isBackgroundPoll
|
calledFromBackgroundPoller: legacyJob.isBackgroundPoll
|
||||||
)
|
)
|
||||||
)?.inserted(db)
|
)?.inserted(db)
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,37 @@
|
||||||
|
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import GRDB
|
||||||
|
import SessionUtilitiesKit
|
||||||
|
|
||||||
|
/// This migration adds an index to the interaction table in order to improve the performance of retrieving the number of unread interactions
|
||||||
|
enum _007_HomeQueryOptimisationIndexes: Migration {
|
||||||
|
static let target: TargetMigrations.Identifier = .messagingKit
|
||||||
|
static let identifier: String = "HomeQueryOptimisationIndexes"
|
||||||
|
static let needsConfigSync: Bool = false
|
||||||
|
static let minExpectedRunDuration: TimeInterval = 0.1
|
||||||
|
|
||||||
|
static func migrate(_ db: Database) throws {
|
||||||
|
try db.create(
|
||||||
|
index: "interaction_on_wasRead_and_hasMention_and_threadId",
|
||||||
|
on: Interaction.databaseTableName,
|
||||||
|
columns: [
|
||||||
|
Interaction.Columns.wasRead.name,
|
||||||
|
Interaction.Columns.hasMention.name,
|
||||||
|
Interaction.Columns.threadId.name
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
try db.create(
|
||||||
|
index: "interaction_on_threadId_and_timestampMs_and_variant",
|
||||||
|
on: Interaction.databaseTableName,
|
||||||
|
columns: [
|
||||||
|
Interaction.Columns.threadId.name,
|
||||||
|
Interaction.Columns.timestampMs.name,
|
||||||
|
Interaction.Columns.variant.name
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
Storage.update(progress: 1, for: self, in: target) // In case this is the last migration
|
||||||
|
}
|
||||||
|
}
|
|
@ -262,7 +262,7 @@ public struct Interaction: Codable, Identifiable, Equatable, FetchableRecord, Mu
|
||||||
self.body = body
|
self.body = body
|
||||||
self.timestampMs = timestampMs
|
self.timestampMs = timestampMs
|
||||||
self.receivedAtTimestampMs = receivedAtTimestampMs
|
self.receivedAtTimestampMs = receivedAtTimestampMs
|
||||||
self.wasRead = (wasRead && variant.canBeUnread)
|
self.wasRead = (wasRead || !variant.canBeUnread)
|
||||||
self.hasMention = hasMention
|
self.hasMention = hasMention
|
||||||
self.expiresInSeconds = expiresInSeconds
|
self.expiresInSeconds = expiresInSeconds
|
||||||
self.expiresStartedAtMs = expiresStartedAtMs
|
self.expiresStartedAtMs = expiresStartedAtMs
|
||||||
|
@ -304,7 +304,7 @@ public struct Interaction: Codable, Identifiable, Equatable, FetchableRecord, Mu
|
||||||
default: return timestampMs
|
default: return timestampMs
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
self.wasRead = (wasRead && variant.canBeUnread)
|
self.wasRead = (wasRead || !variant.canBeUnread)
|
||||||
self.hasMention = hasMention
|
self.hasMention = hasMention
|
||||||
self.expiresInSeconds = expiresInSeconds
|
self.expiresInSeconds = expiresInSeconds
|
||||||
self.expiresStartedAtMs = expiresStartedAtMs
|
self.expiresStartedAtMs = expiresStartedAtMs
|
||||||
|
@ -409,7 +409,7 @@ public extension Interaction {
|
||||||
body: (body ?? self.body),
|
body: (body ?? self.body),
|
||||||
timestampMs: (timestampMs ?? self.timestampMs),
|
timestampMs: (timestampMs ?? self.timestampMs),
|
||||||
receivedAtTimestampMs: self.receivedAtTimestampMs,
|
receivedAtTimestampMs: self.receivedAtTimestampMs,
|
||||||
wasRead: (wasRead ?? self.wasRead),
|
wasRead: ((wasRead ?? self.wasRead) || !self.variant.canBeUnread),
|
||||||
hasMention: (hasMention ?? self.hasMention),
|
hasMention: (hasMention ?? self.hasMention),
|
||||||
expiresInSeconds: (expiresInSeconds ?? self.expiresInSeconds),
|
expiresInSeconds: (expiresInSeconds ?? self.expiresInSeconds),
|
||||||
expiresStartedAtMs: (expiresStartedAtMs ?? self.expiresStartedAtMs),
|
expiresStartedAtMs: (expiresStartedAtMs ?? self.expiresStartedAtMs),
|
||||||
|
@ -453,6 +453,23 @@ public extension Interaction {
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Clear out any notifications for the interactions we mark as read
|
||||||
|
Environment.shared?.notificationsManager.wrappedValue?.cancelNotifications(
|
||||||
|
identifiers: interactionIds
|
||||||
|
.map { interactionId in
|
||||||
|
Interaction.notificationIdentifier(
|
||||||
|
for: interactionId,
|
||||||
|
threadId: threadId,
|
||||||
|
shouldGroupMessagesForThread: false
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.appending(Interaction.notificationIdentifier(
|
||||||
|
for: 0,
|
||||||
|
threadId: threadId,
|
||||||
|
shouldGroupMessagesForThread: true
|
||||||
|
))
|
||||||
|
)
|
||||||
|
|
||||||
// If we want to send read receipts then try to add the 'SendReadReceiptsJob'
|
// If we want to send read receipts then try to add the 'SendReadReceiptsJob'
|
||||||
if trySendReadReceipt {
|
if trySendReadReceipt {
|
||||||
JobRunner.upsert(
|
JobRunner.upsert(
|
||||||
|
@ -573,18 +590,27 @@ public extension Interaction {
|
||||||
|
|
||||||
var notificationIdentifiers: [String] {
|
var notificationIdentifiers: [String] {
|
||||||
[
|
[
|
||||||
notificationIdentifier(isBackgroundPoll: true),
|
notificationIdentifier(shouldGroupMessagesForThread: true),
|
||||||
notificationIdentifier(isBackgroundPoll: false)
|
notificationIdentifier(shouldGroupMessagesForThread: false)
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Functions
|
// MARK: - Functions
|
||||||
|
|
||||||
func notificationIdentifier(isBackgroundPoll: Bool) -> String {
|
func notificationIdentifier(shouldGroupMessagesForThread: Bool) -> String {
|
||||||
// When the app is in the background we want the notifications to be grouped to prevent spam
|
// When the app is in the background we want the notifications to be grouped to prevent spam
|
||||||
guard isBackgroundPoll else { return threadId }
|
return Interaction.notificationIdentifier(
|
||||||
|
for: (id ?? 0),
|
||||||
|
threadId: threadId,
|
||||||
|
shouldGroupMessagesForThread: shouldGroupMessagesForThread
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
return "\(threadId)-\(id ?? 0)"
|
fileprivate static func notificationIdentifier(for id: Int64, threadId: String, shouldGroupMessagesForThread: Bool) -> String {
|
||||||
|
// When the app is in the background we want the notifications to be grouped to prevent spam
|
||||||
|
guard !shouldGroupMessagesForThread else { return threadId }
|
||||||
|
|
||||||
|
return "\(threadId)-\(id)"
|
||||||
}
|
}
|
||||||
|
|
||||||
func markingAsDeleted() -> Interaction {
|
func markingAsDeleted() -> Interaction {
|
||||||
|
@ -598,7 +624,7 @@ public extension Interaction {
|
||||||
body: nil,
|
body: nil,
|
||||||
timestampMs: timestampMs,
|
timestampMs: timestampMs,
|
||||||
receivedAtTimestampMs: receivedAtTimestampMs,
|
receivedAtTimestampMs: receivedAtTimestampMs,
|
||||||
wasRead: (wasRead && Variant.standardIncomingDeleted.canBeUnread),
|
wasRead: (wasRead || !Variant.standardIncomingDeleted.canBeUnread),
|
||||||
hasMention: hasMention,
|
hasMention: hasMention,
|
||||||
expiresInSeconds: expiresInSeconds,
|
expiresInSeconds: expiresInSeconds,
|
||||||
expiresStartedAtMs: expiresStartedAtMs,
|
expiresStartedAtMs: expiresStartedAtMs,
|
||||||
|
|
|
@ -202,13 +202,14 @@ public extension SessionThread {
|
||||||
"""
|
"""
|
||||||
}
|
}
|
||||||
|
|
||||||
static func unreadMessageRequestsThreadIdQuery(userPublicKey: String, includeNonVisible: Bool = false) -> SQLRequest<String> {
|
static func unreadMessageRequestsCountQuery(userPublicKey: String, includeNonVisible: Bool = false) -> SQLRequest<Int> {
|
||||||
let thread: TypedTableAlias<SessionThread> = TypedTableAlias()
|
let thread: TypedTableAlias<SessionThread> = TypedTableAlias()
|
||||||
let interaction: TypedTableAlias<Interaction> = TypedTableAlias()
|
let interaction: TypedTableAlias<Interaction> = TypedTableAlias()
|
||||||
let contact: TypedTableAlias<Contact> = TypedTableAlias()
|
let contact: TypedTableAlias<Contact> = TypedTableAlias()
|
||||||
|
|
||||||
return """
|
return """
|
||||||
SELECT \(thread[.id])
|
SELECT COUNT(DISTINCT id) FROM (
|
||||||
|
SELECT \(thread[.id]) AS id
|
||||||
FROM \(SessionThread.self)
|
FROM \(SessionThread.self)
|
||||||
JOIN \(Interaction.self) ON (
|
JOIN \(Interaction.self) ON (
|
||||||
\(interaction[.threadId]) = \(thread[.id]) AND
|
\(interaction[.threadId]) = \(thread[.id]) AND
|
||||||
|
@ -218,7 +219,7 @@ public extension SessionThread {
|
||||||
WHERE (
|
WHERE (
|
||||||
\(SessionThread.isMessageRequest(userPublicKey: userPublicKey, includeNonVisible: includeNonVisible))
|
\(SessionThread.isMessageRequest(userPublicKey: userPublicKey, includeNonVisible: includeNonVisible))
|
||||||
)
|
)
|
||||||
GROUP BY \(thread[.id])
|
)
|
||||||
"""
|
"""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -276,8 +277,8 @@ public extension SessionThread {
|
||||||
// all the other message request threads have been read
|
// all the other message request threads have been read
|
||||||
if !hasHiddenMessageRequests {
|
if !hasHiddenMessageRequests {
|
||||||
let numUnreadMessageRequestThreads: Int = (try? SessionThread
|
let numUnreadMessageRequestThreads: Int = (try? SessionThread
|
||||||
.unreadMessageRequestsThreadIdQuery(userPublicKey: userPublicKey, includeNonVisible: true)
|
.unreadMessageRequestsCountQuery(userPublicKey: userPublicKey, includeNonVisible: true)
|
||||||
.fetchCount(db))
|
.fetchOne(db))
|
||||||
.defaulting(to: 1)
|
.defaulting(to: 1)
|
||||||
|
|
||||||
guard numUnreadMessageRequestThreads == 1 else { return false }
|
guard numUnreadMessageRequestThreads == 1 else { return false }
|
||||||
|
|
|
@ -37,8 +37,7 @@ public enum MessageReceiveJob: JobExecutor {
|
||||||
db,
|
db,
|
||||||
message: messageInfo.message,
|
message: messageInfo.message,
|
||||||
associatedWithProto: try SNProtoContent.parseData(messageInfo.serializedProtoData),
|
associatedWithProto: try SNProtoContent.parseData(messageInfo.serializedProtoData),
|
||||||
openGroupId: nil,
|
openGroupId: nil
|
||||||
isBackgroundPoll: details.isBackgroundPoll
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
catch {
|
catch {
|
||||||
|
@ -76,7 +75,7 @@ public enum MessageReceiveJob: JobExecutor {
|
||||||
.with(
|
.with(
|
||||||
details: Details(
|
details: Details(
|
||||||
messages: remainingMessagesToProcess,
|
messages: remainingMessagesToProcess,
|
||||||
isBackgroundPoll: details.isBackgroundPoll
|
calledFromBackgroundPoller: details.calledFromBackgroundPoller
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.defaulting(to: job)
|
.defaulting(to: job)
|
||||||
|
@ -164,14 +163,18 @@ extension MessageReceiveJob {
|
||||||
}
|
}
|
||||||
|
|
||||||
public let messages: [MessageInfo]
|
public let messages: [MessageInfo]
|
||||||
public let isBackgroundPoll: Bool
|
private let isBackgroundPoll: Bool
|
||||||
|
|
||||||
|
// Renamed variable for clarity (and didn't want to migrate old MessageReceiveJob
|
||||||
|
// values so didn't rename the original)
|
||||||
|
public var calledFromBackgroundPoller: Bool { isBackgroundPoll }
|
||||||
|
|
||||||
public init(
|
public init(
|
||||||
messages: [MessageInfo],
|
messages: [MessageInfo],
|
||||||
isBackgroundPoll: Bool
|
calledFromBackgroundPoller: Bool
|
||||||
) {
|
) {
|
||||||
self.messages = messages
|
self.messages = messages
|
||||||
self.isBackgroundPoll = isBackgroundPoll
|
self.isBackgroundPoll = calledFromBackgroundPoller
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,5 +6,4 @@ FOUNDATION_EXPORT const unsigned char SessionMessagingKitVersionString[];
|
||||||
#import <SessionMessagingKit/AppReadiness.h>
|
#import <SessionMessagingKit/AppReadiness.h>
|
||||||
#import <SessionMessagingKit/NSData+messagePadding.h>
|
#import <SessionMessagingKit/NSData+messagePadding.h>
|
||||||
#import <SessionMessagingKit/OWSAudioPlayer.h>
|
#import <SessionMessagingKit/OWSAudioPlayer.h>
|
||||||
#import <SessionMessagingKit/OWSBackgroundTask.h>
|
|
||||||
#import <SessionMessagingKit/OWSWindowManager.h>
|
#import <SessionMessagingKit/OWSWindowManager.h>
|
||||||
|
|
|
@ -512,7 +512,6 @@ public final class OpenGroupManager: NSObject {
|
||||||
messages: [OpenGroupAPI.Message],
|
messages: [OpenGroupAPI.Message],
|
||||||
for roomToken: String,
|
for roomToken: String,
|
||||||
on server: String,
|
on server: String,
|
||||||
isBackgroundPoll: Bool,
|
|
||||||
dependencies: OGMDependencies = OGMDependencies()
|
dependencies: OGMDependencies = OGMDependencies()
|
||||||
) {
|
) {
|
||||||
// Sorting the messages by server ID before importing them fixes an issue where messages
|
// Sorting the messages by server ID before importing them fixes an issue where messages
|
||||||
|
@ -564,7 +563,6 @@ public final class OpenGroupManager: NSObject {
|
||||||
message: messageInfo.message,
|
message: messageInfo.message,
|
||||||
associatedWithProto: try SNProtoContent.parseData(messageInfo.serializedProtoData),
|
associatedWithProto: try SNProtoContent.parseData(messageInfo.serializedProtoData),
|
||||||
openGroupId: openGroup.id,
|
openGroupId: openGroup.id,
|
||||||
isBackgroundPoll: isBackgroundPoll,
|
|
||||||
dependencies: dependencies
|
dependencies: dependencies
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -597,7 +595,6 @@ public final class OpenGroupManager: NSObject {
|
||||||
messages: [OpenGroupAPI.DirectMessage],
|
messages: [OpenGroupAPI.DirectMessage],
|
||||||
fromOutbox: Bool,
|
fromOutbox: Bool,
|
||||||
on server: String,
|
on server: String,
|
||||||
isBackgroundPoll: Bool,
|
|
||||||
dependencies: OGMDependencies = OGMDependencies()
|
dependencies: OGMDependencies = OGMDependencies()
|
||||||
) {
|
) {
|
||||||
// Don't need to do anything if we have no messages (it's a valid case)
|
// Don't need to do anything if we have no messages (it's a valid case)
|
||||||
|
@ -694,7 +691,6 @@ public final class OpenGroupManager: NSObject {
|
||||||
message: messageInfo.message,
|
message: messageInfo.message,
|
||||||
associatedWithProto: try SNProtoContent.parseData(messageInfo.serializedProtoData),
|
associatedWithProto: try SNProtoContent.parseData(messageInfo.serializedProtoData),
|
||||||
openGroupId: nil, // Intentionally nil as they are technically not open group messages
|
openGroupId: nil, // Intentionally nil as they are technically not open group messages
|
||||||
isBackgroundPoll: isBackgroundPoll,
|
|
||||||
dependencies: dependencies
|
dependencies: dependencies
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,7 +12,6 @@ extension MessageReceiver {
|
||||||
message: VisibleMessage,
|
message: VisibleMessage,
|
||||||
associatedWithProto proto: SNProtoContent,
|
associatedWithProto proto: SNProtoContent,
|
||||||
openGroupId: String?,
|
openGroupId: String?,
|
||||||
isBackgroundPoll: Bool,
|
|
||||||
dependencies: Dependencies = Dependencies()
|
dependencies: Dependencies = Dependencies()
|
||||||
) throws -> Int64 {
|
) throws -> Int64 {
|
||||||
guard let sender: String = message.sender, let dataMessage = proto.dataMessage else {
|
guard let sender: String = message.sender, let dataMessage = proto.dataMessage else {
|
||||||
|
@ -285,8 +284,7 @@ extension MessageReceiver {
|
||||||
.notifyUser(
|
.notifyUser(
|
||||||
db,
|
db,
|
||||||
for: interaction,
|
for: interaction,
|
||||||
in: thread,
|
in: thread
|
||||||
isBackgroundPoll: isBackgroundPoll
|
|
||||||
)
|
)
|
||||||
|
|
||||||
return interactionId
|
return interactionId
|
||||||
|
|
|
@ -180,7 +180,6 @@ public enum MessageReceiver {
|
||||||
message: Message,
|
message: Message,
|
||||||
associatedWithProto proto: SNProtoContent,
|
associatedWithProto proto: SNProtoContent,
|
||||||
openGroupId: String?,
|
openGroupId: String?,
|
||||||
isBackgroundPoll: Bool,
|
|
||||||
dependencies: SMKDependencies = SMKDependencies()
|
dependencies: SMKDependencies = SMKDependencies()
|
||||||
) throws {
|
) throws {
|
||||||
switch message {
|
switch message {
|
||||||
|
@ -216,8 +215,7 @@ public enum MessageReceiver {
|
||||||
db,
|
db,
|
||||||
message: message,
|
message: message,
|
||||||
associatedWithProto: proto,
|
associatedWithProto: proto,
|
||||||
openGroupId: openGroupId,
|
openGroupId: openGroupId
|
||||||
isBackgroundPoll: isBackgroundPoll
|
|
||||||
)
|
)
|
||||||
|
|
||||||
default: fatalError()
|
default: fatalError()
|
||||||
|
|
|
@ -4,8 +4,14 @@ import Foundation
|
||||||
import GRDB
|
import GRDB
|
||||||
|
|
||||||
public protocol NotificationsProtocol {
|
public protocol NotificationsProtocol {
|
||||||
func notifyUser(_ db: Database, for interaction: Interaction, in thread: SessionThread, isBackgroundPoll: Bool)
|
func notifyUser(_ db: Database, for interaction: Interaction, in thread: SessionThread)
|
||||||
func notifyUser(_ db: Database, forIncomingCall interaction: Interaction, in thread: SessionThread)
|
func notifyUser(_ db: Database, forIncomingCall interaction: Interaction, in thread: SessionThread)
|
||||||
func cancelNotifications(identifiers: [String])
|
func cancelNotifications(identifiers: [String])
|
||||||
func clearAllNotifications()
|
func clearAllNotifications()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public enum Notifications {
|
||||||
|
/// Delay notification of incoming messages when we want to group them (eg. during background polling) to avoid
|
||||||
|
/// firing too many notifications at the same time
|
||||||
|
public static let delayForGroupedNotifications: TimeInterval = 5
|
||||||
|
}
|
||||||
|
|
|
@ -145,7 +145,7 @@ public final class ClosedGroupPoller {
|
||||||
_ groupPublicKey: String,
|
_ groupPublicKey: String,
|
||||||
on queue: DispatchQueue = SessionSnodeKit.Threading.workQueue,
|
on queue: DispatchQueue = SessionSnodeKit.Threading.workQueue,
|
||||||
maxRetryCount: UInt = 0,
|
maxRetryCount: UInt = 0,
|
||||||
isBackgroundPoll: Bool = false,
|
calledFromBackgroundPoller: Bool = false,
|
||||||
isBackgroundPollValid: @escaping (() -> Bool) = { true },
|
isBackgroundPollValid: @escaping (() -> Bool) = { true },
|
||||||
poller: ClosedGroupPoller? = nil
|
poller: ClosedGroupPoller? = nil
|
||||||
) -> Promise<Void> {
|
) -> Promise<Void> {
|
||||||
|
@ -156,7 +156,7 @@ public final class ClosedGroupPoller {
|
||||||
|
|
||||||
return attempt(maxRetryCount: maxRetryCount, recoveringOn: queue) {
|
return attempt(maxRetryCount: maxRetryCount, recoveringOn: queue) {
|
||||||
guard
|
guard
|
||||||
(isBackgroundPoll && isBackgroundPollValid()) ||
|
(calledFromBackgroundPoller && isBackgroundPollValid()) ||
|
||||||
poller?.isPolling.wrappedValue[groupPublicKey] == true
|
poller?.isPolling.wrappedValue[groupPublicKey] == true
|
||||||
else { return Promise(error: Error.pollingCanceled) }
|
else { return Promise(error: Error.pollingCanceled) }
|
||||||
|
|
||||||
|
@ -178,7 +178,7 @@ public final class ClosedGroupPoller {
|
||||||
return when(resolved: promises)
|
return when(resolved: promises)
|
||||||
.then(on: queue) { messageResults -> Promise<Void> in
|
.then(on: queue) { messageResults -> Promise<Void> in
|
||||||
guard
|
guard
|
||||||
(isBackgroundPoll && isBackgroundPollValid()) ||
|
(calledFromBackgroundPoller && isBackgroundPollValid()) ||
|
||||||
poller?.isPolling.wrappedValue[groupPublicKey] == true
|
poller?.isPolling.wrappedValue[groupPublicKey] == true
|
||||||
else { return Promise.value(()) }
|
else { return Promise.value(()) }
|
||||||
|
|
||||||
|
@ -195,7 +195,7 @@ public final class ClosedGroupPoller {
|
||||||
|
|
||||||
// No need to do anything if there are no messages
|
// No need to do anything if there are no messages
|
||||||
guard !allMessages.isEmpty else {
|
guard !allMessages.isEmpty else {
|
||||||
if !isBackgroundPoll {
|
if !calledFromBackgroundPoller {
|
||||||
SNLog("Received no new messages in closed group with public key: \(groupPublicKey)")
|
SNLog("Received no new messages in closed group with public key: \(groupPublicKey)")
|
||||||
}
|
}
|
||||||
return Promise.value(())
|
return Promise.value(())
|
||||||
|
@ -221,7 +221,7 @@ public final class ClosedGroupPoller {
|
||||||
// In the background ignore 'SQLITE_ABORT' (it generally means
|
// In the background ignore 'SQLITE_ABORT' (it generally means
|
||||||
// the BackgroundPoller has timed out
|
// the BackgroundPoller has timed out
|
||||||
case DatabaseError.SQLITE_ABORT:
|
case DatabaseError.SQLITE_ABORT:
|
||||||
guard !isBackgroundPoll else { break }
|
guard !calledFromBackgroundPoller else { break }
|
||||||
|
|
||||||
SNLog("Failed to the database being suspended (running in background with no background task).")
|
SNLog("Failed to the database being suspended (running in background with no background task).")
|
||||||
break
|
break
|
||||||
|
@ -241,16 +241,16 @@ public final class ClosedGroupPoller {
|
||||||
threadId: groupPublicKey,
|
threadId: groupPublicKey,
|
||||||
details: MessageReceiveJob.Details(
|
details: MessageReceiveJob.Details(
|
||||||
messages: processedMessages.map { $0.messageInfo },
|
messages: processedMessages.map { $0.messageInfo },
|
||||||
isBackgroundPoll: isBackgroundPoll
|
calledFromBackgroundPoller: calledFromBackgroundPoller
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
// If we are force-polling then add to the JobRunner so they are persistent and will retry on
|
// If we are force-polling then add to the JobRunner so they are persistent and will retry on
|
||||||
// the next app run if they fail but don't let them auto-start
|
// the next app run if they fail but don't let them auto-start
|
||||||
JobRunner.add(db, job: jobToRun, canStartJob: !isBackgroundPoll)
|
JobRunner.add(db, job: jobToRun, canStartJob: !calledFromBackgroundPoller)
|
||||||
}
|
}
|
||||||
|
|
||||||
if isBackgroundPoll {
|
if calledFromBackgroundPoller {
|
||||||
// We want to try to handle the receive jobs immediately in the background
|
// We want to try to handle the receive jobs immediately in the background
|
||||||
promises = promises.appending(
|
promises = promises.appending(
|
||||||
jobToRun.map { job -> Promise<Void> in
|
jobToRun.map { job -> Promise<Void> in
|
||||||
|
@ -278,7 +278,7 @@ public final class ClosedGroupPoller {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !isBackgroundPoll {
|
if !calledFromBackgroundPoller {
|
||||||
promise.catch2 { error in
|
promise.catch2 { error in
|
||||||
SNLog("Polling failed for closed group with public key: \(groupPublicKey) due to error: \(error).")
|
SNLog("Polling failed for closed group with public key: \(groupPublicKey) due to error: \(error).")
|
||||||
}
|
}
|
||||||
|
|
|
@ -67,12 +67,12 @@ extension OpenGroupAPI {
|
||||||
|
|
||||||
@discardableResult
|
@discardableResult
|
||||||
public func poll(using dependencies: OpenGroupManager.OGMDependencies = OpenGroupManager.OGMDependencies()) -> Promise<Void> {
|
public func poll(using dependencies: OpenGroupManager.OGMDependencies = OpenGroupManager.OGMDependencies()) -> Promise<Void> {
|
||||||
return poll(isBackgroundPoll: false, isPostCapabilitiesRetry: false, using: dependencies)
|
return poll(calledFromBackgroundPoller: false, isPostCapabilitiesRetry: false, using: dependencies)
|
||||||
}
|
}
|
||||||
|
|
||||||
@discardableResult
|
@discardableResult
|
||||||
public func poll(
|
public func poll(
|
||||||
isBackgroundPoll: Bool,
|
calledFromBackgroundPoller: Bool,
|
||||||
isBackgroundPollerValid: @escaping (() -> Bool) = { true },
|
isBackgroundPollerValid: @escaping (() -> Bool) = { true },
|
||||||
isPostCapabilitiesRetry: Bool,
|
isPostCapabilitiesRetry: Bool,
|
||||||
using dependencies: OpenGroupManager.OGMDependencies = OpenGroupManager.OGMDependencies()
|
using dependencies: OpenGroupManager.OGMDependencies = OpenGroupManager.OGMDependencies()
|
||||||
|
@ -107,7 +107,7 @@ extension OpenGroupAPI {
|
||||||
.map(on: OpenGroupAPI.workQueue) { (failureCount, $0) }
|
.map(on: OpenGroupAPI.workQueue) { (failureCount, $0) }
|
||||||
}
|
}
|
||||||
.done(on: OpenGroupAPI.workQueue) { [weak self] failureCount, response in
|
.done(on: OpenGroupAPI.workQueue) { [weak self] failureCount, response in
|
||||||
guard !isBackgroundPoll || isBackgroundPollerValid() else {
|
guard !calledFromBackgroundPoller || isBackgroundPollerValid() else {
|
||||||
// If this was a background poll and the background poll is no longer valid
|
// If this was a background poll and the background poll is no longer valid
|
||||||
// then just stop
|
// then just stop
|
||||||
self?.isPolling = false
|
self?.isPolling = false
|
||||||
|
@ -119,7 +119,6 @@ extension OpenGroupAPI {
|
||||||
self?.handlePollResponse(
|
self?.handlePollResponse(
|
||||||
response,
|
response,
|
||||||
failureCount: failureCount,
|
failureCount: failureCount,
|
||||||
isBackgroundPoll: isBackgroundPoll,
|
|
||||||
using: dependencies
|
using: dependencies
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -133,7 +132,7 @@ extension OpenGroupAPI {
|
||||||
seal.fulfill(())
|
seal.fulfill(())
|
||||||
}
|
}
|
||||||
.catch(on: OpenGroupAPI.workQueue) { [weak self] error in
|
.catch(on: OpenGroupAPI.workQueue) { [weak self] error in
|
||||||
guard !isBackgroundPoll || isBackgroundPollerValid() else {
|
guard !calledFromBackgroundPoller || isBackgroundPollerValid() else {
|
||||||
// If this was a background poll and the background poll is no longer valid
|
// If this was a background poll and the background poll is no longer valid
|
||||||
// then just stop
|
// then just stop
|
||||||
self?.isPolling = false
|
self?.isPolling = false
|
||||||
|
@ -145,7 +144,8 @@ extension OpenGroupAPI {
|
||||||
// method will always resolve)
|
// method will always resolve)
|
||||||
self?.updateCapabilitiesAndRetryIfNeeded(
|
self?.updateCapabilitiesAndRetryIfNeeded(
|
||||||
server: server,
|
server: server,
|
||||||
isBackgroundPoll: isBackgroundPoll,
|
calledFromBackgroundPoller: calledFromBackgroundPoller,
|
||||||
|
isBackgroundPollerValid: isBackgroundPollerValid,
|
||||||
isPostCapabilitiesRetry: isPostCapabilitiesRetry,
|
isPostCapabilitiesRetry: isPostCapabilitiesRetry,
|
||||||
error: error
|
error: error
|
||||||
)
|
)
|
||||||
|
@ -186,7 +186,8 @@ extension OpenGroupAPI {
|
||||||
|
|
||||||
private func updateCapabilitiesAndRetryIfNeeded(
|
private func updateCapabilitiesAndRetryIfNeeded(
|
||||||
server: String,
|
server: String,
|
||||||
isBackgroundPoll: Bool,
|
calledFromBackgroundPoller: Bool,
|
||||||
|
isBackgroundPollerValid: @escaping (() -> Bool) = { true },
|
||||||
isPostCapabilitiesRetry: Bool,
|
isPostCapabilitiesRetry: Bool,
|
||||||
error: Error,
|
error: Error,
|
||||||
using dependencies: OpenGroupManager.OGMDependencies = OpenGroupManager.OGMDependencies()
|
using dependencies: OpenGroupManager.OGMDependencies = OpenGroupManager.OGMDependencies()
|
||||||
|
@ -233,7 +234,8 @@ extension OpenGroupAPI {
|
||||||
// Regardless of the outcome we can just resolve this
|
// Regardless of the outcome we can just resolve this
|
||||||
// immediately as it'll handle it's own response
|
// immediately as it'll handle it's own response
|
||||||
return strongSelf.poll(
|
return strongSelf.poll(
|
||||||
isBackgroundPoll: isBackgroundPoll,
|
calledFromBackgroundPoller: calledFromBackgroundPoller,
|
||||||
|
isBackgroundPollerValid: isBackgroundPollerValid,
|
||||||
isPostCapabilitiesRetry: true,
|
isPostCapabilitiesRetry: true,
|
||||||
using: dependencies
|
using: dependencies
|
||||||
)
|
)
|
||||||
|
@ -251,7 +253,6 @@ extension OpenGroupAPI {
|
||||||
private func handlePollResponse(
|
private func handlePollResponse(
|
||||||
_ response: PollResponse,
|
_ response: PollResponse,
|
||||||
failureCount: Int64,
|
failureCount: Int64,
|
||||||
isBackgroundPoll: Bool,
|
|
||||||
using dependencies: OpenGroupManager.OGMDependencies = OpenGroupManager.OGMDependencies()
|
using dependencies: OpenGroupManager.OGMDependencies = OpenGroupManager.OGMDependencies()
|
||||||
) {
|
) {
|
||||||
let server: String = self.server
|
let server: String = self.server
|
||||||
|
@ -440,7 +441,6 @@ extension OpenGroupAPI {
|
||||||
messages: responseBody.compactMap { $0.value },
|
messages: responseBody.compactMap { $0.value },
|
||||||
for: roomToken,
|
for: roomToken,
|
||||||
on: server,
|
on: server,
|
||||||
isBackgroundPoll: isBackgroundPoll,
|
|
||||||
dependencies: dependencies
|
dependencies: dependencies
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -464,7 +464,6 @@ extension OpenGroupAPI {
|
||||||
messages: messages,
|
messages: messages,
|
||||||
fromOutbox: fromOutbox,
|
fromOutbox: fromOutbox,
|
||||||
on: server,
|
on: server,
|
||||||
isBackgroundPoll: isBackgroundPoll,
|
|
||||||
dependencies: dependencies
|
dependencies: dependencies
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -173,7 +173,7 @@ public final class Poller {
|
||||||
threadId: threadId,
|
threadId: threadId,
|
||||||
details: MessageReceiveJob.Details(
|
details: MessageReceiveJob.Details(
|
||||||
messages: threadMessages.map { $0.messageInfo },
|
messages: threadMessages.map { $0.messageInfo },
|
||||||
isBackgroundPoll: false
|
calledFromBackgroundPoller: false
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
|
@ -350,7 +350,6 @@ 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 baseQuery(
|
static func baseQuery(
|
||||||
userPublicKey: String,
|
userPublicKey: String,
|
||||||
filterSQL: SQL,
|
|
||||||
groupSQL: SQL,
|
groupSQL: SQL,
|
||||||
orderSQL: SQL
|
orderSQL: SQL
|
||||||
) -> (([Int64]) -> AdaptedFetchRequest<SQLRequest<SessionThreadViewModel>>) {
|
) -> (([Int64]) -> AdaptedFetchRequest<SQLRequest<SessionThreadViewModel>>) {
|
||||||
|
@ -368,6 +367,7 @@ public extension SessionThreadViewModel {
|
||||||
let interactionAttachment: TypedTableAlias<InteractionAttachment> = TypedTableAlias()
|
let interactionAttachment: TypedTableAlias<InteractionAttachment> = TypedTableAlias()
|
||||||
let profile: TypedTableAlias<Profile> = TypedTableAlias()
|
let profile: TypedTableAlias<Profile> = TypedTableAlias()
|
||||||
|
|
||||||
|
let interactionTimestampMsColumnLiteral: SQL = SQL(stringLiteral: Interaction.Columns.timestampMs.name)
|
||||||
let profileIdColumnLiteral: SQL = SQL(stringLiteral: Profile.Columns.id.name)
|
let profileIdColumnLiteral: SQL = SQL(stringLiteral: Profile.Columns.id.name)
|
||||||
let profileNicknameColumnLiteral: SQL = SQL(stringLiteral: Profile.Columns.nickname.name)
|
let profileNicknameColumnLiteral: SQL = SQL(stringLiteral: Profile.Columns.nickname.name)
|
||||||
let profileNameColumnLiteral: SQL = SQL(stringLiteral: Profile.Columns.name.name)
|
let profileNameColumnLiteral: SQL = SQL(stringLiteral: Profile.Columns.name.name)
|
||||||
|
@ -412,7 +412,7 @@ public extension SessionThreadViewModel {
|
||||||
|
|
||||||
\(Interaction.self).\(ViewModel.interactionIdKey),
|
\(Interaction.self).\(ViewModel.interactionIdKey),
|
||||||
\(Interaction.self).\(ViewModel.interactionVariantKey),
|
\(Interaction.self).\(ViewModel.interactionVariantKey),
|
||||||
\(Interaction.self).\(ViewModel.interactionTimestampMsKey),
|
\(Interaction.self).\(interactionTimestampMsColumnLiteral) AS \(ViewModel.interactionTimestampMsKey),
|
||||||
\(Interaction.self).\(ViewModel.interactionBodyKey),
|
\(Interaction.self).\(ViewModel.interactionBodyKey),
|
||||||
|
|
||||||
-- Default to 'sending' assuming non-processed interaction when null
|
-- Default to 'sending' assuming non-processed interaction when null
|
||||||
|
@ -440,7 +440,7 @@ public extension SessionThreadViewModel {
|
||||||
\(interaction[.id]) AS \(ViewModel.interactionIdKey),
|
\(interaction[.id]) AS \(ViewModel.interactionIdKey),
|
||||||
\(interaction[.threadId]),
|
\(interaction[.threadId]),
|
||||||
\(interaction[.variant]) AS \(ViewModel.interactionVariantKey),
|
\(interaction[.variant]) AS \(ViewModel.interactionVariantKey),
|
||||||
MAX(\(interaction[.timestampMs])) AS \(ViewModel.interactionTimestampMsKey),
|
MAX(\(interaction[.timestampMs])) AS \(interactionTimestampMsColumnLiteral),
|
||||||
\(interaction[.body]) AS \(ViewModel.interactionBodyKey),
|
\(interaction[.body]) AS \(ViewModel.interactionBodyKey),
|
||||||
\(interaction[.authorId]),
|
\(interaction[.authorId]),
|
||||||
\(interaction[.linkPreviewUrl]),
|
\(interaction[.linkPreviewUrl]),
|
||||||
|
@ -461,7 +461,7 @@ public extension SessionThreadViewModel {
|
||||||
LEFT JOIN \(LinkPreview.self) ON (
|
LEFT JOIN \(LinkPreview.self) ON (
|
||||||
\(linkPreview[.url]) = \(interaction[.linkPreviewUrl]) AND
|
\(linkPreview[.url]) = \(interaction[.linkPreviewUrl]) AND
|
||||||
\(SQL("\(linkPreview[.variant]) = \(LinkPreview.Variant.openGroupInvitation)")) AND
|
\(SQL("\(linkPreview[.variant]) = \(LinkPreview.Variant.openGroupInvitation)")) AND
|
||||||
\(Interaction.linkPreviewFilterLiteral(timestampColumn: ViewModel.interactionTimestampMsKey))
|
\(Interaction.linkPreviewFilterLiteral(timestampColumn: interactionTimestampMsColumnLiteral))
|
||||||
)
|
)
|
||||||
LEFT JOIN \(InteractionAttachment.self) AS \(firstInteractionAttachmentLiteral) ON (
|
LEFT JOIN \(InteractionAttachment.self) AS \(firstInteractionAttachmentLiteral) ON (
|
||||||
\(firstInteractionAttachmentLiteral).\(interactionAttachmentAlbumIndexColumnLiteral) = 0 AND
|
\(firstInteractionAttachmentLiteral).\(interactionAttachmentAlbumIndexColumnLiteral) = 0 AND
|
||||||
|
@ -545,12 +545,14 @@ public extension SessionThreadViewModel {
|
||||||
let contact: TypedTableAlias<Contact> = TypedTableAlias()
|
let contact: TypedTableAlias<Contact> = TypedTableAlias()
|
||||||
let interaction: TypedTableAlias<Interaction> = TypedTableAlias()
|
let interaction: TypedTableAlias<Interaction> = TypedTableAlias()
|
||||||
|
|
||||||
|
let interactionTimestampMsColumnLiteral: SQL = SQL(stringLiteral: Interaction.Columns.timestampMs.name)
|
||||||
|
|
||||||
return """
|
return """
|
||||||
LEFT JOIN \(Contact.self) ON \(contact[.id]) = \(thread[.id])
|
LEFT JOIN \(Contact.self) ON \(contact[.id]) = \(thread[.id])
|
||||||
LEFT JOIN (
|
LEFT JOIN (
|
||||||
SELECT
|
SELECT
|
||||||
\(interaction[.threadId]),
|
\(interaction[.threadId]),
|
||||||
MAX(\(interaction[.timestampMs])) AS \(ViewModel.interactionTimestampMsKey)
|
MAX(\(interaction[.timestampMs])) AS \(interactionTimestampMsColumnLiteral)
|
||||||
FROM \(Interaction.self)
|
FROM \(Interaction.self)
|
||||||
WHERE \(SQL("\(interaction[.variant]) != \(Interaction.Variant.standardIncomingDeleted)"))
|
WHERE \(SQL("\(interaction[.variant]) != \(Interaction.Variant.standardIncomingDeleted)"))
|
||||||
GROUP BY \(interaction[.threadId])
|
GROUP BY \(interaction[.threadId])
|
||||||
|
@ -561,6 +563,7 @@ public extension SessionThreadViewModel {
|
||||||
static func homeFilterSQL(userPublicKey: String) -> SQL {
|
static func homeFilterSQL(userPublicKey: String) -> SQL {
|
||||||
let thread: TypedTableAlias<SessionThread> = TypedTableAlias()
|
let thread: TypedTableAlias<SessionThread> = TypedTableAlias()
|
||||||
let contact: TypedTableAlias<Contact> = TypedTableAlias()
|
let contact: TypedTableAlias<Contact> = TypedTableAlias()
|
||||||
|
let interaction: TypedTableAlias<Interaction> = TypedTableAlias()
|
||||||
|
|
||||||
return """
|
return """
|
||||||
\(thread[.shouldBeVisible]) = true AND (
|
\(thread[.shouldBeVisible]) = true AND (
|
||||||
|
@ -571,7 +574,7 @@ public extension SessionThreadViewModel {
|
||||||
) AND (
|
) AND (
|
||||||
-- Only show the 'Note to Self' thread if it has an interaction
|
-- Only show the 'Note to Self' thread if it has an interaction
|
||||||
\(SQL("\(thread[.id]) != \(userPublicKey)")) OR
|
\(SQL("\(thread[.id]) != \(userPublicKey)")) OR
|
||||||
\(Interaction.self).\(ViewModel.interactionTimestampMsKey) IS NOT NULL
|
\(interaction[.timestampMs]) IS NOT NULL
|
||||||
)
|
)
|
||||||
"""
|
"""
|
||||||
}
|
}
|
||||||
|
@ -598,14 +601,16 @@ public extension SessionThreadViewModel {
|
||||||
|
|
||||||
static let homeOrderSQL: SQL = {
|
static let homeOrderSQL: SQL = {
|
||||||
let thread: TypedTableAlias<SessionThread> = TypedTableAlias()
|
let thread: TypedTableAlias<SessionThread> = TypedTableAlias()
|
||||||
|
let interaction: TypedTableAlias<Interaction> = TypedTableAlias()
|
||||||
|
|
||||||
return SQL("\(thread[.isPinned]) DESC, IFNULL(\(Interaction.self).\(ViewModel.interactionTimestampMsKey), (\(thread[.creationDateTimestamp]) * 1000)) DESC")
|
return SQL("\(thread[.isPinned]) DESC, IFNULL(\(interaction[.timestampMs]), (\(thread[.creationDateTimestamp]) * 1000)) DESC")
|
||||||
}()
|
}()
|
||||||
|
|
||||||
static let messageRequetsOrderSQL: SQL = {
|
static let messageRequetsOrderSQL: SQL = {
|
||||||
let thread: TypedTableAlias<SessionThread> = TypedTableAlias()
|
let thread: TypedTableAlias<SessionThread> = TypedTableAlias()
|
||||||
|
let interaction: TypedTableAlias<Interaction> = TypedTableAlias()
|
||||||
|
|
||||||
return SQL("IFNULL(\(Interaction.self).\(ViewModel.interactionTimestampMsKey), (\(thread[.creationDateTimestamp]) * 1000)) DESC")
|
return SQL("IFNULL(\(interaction[.timestampMs]), (\(thread[.creationDateTimestamp]) * 1000)) DESC")
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -684,7 +689,7 @@ public extension SessionThreadViewModel {
|
||||||
SUM(\(interaction[.wasRead]) = false) AS \(ViewModel.threadUnreadCountKey)
|
SUM(\(interaction[.wasRead]) = false) AS \(ViewModel.threadUnreadCountKey)
|
||||||
|
|
||||||
FROM \(Interaction.self)
|
FROM \(Interaction.self)
|
||||||
GROUP BY \(interaction[.threadId])
|
WHERE \(SQL("\(interaction[.threadId]) = \(threadId)"))
|
||||||
) AS \(Interaction.self) ON \(interaction[.threadId]) = \(thread[.id])
|
) AS \(Interaction.self) ON \(interaction[.threadId]) = \(thread[.id])
|
||||||
|
|
||||||
LEFT JOIN \(Profile.self) AS \(ViewModel.contactProfileKey) ON \(ViewModel.contactProfileKey).\(profileIdColumnLiteral) = \(thread[.id])
|
LEFT JOIN \(Profile.self) AS \(ViewModel.contactProfileKey) ON \(ViewModel.contactProfileKey).\(profileIdColumnLiteral) = \(thread[.id])
|
||||||
|
@ -698,17 +703,15 @@ public extension SessionThreadViewModel {
|
||||||
LEFT JOIN (
|
LEFT JOIN (
|
||||||
SELECT
|
SELECT
|
||||||
\(groupMember[.groupId]),
|
\(groupMember[.groupId]),
|
||||||
COUNT(*) AS \(ViewModel.closedGroupUserCountKey)
|
COUNT(\(groupMember.alias[Column.rowID])) AS \(ViewModel.closedGroupUserCountKey)
|
||||||
FROM \(GroupMember.self)
|
FROM \(GroupMember.self)
|
||||||
WHERE (
|
WHERE (
|
||||||
\(SQL("\(groupMember[.groupId]) = \(threadId)")) AND
|
\(SQL("\(groupMember[.groupId]) = \(threadId)")) AND
|
||||||
\(SQL("\(groupMember[.role]) = \(GroupMember.Role.standard)"))
|
\(SQL("\(groupMember[.role]) = \(GroupMember.Role.standard)"))
|
||||||
)
|
)
|
||||||
GROUP BY \(groupMember[.groupId])
|
|
||||||
) AS \(closedGroupUserCountTableLiteral) ON \(SQL("\(closedGroupUserCountTableLiteral).\(groupMemberGroupIdColumnLiteral) = \(threadId)"))
|
) AS \(closedGroupUserCountTableLiteral) ON \(SQL("\(closedGroupUserCountTableLiteral).\(groupMemberGroupIdColumnLiteral) = \(threadId)"))
|
||||||
|
|
||||||
WHERE \(SQL("\(thread[.id]) = \(threadId)"))
|
WHERE \(SQL("\(thread[.id]) = \(threadId)"))
|
||||||
GROUP BY \(thread[.id])
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
return request.adapted { db in
|
return request.adapted { db in
|
||||||
|
|
|
@ -2120,7 +2120,6 @@ class OpenGroupManagerSpec: QuickSpec {
|
||||||
],
|
],
|
||||||
for: "testRoom",
|
for: "testRoom",
|
||||||
on: "testServer",
|
on: "testServer",
|
||||||
isBackgroundPoll: false,
|
|
||||||
dependencies: dependencies
|
dependencies: dependencies
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -2142,7 +2141,6 @@ class OpenGroupManagerSpec: QuickSpec {
|
||||||
messages: [],
|
messages: [],
|
||||||
for: "testRoom",
|
for: "testRoom",
|
||||||
on: "testServer",
|
on: "testServer",
|
||||||
isBackgroundPoll: false,
|
|
||||||
dependencies: dependencies
|
dependencies: dependencies
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -2182,7 +2180,6 @@ class OpenGroupManagerSpec: QuickSpec {
|
||||||
],
|
],
|
||||||
for: "testRoom",
|
for: "testRoom",
|
||||||
on: "testServer",
|
on: "testServer",
|
||||||
isBackgroundPoll: false,
|
|
||||||
dependencies: dependencies
|
dependencies: dependencies
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -2215,7 +2212,6 @@ class OpenGroupManagerSpec: QuickSpec {
|
||||||
],
|
],
|
||||||
for: "testRoom",
|
for: "testRoom",
|
||||||
on: "testServer",
|
on: "testServer",
|
||||||
isBackgroundPoll: false,
|
|
||||||
dependencies: dependencies
|
dependencies: dependencies
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -2230,7 +2226,6 @@ class OpenGroupManagerSpec: QuickSpec {
|
||||||
messages: [testMessage],
|
messages: [testMessage],
|
||||||
for: "testRoom",
|
for: "testRoom",
|
||||||
on: "testServer",
|
on: "testServer",
|
||||||
isBackgroundPoll: false,
|
|
||||||
dependencies: dependencies
|
dependencies: dependencies
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -2260,7 +2255,6 @@ class OpenGroupManagerSpec: QuickSpec {
|
||||||
],
|
],
|
||||||
for: "testRoom",
|
for: "testRoom",
|
||||||
on: "testServer",
|
on: "testServer",
|
||||||
isBackgroundPoll: false,
|
|
||||||
dependencies: dependencies
|
dependencies: dependencies
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -2298,7 +2292,6 @@ class OpenGroupManagerSpec: QuickSpec {
|
||||||
],
|
],
|
||||||
for: "testRoom",
|
for: "testRoom",
|
||||||
on: "testServer",
|
on: "testServer",
|
||||||
isBackgroundPoll: false,
|
|
||||||
dependencies: dependencies
|
dependencies: dependencies
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -2327,7 +2320,6 @@ class OpenGroupManagerSpec: QuickSpec {
|
||||||
],
|
],
|
||||||
for: "testRoom",
|
for: "testRoom",
|
||||||
on: "testServer",
|
on: "testServer",
|
||||||
isBackgroundPoll: false,
|
|
||||||
dependencies: dependencies
|
dependencies: dependencies
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -2379,7 +2371,6 @@ class OpenGroupManagerSpec: QuickSpec {
|
||||||
messages: [],
|
messages: [],
|
||||||
fromOutbox: false,
|
fromOutbox: false,
|
||||||
on: "testServer",
|
on: "testServer",
|
||||||
isBackgroundPoll: false,
|
|
||||||
dependencies: dependencies
|
dependencies: dependencies
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -2413,7 +2404,6 @@ class OpenGroupManagerSpec: QuickSpec {
|
||||||
messages: [testDirectMessage],
|
messages: [testDirectMessage],
|
||||||
fromOutbox: false,
|
fromOutbox: false,
|
||||||
on: "testServer",
|
on: "testServer",
|
||||||
isBackgroundPoll: false,
|
|
||||||
dependencies: dependencies
|
dependencies: dependencies
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -2452,7 +2442,6 @@ class OpenGroupManagerSpec: QuickSpec {
|
||||||
messages: [testDirectMessage],
|
messages: [testDirectMessage],
|
||||||
fromOutbox: false,
|
fromOutbox: false,
|
||||||
on: "testServer",
|
on: "testServer",
|
||||||
isBackgroundPoll: false,
|
|
||||||
dependencies: dependencies
|
dependencies: dependencies
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -2478,7 +2467,6 @@ class OpenGroupManagerSpec: QuickSpec {
|
||||||
messages: [testDirectMessage],
|
messages: [testDirectMessage],
|
||||||
fromOutbox: false,
|
fromOutbox: false,
|
||||||
on: "testServer",
|
on: "testServer",
|
||||||
isBackgroundPoll: false,
|
|
||||||
dependencies: dependencies
|
dependencies: dependencies
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -2509,7 +2497,6 @@ class OpenGroupManagerSpec: QuickSpec {
|
||||||
messages: [testDirectMessage],
|
messages: [testDirectMessage],
|
||||||
fromOutbox: false,
|
fromOutbox: false,
|
||||||
on: "testServer",
|
on: "testServer",
|
||||||
isBackgroundPoll: false,
|
|
||||||
dependencies: dependencies
|
dependencies: dependencies
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -2524,7 +2511,6 @@ class OpenGroupManagerSpec: QuickSpec {
|
||||||
messages: [testDirectMessage],
|
messages: [testDirectMessage],
|
||||||
fromOutbox: false,
|
fromOutbox: false,
|
||||||
on: "testServer",
|
on: "testServer",
|
||||||
isBackgroundPoll: false,
|
|
||||||
dependencies: dependencies
|
dependencies: dependencies
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -2549,7 +2535,6 @@ class OpenGroupManagerSpec: QuickSpec {
|
||||||
],
|
],
|
||||||
fromOutbox: false,
|
fromOutbox: false,
|
||||||
on: "testServer",
|
on: "testServer",
|
||||||
isBackgroundPoll: false,
|
|
||||||
dependencies: dependencies
|
dependencies: dependencies
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -2576,7 +2561,6 @@ class OpenGroupManagerSpec: QuickSpec {
|
||||||
messages: [testDirectMessage],
|
messages: [testDirectMessage],
|
||||||
fromOutbox: true,
|
fromOutbox: true,
|
||||||
on: "testServer",
|
on: "testServer",
|
||||||
isBackgroundPoll: false,
|
|
||||||
dependencies: dependencies
|
dependencies: dependencies
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -2607,7 +2591,6 @@ class OpenGroupManagerSpec: QuickSpec {
|
||||||
messages: [testDirectMessage],
|
messages: [testDirectMessage],
|
||||||
fromOutbox: true,
|
fromOutbox: true,
|
||||||
on: "testServer",
|
on: "testServer",
|
||||||
isBackgroundPoll: false,
|
|
||||||
dependencies: dependencies
|
dependencies: dependencies
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -2623,7 +2606,6 @@ class OpenGroupManagerSpec: QuickSpec {
|
||||||
messages: [testDirectMessage],
|
messages: [testDirectMessage],
|
||||||
fromOutbox: true,
|
fromOutbox: true,
|
||||||
on: "testServer",
|
on: "testServer",
|
||||||
isBackgroundPoll: false,
|
|
||||||
dependencies: dependencies
|
dependencies: dependencies
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -2659,7 +2641,6 @@ class OpenGroupManagerSpec: QuickSpec {
|
||||||
messages: [testDirectMessage],
|
messages: [testDirectMessage],
|
||||||
fromOutbox: true,
|
fromOutbox: true,
|
||||||
on: "testServer",
|
on: "testServer",
|
||||||
isBackgroundPoll: false,
|
|
||||||
dependencies: dependencies
|
dependencies: dependencies
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -2674,7 +2655,6 @@ class OpenGroupManagerSpec: QuickSpec {
|
||||||
messages: [testDirectMessage],
|
messages: [testDirectMessage],
|
||||||
fromOutbox: true,
|
fromOutbox: true,
|
||||||
on: "testServer",
|
on: "testServer",
|
||||||
isBackgroundPoll: false,
|
|
||||||
dependencies: dependencies
|
dependencies: dependencies
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -2699,7 +2679,6 @@ class OpenGroupManagerSpec: QuickSpec {
|
||||||
],
|
],
|
||||||
fromOutbox: true,
|
fromOutbox: true,
|
||||||
on: "testServer",
|
on: "testServer",
|
||||||
isBackgroundPoll: false,
|
|
||||||
dependencies: dependencies
|
dependencies: dependencies
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,7 +9,7 @@ import SessionMessagingKit
|
||||||
public class NSENotificationPresenter: NSObject, NotificationsProtocol {
|
public class NSENotificationPresenter: NSObject, NotificationsProtocol {
|
||||||
private var notifications: [String: UNNotificationRequest] = [:]
|
private var notifications: [String: UNNotificationRequest] = [:]
|
||||||
|
|
||||||
public func notifyUser(_ db: Database, for interaction: Interaction, in thread: SessionThread, isBackgroundPoll: Bool) {
|
public func notifyUser(_ db: Database, for interaction: Interaction, in thread: SessionThread) {
|
||||||
let isMessageRequest: Bool = thread.isMessageRequest(db, includeNonVisible: true)
|
let isMessageRequest: Bool = thread.isMessageRequest(db, includeNonVisible: true)
|
||||||
|
|
||||||
// Ensure we should be showing a notification for the thread
|
// Ensure we should be showing a notification for the thread
|
||||||
|
@ -18,6 +18,12 @@ public class NSENotificationPresenter: NSObject, NotificationsProtocol {
|
||||||
}
|
}
|
||||||
|
|
||||||
let senderName: String = Profile.displayName(db, id: interaction.authorId, threadVariant: thread.variant)
|
let senderName: String = Profile.displayName(db, id: interaction.authorId, threadVariant: thread.variant)
|
||||||
|
let groupName: String = SessionThread.displayName(
|
||||||
|
threadId: thread.id,
|
||||||
|
variant: thread.variant,
|
||||||
|
closedGroupName: (try? thread.closedGroup.fetchOne(db))?.name,
|
||||||
|
openGroupName: (try? thread.openGroup.fetchOne(db))?.name
|
||||||
|
)
|
||||||
var notificationTitle: String = senderName
|
var notificationTitle: String = senderName
|
||||||
|
|
||||||
if thread.variant == .closedGroup || thread.variant == .openGroup {
|
if thread.variant == .closedGroup || thread.variant == .openGroup {
|
||||||
|
@ -26,22 +32,11 @@ public class NSENotificationPresenter: NSObject, NotificationsProtocol {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
notificationTitle = {
|
notificationTitle = String(
|
||||||
let groupName: String = SessionThread.displayName(
|
|
||||||
threadId: thread.id,
|
|
||||||
variant: thread.variant,
|
|
||||||
closedGroupName: (try? thread.closedGroup.fetchOne(db))?.name,
|
|
||||||
openGroupName: (try? thread.openGroup.fetchOne(db))?.name
|
|
||||||
)
|
|
||||||
|
|
||||||
guard !isBackgroundPoll else { return groupName }
|
|
||||||
|
|
||||||
return String(
|
|
||||||
format: NotificationStrings.incomingGroupMessageTitleFormat,
|
format: NotificationStrings.incomingGroupMessageTitleFormat,
|
||||||
senderName,
|
senderName,
|
||||||
groupName
|
groupName
|
||||||
)
|
)
|
||||||
}()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let snippet: String = (interaction.previewText(db)
|
let snippet: String = (interaction.previewText(db)
|
||||||
|
@ -88,21 +83,31 @@ public class NSENotificationPresenter: NSObject, NotificationsProtocol {
|
||||||
notificationContent.body = "MESSAGE_REQUESTS_NOTIFICATION".localized()
|
notificationContent.body = "MESSAGE_REQUESTS_NOTIFICATION".localized()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add request
|
// Add request (try to group notifications for interactions from open groups)
|
||||||
let identifier = interaction.notificationIdentifier(isBackgroundPoll: isBackgroundPoll)
|
let identifier: String = interaction.notificationIdentifier(
|
||||||
|
shouldGroupMessagesForThread: (thread.variant == .openGroup)
|
||||||
|
)
|
||||||
var trigger: UNNotificationTrigger?
|
var trigger: UNNotificationTrigger?
|
||||||
|
|
||||||
if isBackgroundPoll {
|
if thread.variant == .openGroup {
|
||||||
trigger = UNTimeIntervalNotificationTrigger(timeInterval: 5, repeats: false)
|
trigger = UNTimeIntervalNotificationTrigger(
|
||||||
|
timeInterval: Notifications.delayForGroupedNotifications,
|
||||||
|
repeats: false
|
||||||
|
)
|
||||||
|
|
||||||
var numberOfNotifications: Int = (notifications[identifier]?
|
let numberExistingNotifications: Int? = notifications[identifier]?
|
||||||
.content
|
.content
|
||||||
.userInfo[NotificationServiceExtension.threadNotificationCounter]
|
.userInfo[NotificationServiceExtension.threadNotificationCounter]
|
||||||
.asType(Int.self))
|
.asType(Int.self)
|
||||||
.defaulting(to: 1)
|
var numberOfNotifications: Int = (numberExistingNotifications ?? 1)
|
||||||
|
|
||||||
if numberOfNotifications > 1 {
|
if numberExistingNotifications != nil {
|
||||||
numberOfNotifications += 1 // Add one for the current notification
|
numberOfNotifications += 1 // Add one for the current notification
|
||||||
|
|
||||||
|
notificationContent.title = (previewType == .noNameNoPreview ?
|
||||||
|
notificationContent.title :
|
||||||
|
groupName
|
||||||
|
)
|
||||||
notificationContent.body = String(
|
notificationContent.body = String(
|
||||||
format: NotificationStrings.incomingCollapsedMessagesBody,
|
format: NotificationStrings.incomingCollapsedMessagesBody,
|
||||||
"\(numberOfNotifications)"
|
"\(numberOfNotifications)"
|
||||||
|
@ -112,7 +117,11 @@ public class NSENotificationPresenter: NSObject, NotificationsProtocol {
|
||||||
notificationContent.userInfo[NotificationServiceExtension.threadNotificationCounter] = numberOfNotifications
|
notificationContent.userInfo[NotificationServiceExtension.threadNotificationCounter] = numberOfNotifications
|
||||||
}
|
}
|
||||||
|
|
||||||
let request = UNNotificationRequest(identifier: identifier, content: notificationContent, trigger: trigger)
|
let request = UNNotificationRequest(
|
||||||
|
identifier: identifier,
|
||||||
|
content: notificationContent,
|
||||||
|
trigger: trigger
|
||||||
|
)
|
||||||
|
|
||||||
SNLog("Add remote notification request: \(notificationContent.body)")
|
SNLog("Add remote notification request: \(notificationContent.body)")
|
||||||
let semaphore = DispatchSemaphore(value: 0)
|
let semaphore = DispatchSemaphore(value: 0)
|
||||||
|
|
|
@ -83,8 +83,7 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension
|
||||||
db,
|
db,
|
||||||
message: visibleMessage,
|
message: visibleMessage,
|
||||||
associatedWithProto: processedMessage.proto,
|
associatedWithProto: processedMessage.proto,
|
||||||
openGroupId: (isOpenGroup ? processedMessage.threadId : nil),
|
openGroupId: (isOpenGroup ? processedMessage.threadId : nil)
|
||||||
isBackgroundPoll: false
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Remove the notifications if there is an outgoing messages from a linked device
|
// Remove the notifications if there is an outgoing messages from a linked device
|
||||||
|
@ -329,7 +328,7 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension
|
||||||
.defaulting(to: [])
|
.defaulting(to: [])
|
||||||
.map { server in
|
.map { server in
|
||||||
OpenGroupAPI.Poller(for: server)
|
OpenGroupAPI.Poller(for: server)
|
||||||
.poll(isBackgroundPoll: true, isPostCapabilitiesRetry: false)
|
.poll(calledFromBackgroundPoller: true, isPostCapabilitiesRetry: false)
|
||||||
.timeout(
|
.timeout(
|
||||||
seconds: 20,
|
seconds: 20,
|
||||||
timeoutError: NotificationServiceError.timeout
|
timeoutError: NotificationServiceError.timeout
|
||||||
|
|
|
@ -379,7 +379,8 @@ public class PagedDatabaseObserver<ObservedTable, T>: TransactionObserver where
|
||||||
let orderSQL: SQL = self.orderSQL
|
let orderSQL: SQL = self.orderSQL
|
||||||
let dataQuery: ([Int64]) -> AdaptedFetchRequest<SQLRequest<T>> = self.dataQuery
|
let dataQuery: ([Int64]) -> AdaptedFetchRequest<SQLRequest<T>> = self.dataQuery
|
||||||
|
|
||||||
let loadedPage: (data: [T]?, pageInfo: PagedData.PageInfo)? = Storage.shared.read { [weak self] db in
|
let loadedPage: (data: [T]?, pageInfo: PagedData.PageInfo, failureCallback: (() -> ())?)? = Storage.shared.read { [weak self] db in
|
||||||
|
typealias QueryInfo = (limit: Int, offset: Int, updatedCacheOffset: Int)
|
||||||
let totalCount: Int = PagedData.totalCount(
|
let totalCount: Int = PagedData.totalCount(
|
||||||
db,
|
db,
|
||||||
tableName: pagedTableName,
|
tableName: pagedTableName,
|
||||||
|
@ -387,7 +388,7 @@ public class PagedDatabaseObserver<ObservedTable, T>: TransactionObserver where
|
||||||
filterSQL: filterSQL
|
filterSQL: filterSQL
|
||||||
)
|
)
|
||||||
|
|
||||||
let queryInfo: (limit: Int, offset: Int, updatedCacheOffset: Int)? = {
|
let (queryInfo, callback): (QueryInfo?, (() -> ())?) = {
|
||||||
switch target {
|
switch target {
|
||||||
case .initialPageAround(let targetId):
|
case .initialPageAround(let targetId):
|
||||||
// If we want to focus on a specific item then we need to find it's index in
|
// If we want to focus on a specific item then we need to find it's index in
|
||||||
|
@ -404,7 +405,7 @@ public class PagedDatabaseObserver<ObservedTable, T>: TransactionObserver where
|
||||||
|
|
||||||
// If we couldn't find the targetId then just load the first page
|
// If we couldn't find the targetId then just load the first page
|
||||||
guard let targetIndex: Int = maybeIndex else {
|
guard let targetIndex: Int = maybeIndex else {
|
||||||
return (currentPageInfo.pageSize, 0, 0)
|
return ((currentPageInfo.pageSize, 0, 0), nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
let updatedOffset: Int = {
|
let updatedOffset: Int = {
|
||||||
|
@ -421,22 +422,28 @@ public class PagedDatabaseObserver<ObservedTable, T>: TransactionObserver where
|
||||||
return (targetIndex - halfPageSize)
|
return (targetIndex - halfPageSize)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
return (currentPageInfo.pageSize, updatedOffset, updatedOffset)
|
return ((currentPageInfo.pageSize, updatedOffset, updatedOffset), nil)
|
||||||
|
|
||||||
case .pageBefore:
|
case .pageBefore:
|
||||||
let updatedOffset: Int = max(0, (currentPageInfo.pageOffset - currentPageInfo.pageSize))
|
let updatedOffset: Int = max(0, (currentPageInfo.pageOffset - currentPageInfo.pageSize))
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
(
|
||||||
currentPageInfo.pageSize,
|
currentPageInfo.pageSize,
|
||||||
updatedOffset,
|
updatedOffset,
|
||||||
updatedOffset
|
updatedOffset
|
||||||
|
),
|
||||||
|
nil
|
||||||
)
|
)
|
||||||
|
|
||||||
case .pageAfter:
|
case .pageAfter:
|
||||||
return (
|
return (
|
||||||
|
(
|
||||||
currentPageInfo.pageSize,
|
currentPageInfo.pageSize,
|
||||||
(currentPageInfo.pageOffset + currentPageInfo.currentCount),
|
(currentPageInfo.pageOffset + currentPageInfo.currentCount),
|
||||||
currentPageInfo.pageOffset
|
currentPageInfo.pageOffset
|
||||||
|
),
|
||||||
|
nil
|
||||||
)
|
)
|
||||||
|
|
||||||
case .untilInclusive(let targetId, let padding):
|
case .untilInclusive(let targetId, let padding):
|
||||||
|
@ -459,16 +466,19 @@ public class PagedDatabaseObserver<ObservedTable, T>: TransactionObserver where
|
||||||
targetIndex < currentPageInfo.pageOffset ||
|
targetIndex < currentPageInfo.pageOffset ||
|
||||||
targetIndex >= cacheCurrentEndIndex
|
targetIndex >= cacheCurrentEndIndex
|
||||||
)
|
)
|
||||||
else { return nil }
|
else { return (nil, nil) }
|
||||||
|
|
||||||
// If the target is before the cached data then load before
|
// If the target is before the cached data then load before
|
||||||
if targetIndex < currentPageInfo.pageOffset {
|
if targetIndex < currentPageInfo.pageOffset {
|
||||||
let finalIndex: Int = max(0, (targetIndex - abs(padding)))
|
let finalIndex: Int = max(0, (targetIndex - abs(padding)))
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
(
|
||||||
(currentPageInfo.pageOffset - finalIndex),
|
(currentPageInfo.pageOffset - finalIndex),
|
||||||
finalIndex,
|
finalIndex,
|
||||||
finalIndex
|
finalIndex
|
||||||
|
),
|
||||||
|
nil
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -477,23 +487,81 @@ public class PagedDatabaseObserver<ObservedTable, T>: TransactionObserver where
|
||||||
let finalIndex: Int = min(totalCount, (targetIndex + 1 + abs(padding)))
|
let finalIndex: Int = min(totalCount, (targetIndex + 1 + abs(padding)))
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
(
|
||||||
(finalIndex - cacheCurrentEndIndex),
|
(finalIndex - cacheCurrentEndIndex),
|
||||||
cacheCurrentEndIndex,
|
cacheCurrentEndIndex,
|
||||||
currentPageInfo.pageOffset
|
currentPageInfo.pageOffset
|
||||||
|
),
|
||||||
|
nil
|
||||||
)
|
)
|
||||||
|
|
||||||
|
case .jumpTo(let targetId, let paddingForInclusive):
|
||||||
|
// If we want to focus on a specific item then we need to find it's index in
|
||||||
|
// the queried data
|
||||||
|
let maybeIndex: Int? = PagedData.index(
|
||||||
|
db,
|
||||||
|
for: targetId,
|
||||||
|
tableName: pagedTableName,
|
||||||
|
idColumn: idColumnName,
|
||||||
|
orderSQL: orderSQL,
|
||||||
|
filterSQL: filterSQL
|
||||||
|
)
|
||||||
|
let cacheCurrentEndIndex: Int = (currentPageInfo.pageOffset + currentPageInfo.currentCount)
|
||||||
|
|
||||||
|
// If we couldn't find the targetId or it's already in the cache then do nothing
|
||||||
|
guard
|
||||||
|
let targetIndex: Int = maybeIndex.map({ max(0, min(totalCount, $0)) }),
|
||||||
|
(
|
||||||
|
targetIndex < currentPageInfo.pageOffset ||
|
||||||
|
targetIndex >= cacheCurrentEndIndex
|
||||||
|
)
|
||||||
|
else { return (nil, nil) }
|
||||||
|
|
||||||
|
// If the target id is within a single page of the current cached data
|
||||||
|
// then trigger the `untilInclusive` behaviour instead
|
||||||
|
guard
|
||||||
|
abs(targetIndex - cacheCurrentEndIndex) > currentPageInfo.pageSize ||
|
||||||
|
abs(targetIndex - currentPageInfo.pageOffset) > currentPageInfo.pageSize
|
||||||
|
else {
|
||||||
|
let callback: () -> () = {
|
||||||
|
self?.load(.untilInclusive(id: targetId, padding: paddingForInclusive))
|
||||||
|
}
|
||||||
|
return (nil, callback)
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the targetId is further than 1 pageSize away then discard the current
|
||||||
|
// cached data and trigger a fresh `initialPageAround`
|
||||||
|
let callback: () -> () = {
|
||||||
|
self?.dataCache.mutate { $0 = DataCache() }
|
||||||
|
self?.associatedRecords.forEach { $0.clearCache(db) }
|
||||||
|
self?.pageInfo.mutate {
|
||||||
|
$0 = PagedData.PageInfo(
|
||||||
|
pageSize: currentPageInfo.pageSize,
|
||||||
|
pageOffset: 0,
|
||||||
|
currentCount: 0,
|
||||||
|
totalCount: 0
|
||||||
|
)
|
||||||
|
}
|
||||||
|
self?.load(.initialPageAround(id: targetId))
|
||||||
|
}
|
||||||
|
|
||||||
|
return (nil, callback)
|
||||||
|
|
||||||
case .reloadCurrent:
|
case .reloadCurrent:
|
||||||
return (
|
return (
|
||||||
|
(
|
||||||
currentPageInfo.currentCount,
|
currentPageInfo.currentCount,
|
||||||
currentPageInfo.pageOffset,
|
currentPageInfo.pageOffset,
|
||||||
currentPageInfo.pageOffset
|
currentPageInfo.pageOffset
|
||||||
|
),
|
||||||
|
nil
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
// If there is no queryOffset then we already have the data we need so
|
// If there is no queryOffset then we already have the data we need so
|
||||||
// early-out (may as well update the 'totalCount' since it may be relevant)
|
// early-out (may as well update the 'totalCount' since it may be relevant)
|
||||||
guard let queryInfo: (limit: Int, offset: Int, updatedCacheOffset: Int) = queryInfo else {
|
guard let queryInfo: QueryInfo = queryInfo else {
|
||||||
return (
|
return (
|
||||||
nil,
|
nil,
|
||||||
PagedData.PageInfo(
|
PagedData.PageInfo(
|
||||||
|
@ -501,7 +569,8 @@ public class PagedDatabaseObserver<ObservedTable, T>: TransactionObserver where
|
||||||
pageOffset: currentPageInfo.pageOffset,
|
pageOffset: currentPageInfo.pageOffset,
|
||||||
currentCount: currentPageInfo.currentCount,
|
currentCount: currentPageInfo.currentCount,
|
||||||
totalCount: totalCount
|
totalCount: totalCount
|
||||||
)
|
),
|
||||||
|
callback
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -540,7 +609,7 @@ public class PagedDatabaseObserver<ObservedTable, T>: TransactionObserver where
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (newData, updatedLimitInfo)
|
return (newData, updatedLimitInfo, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Unwrap the updated data
|
// Unwrap the updated data
|
||||||
|
@ -554,6 +623,7 @@ public class PagedDatabaseObserver<ObservedTable, T>: TransactionObserver where
|
||||||
self.pageInfo.mutate { $0 = updatedPageInfo }
|
self.pageInfo.mutate { $0 = updatedPageInfo }
|
||||||
}
|
}
|
||||||
self.isLoadingMoreData.mutate { $0 = false }
|
self.isLoadingMoreData.mutate { $0 = false }
|
||||||
|
loadedPage?.failureCallback?()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -651,6 +721,7 @@ public protocol ErasedAssociatedRecord {
|
||||||
pageInfo: PagedData.PageInfo
|
pageInfo: PagedData.PageInfo
|
||||||
) -> Bool
|
) -> Bool
|
||||||
@discardableResult func updateCache(_ db: Database, rowIds: [Int64], hasOtherChanges: Bool) -> Bool
|
@discardableResult func updateCache(_ db: Database, rowIds: [Int64], hasOtherChanges: Bool) -> Bool
|
||||||
|
func clearCache(_ db: Database)
|
||||||
func attachAssociatedData<O>(to unassociatedCache: DataCache<O>) -> DataCache<O>
|
func attachAssociatedData<O>(to unassociatedCache: DataCache<O>) -> DataCache<O>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -733,6 +804,7 @@ public enum PagedData {
|
||||||
case pageBefore
|
case pageBefore
|
||||||
case pageAfter
|
case pageAfter
|
||||||
case untilInclusive(id: SQLExpression, padding: Int)
|
case untilInclusive(id: SQLExpression, padding: Int)
|
||||||
|
case jumpTo(id: SQLExpression, paddingForInclusive: Int)
|
||||||
case reloadCurrent
|
case reloadCurrent
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -755,6 +827,13 @@ public enum PagedData {
|
||||||
/// the padding would mean more data should be loaded)
|
/// the padding would mean more data should be loaded)
|
||||||
case untilInclusive(id: ID, padding: Int)
|
case untilInclusive(id: ID, padding: Int)
|
||||||
|
|
||||||
|
/// This will jump to the specified id, loading a page around it and clearing out any
|
||||||
|
/// data that was previously cached
|
||||||
|
///
|
||||||
|
/// **Note:** If the id is within 1 pageSize of the currently cached data then this
|
||||||
|
/// will behave as per the `untilInclusive(id:padding:)` type
|
||||||
|
case jumpTo(id: ID, paddingForInclusive: Int)
|
||||||
|
|
||||||
fileprivate var internalTarget: InternalTarget {
|
fileprivate var internalTarget: InternalTarget {
|
||||||
switch self {
|
switch self {
|
||||||
case .initialPageAround(let id): return .initialPageAround(id: id.sqlExpression)
|
case .initialPageAround(let id): return .initialPageAround(id: id.sqlExpression)
|
||||||
|
@ -762,6 +841,9 @@ public enum PagedData {
|
||||||
case .pageAfter: return .pageAfter
|
case .pageAfter: return .pageAfter
|
||||||
case .untilInclusive(let id, let padding):
|
case .untilInclusive(let id, let padding):
|
||||||
return .untilInclusive(id: id.sqlExpression, padding: padding)
|
return .untilInclusive(id: id.sqlExpression, padding: padding)
|
||||||
|
|
||||||
|
case .jumpTo(let id, let paddingForInclusive):
|
||||||
|
return .jumpTo(id: id.sqlExpression, paddingForInclusive: paddingForInclusive)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1144,6 +1226,10 @@ public class AssociatedRecord<T, PagedType>: ErasedAssociatedRecord where T: Fet
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public func clearCache(_ db: Database) {
|
||||||
|
dataCache.mutate { $0 = DataCache() }
|
||||||
|
}
|
||||||
|
|
||||||
public func attachAssociatedData<O>(to unassociatedCache: DataCache<O>) -> DataCache<O> {
|
public func attachAssociatedData<O>(to unassociatedCache: DataCache<O>) -> DataCache<O> {
|
||||||
guard let typedCache: DataCache<PagedType> = unassociatedCache as? DataCache<PagedType> else {
|
guard let typedCache: DataCache<PagedType> = unassociatedCache as? DataCache<PagedType> else {
|
||||||
return unassociatedCache
|
return unassociatedCache
|
||||||
|
|
|
@ -103,6 +103,7 @@ public final class JobRunner {
|
||||||
internal static var executorMap: Atomic<[Job.Variant: JobExecutor.Type]> = Atomic([:])
|
internal static var executorMap: Atomic<[Job.Variant: JobExecutor.Type]> = Atomic([:])
|
||||||
fileprivate static var perSessionJobsCompleted: Atomic<Set<Int64>> = Atomic([])
|
fileprivate static var perSessionJobsCompleted: Atomic<Set<Int64>> = Atomic([])
|
||||||
private static var hasCompletedInitialBecomeActive: Atomic<Bool> = Atomic(false)
|
private static var hasCompletedInitialBecomeActive: Atomic<Bool> = Atomic(false)
|
||||||
|
private static var shutdownBackgroundTask: Atomic<OWSBackgroundTask?> = Atomic(nil)
|
||||||
|
|
||||||
// MARK: - Configuration
|
// MARK: - Configuration
|
||||||
|
|
||||||
|
@ -222,6 +223,14 @@ public final class JobRunner {
|
||||||
}
|
}
|
||||||
|
|
||||||
public static func appDidBecomeActive() {
|
public static func appDidBecomeActive() {
|
||||||
|
// If we have a running "sutdownBackgroundTask" then we want to cancel it as otherwise it
|
||||||
|
// can result in the database being suspended and us being unable to interact with it at all
|
||||||
|
shutdownBackgroundTask.mutate {
|
||||||
|
$0?.cancel()
|
||||||
|
$0 = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Retrieve any jobs which should run when becoming active
|
||||||
let hasCompletedInitialBecomeActive: Bool = JobRunner.hasCompletedInitialBecomeActive.wrappedValue
|
let hasCompletedInitialBecomeActive: Bool = JobRunner.hasCompletedInitialBecomeActive.wrappedValue
|
||||||
let jobsToRun: [Job] = Storage.shared
|
let jobsToRun: [Job] = Storage.shared
|
||||||
.read { db in
|
.read { db in
|
||||||
|
@ -259,9 +268,56 @@ public final class JobRunner {
|
||||||
/// Calling this will clear the JobRunner queues and stop it from running new jobs, any currently executing jobs will continue to run
|
/// Calling this will clear the JobRunner queues and stop it from running new jobs, any currently executing jobs will continue to run
|
||||||
/// though (this means if we suspend the database it's likely that any currently running jobs will fail to complete and fail to record their
|
/// though (this means if we suspend the database it's likely that any currently running jobs will fail to complete and fail to record their
|
||||||
/// failure - they _should_ be picked up again the next time the app is launched)
|
/// failure - they _should_ be picked up again the next time the app is launched)
|
||||||
public static func stopAndClearPendingJobs() {
|
public static func stopAndClearPendingJobs(
|
||||||
queues.wrappedValue.values.forEach { queue in
|
exceptForVariant: Job.Variant? = nil,
|
||||||
queue.stopAndClearPendingJobs()
|
onComplete: (() -> ())? = nil
|
||||||
|
) {
|
||||||
|
// Stop all queues except for the one containing the `exceptForVariant`
|
||||||
|
queues.wrappedValue
|
||||||
|
.values
|
||||||
|
.filter { queue -> Bool in
|
||||||
|
guard let exceptForVariant: Job.Variant = exceptForVariant else { return true }
|
||||||
|
|
||||||
|
return !queue.jobVariants.contains(exceptForVariant)
|
||||||
|
}
|
||||||
|
.forEach { $0.stopAndClearPendingJobs() }
|
||||||
|
|
||||||
|
// Ensure the queue is actually running (if not the trigger the callback immediately)
|
||||||
|
guard
|
||||||
|
let exceptForVariant: Job.Variant = exceptForVariant,
|
||||||
|
let queue: JobQueue = queues.wrappedValue[exceptForVariant],
|
||||||
|
queue.isRunning.wrappedValue == true
|
||||||
|
else {
|
||||||
|
onComplete?()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let oldQueueDrained: (() -> ())? = queue.onQueueDrained
|
||||||
|
|
||||||
|
// Create a backgroundTask to give the queue the chance to properly be drained
|
||||||
|
shutdownBackgroundTask.mutate {
|
||||||
|
$0 = OWSBackgroundTask(labelStr: #function) { [weak queue] state in
|
||||||
|
// If the background task didn't succeed then trigger the onComplete (and hope we have
|
||||||
|
// enough time to complete it's logic)
|
||||||
|
guard state != .cancelled else {
|
||||||
|
queue?.onQueueDrained = oldQueueDrained
|
||||||
|
return
|
||||||
|
}
|
||||||
|
guard state != .success else { return }
|
||||||
|
|
||||||
|
onComplete?()
|
||||||
|
queue?.onQueueDrained = oldQueueDrained
|
||||||
|
queue?.stopAndClearPendingJobs()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add a callback to be triggered once the queue is drained
|
||||||
|
queue.onQueueDrained = { [weak queue] in
|
||||||
|
oldQueueDrained?()
|
||||||
|
queue?.onQueueDrained = oldQueueDrained
|
||||||
|
onComplete?()
|
||||||
|
|
||||||
|
shutdownBackgroundTask.mutate { $0 = nil }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -370,7 +426,7 @@ private final class JobQueue {
|
||||||
/// The specific types of jobs this queue manages, if this is left empty it will handle all jobs not handled by other queues
|
/// The specific types of jobs this queue manages, if this is left empty it will handle all jobs not handled by other queues
|
||||||
fileprivate let jobVariants: [Job.Variant]
|
fileprivate let jobVariants: [Job.Variant]
|
||||||
|
|
||||||
private let onQueueDrained: (() -> ())?
|
fileprivate var onQueueDrained: (() -> ())?
|
||||||
|
|
||||||
private lazy var internalQueue: DispatchQueue = {
|
private lazy var internalQueue: DispatchQueue = {
|
||||||
let result: DispatchQueue = DispatchQueue(
|
let result: DispatchQueue = DispatchQueue(
|
||||||
|
|
|
@ -15,4 +15,5 @@ FOUNDATION_EXPORT const unsigned char SessionUtilitiesKitVersionString[];
|
||||||
#import <SessionUtilitiesKit/OWSMath.h>
|
#import <SessionUtilitiesKit/OWSMath.h>
|
||||||
#import <SessionUtilitiesKit/UIImage+OWS.h>
|
#import <SessionUtilitiesKit/UIImage+OWS.h>
|
||||||
#import <SessionUtilitiesKit/UIView+OWS.h>
|
#import <SessionUtilitiesKit/UIView+OWS.h>
|
||||||
|
#import <SessionUtilitiesKit/OWSBackgroundTask.h>
|
||||||
|
|
||||||
|
|
|
@ -10,6 +10,7 @@ typedef NS_ENUM(NSUInteger, BackgroundTaskState) {
|
||||||
BackgroundTaskState_Success,
|
BackgroundTaskState_Success,
|
||||||
BackgroundTaskState_CouldNotStart,
|
BackgroundTaskState_CouldNotStart,
|
||||||
BackgroundTaskState_Expired,
|
BackgroundTaskState_Expired,
|
||||||
|
BackgroundTaskState_Cancelled,
|
||||||
};
|
};
|
||||||
|
|
||||||
typedef void (^BackgroundTaskCompletionBlock)(BackgroundTaskState backgroundTaskState);
|
typedef void (^BackgroundTaskCompletionBlock)(BackgroundTaskState backgroundTaskState);
|
||||||
|
@ -57,6 +58,8 @@ typedef void (^BackgroundTaskCompletionBlock)(BackgroundTaskState backgroundTask
|
||||||
+ (OWSBackgroundTask *)backgroundTaskWithLabel:(NSString *)label
|
+ (OWSBackgroundTask *)backgroundTaskWithLabel:(NSString *)label
|
||||||
completionBlock:(BackgroundTaskCompletionBlock)completionBlock;
|
completionBlock:(BackgroundTaskCompletionBlock)completionBlock;
|
||||||
|
|
||||||
|
- (void)cancel;
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|
||||||
NS_ASSUME_NONNULL_END
|
NS_ASSUME_NONNULL_END
|
|
@ -375,6 +375,31 @@ typedef NSNumber *OWSTaskId;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
- (void)cancel
|
||||||
|
{
|
||||||
|
// Make a local copy of this state, since this method is called by `dealloc`.
|
||||||
|
BackgroundTaskCompletionBlock _Nullable completionBlock;
|
||||||
|
|
||||||
|
@synchronized(self)
|
||||||
|
{
|
||||||
|
if (!self.taskId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
[OWSBackgroundTaskManager.sharedManager removeTask:self.taskId];
|
||||||
|
self.taskId = nil;
|
||||||
|
|
||||||
|
completionBlock = self.completionBlock;
|
||||||
|
self.completionBlock = nil;
|
||||||
|
}
|
||||||
|
|
||||||
|
// endBackgroundTask must be called on the main thread.
|
||||||
|
DispatchMainThreadSafe(^{
|
||||||
|
if (completionBlock) {
|
||||||
|
completionBlock(BackgroundTaskState_Cancelled);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
- (void)endBackgroundTask
|
- (void)endBackgroundTask
|
||||||
{
|
{
|
||||||
// Make a local copy of this state, since this method is called by `dealloc`.
|
// Make a local copy of this state, since this method is called by `dealloc`.
|
|
@ -57,10 +57,23 @@ public final class ProfilePictureView: UIView {
|
||||||
return result
|
return result
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
private lazy var additionalProfilePlaceholderImageView: UIImageView = {
|
||||||
|
let result: UIImageView = UIImageView(
|
||||||
|
image: UIImage(systemName: "person.fill")?.withRenderingMode(.alwaysTemplate)
|
||||||
|
)
|
||||||
|
result.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
result.contentMode = .scaleAspectFill
|
||||||
|
result.tintColor = Colors.text
|
||||||
|
result.isHidden = true
|
||||||
|
|
||||||
|
return result
|
||||||
|
}()
|
||||||
|
|
||||||
private lazy var additionalImageView: UIImageView = {
|
private lazy var additionalImageView: UIImageView = {
|
||||||
let result: UIImageView = UIImageView()
|
let result: UIImageView = UIImageView()
|
||||||
result.translatesAutoresizingMaskIntoConstraints = false
|
result.translatesAutoresizingMaskIntoConstraints = false
|
||||||
result.contentMode = .scaleAspectFill
|
result.contentMode = .scaleAspectFill
|
||||||
|
result.tintColor = Colors.text
|
||||||
result.isHidden = true
|
result.isHidden = true
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
@ -107,11 +120,17 @@ public final class ProfilePictureView: UIView {
|
||||||
imageContainerView.addSubview(animatedImageView)
|
imageContainerView.addSubview(animatedImageView)
|
||||||
additionalImageContainerView.addSubview(additionalImageView)
|
additionalImageContainerView.addSubview(additionalImageView)
|
||||||
additionalImageContainerView.addSubview(additionalAnimatedImageView)
|
additionalImageContainerView.addSubview(additionalAnimatedImageView)
|
||||||
|
additionalImageContainerView.addSubview(additionalProfilePlaceholderImageView)
|
||||||
|
|
||||||
imageView.pin(to: imageContainerView)
|
imageView.pin(to: imageContainerView)
|
||||||
animatedImageView.pin(to: imageContainerView)
|
animatedImageView.pin(to: imageContainerView)
|
||||||
additionalImageView.pin(to: additionalImageContainerView)
|
additionalImageView.pin(to: additionalImageContainerView)
|
||||||
additionalAnimatedImageView.pin(to: additionalImageContainerView)
|
additionalAnimatedImageView.pin(to: additionalImageContainerView)
|
||||||
|
|
||||||
|
additionalProfilePlaceholderImageView.pin(.top, to: .top, of: additionalImageContainerView, withInset: 3)
|
||||||
|
additionalProfilePlaceholderImageView.pin(.left, to: .left, of: additionalImageContainerView)
|
||||||
|
additionalProfilePlaceholderImageView.pin(.right, to: .right, of: additionalImageContainerView)
|
||||||
|
additionalProfilePlaceholderImageView.pin(.bottom, to: .bottom, of: additionalImageContainerView, withInset: 3)
|
||||||
}
|
}
|
||||||
|
|
||||||
// FIXME: Remove this once we refactor the OWSConversationSettingsViewController to Swift (use the HomeViewModel approach)
|
// FIXME: Remove this once we refactor the OWSConversationSettingsViewController to Swift (use the HomeViewModel approach)
|
||||||
|
@ -172,6 +191,7 @@ public final class ProfilePictureView: UIView {
|
||||||
additionalAnimatedImageView.image = nil
|
additionalAnimatedImageView.image = nil
|
||||||
additionalImageView.isHidden = true
|
additionalImageView.isHidden = true
|
||||||
additionalAnimatedImageView.isHidden = true
|
additionalAnimatedImageView.isHidden = true
|
||||||
|
additionalProfilePlaceholderImageView.isHidden = true
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
guard !publicKey.isEmpty || openGroupProfilePictureData != nil else { return }
|
guard !publicKey.isEmpty || openGroupProfilePictureData != nil else { return }
|
||||||
|
@ -240,6 +260,12 @@ public final class ProfilePictureView: UIView {
|
||||||
additionalAnimatedImageView.image = animatedImage
|
additionalAnimatedImageView.image = animatedImage
|
||||||
additionalImageView.isHidden = (animatedImage != nil)
|
additionalImageView.isHidden = (animatedImage != nil)
|
||||||
additionalAnimatedImageView.isHidden = (animatedImage == nil)
|
additionalAnimatedImageView.isHidden = (animatedImage == nil)
|
||||||
|
additionalProfilePlaceholderImageView.isHidden = true
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
additionalImageView.isHidden = true
|
||||||
|
additionalAnimatedImageView.isHidden = true
|
||||||
|
additionalProfilePlaceholderImageView.isHidden = false
|
||||||
}
|
}
|
||||||
|
|
||||||
default:
|
default:
|
||||||
|
@ -251,6 +277,7 @@ public final class ProfilePictureView: UIView {
|
||||||
additionalImageView.isHidden = true
|
additionalImageView.isHidden = true
|
||||||
additionalAnimatedImageView.image = nil
|
additionalAnimatedImageView.image = nil
|
||||||
additionalAnimatedImageView.isHidden = true
|
additionalAnimatedImageView.isHidden = true
|
||||||
|
additionalProfilePlaceholderImageView.isHidden = true
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set the image
|
// Set the image
|
||||||
|
|
|
@ -7,7 +7,7 @@ import SessionMessagingKit
|
||||||
public class NoopNotificationsManager: NotificationsProtocol {
|
public class NoopNotificationsManager: NotificationsProtocol {
|
||||||
public init() {}
|
public init() {}
|
||||||
|
|
||||||
public func notifyUser(_ db: Database, for interaction: Interaction, in thread: SessionThread, isBackgroundPoll: Bool) {
|
public func notifyUser(_ db: Database, for interaction: Interaction, in thread: SessionThread) {
|
||||||
owsFailDebug("")
|
owsFailDebug("")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue