Merge branch 'various-bugs-and-optimisations' into emoji-reacts
This commit is contained in:
commit
c7c92f747c
|
@ -137,6 +137,7 @@
|
|||
7B7CB190270FB2150079FF93 /* MiniCallView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B7CB18F270FB2150079FF93 /* MiniCallView.swift */; };
|
||||
7B7CB192271508AD0079FF93 /* CallRingTonePlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B7CB191271508AD0079FF93 /* CallRingTonePlayer.swift */; };
|
||||
7B81682328A4C1210069F315 /* UpdateTypes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B81682228A4C1210069F315 /* UpdateTypes.swift */; };
|
||||
7B81682828B310D50069F315 /* _007_HomeQueryOptimisationIndexes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B81682728B310D50069F315 /* _007_HomeQueryOptimisationIndexes.swift */; };
|
||||
7B8D5FC428332600008324D9 /* VisibleMessage+Reaction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B8D5FC328332600008324D9 /* VisibleMessage+Reaction.swift */; };
|
||||
7B93D06A27CF173D00811CB6 /* MessageRequestsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B93D06927CF173D00811CB6 /* MessageRequestsViewController.swift */; };
|
||||
7B93D07027CF194000811CB6 /* ConfigurationMessage+Convenience.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B93D06E27CF194000811CB6 /* ConfigurationMessage+Convenience.swift */; };
|
||||
|
@ -318,8 +319,6 @@
|
|||
C32C5DD2256DD9E5003C73A2 /* LRUCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33FDAFD255A580600E217F9 /* LRUCache.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 */; };
|
||||
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 */; };
|
||||
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 */; };
|
||||
|
@ -602,7 +601,7 @@
|
|||
FD09799927FFC1A300936362 /* Attachment.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD09799827FFC1A300936362 /* Attachment.swift */; };
|
||||
FD09799B27FFC82D00936362 /* Quote.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD09799A27FFC82D00936362 /* Quote.swift */; };
|
||||
FD09B7E328865FDA00ED0B66 /* HighlightMentionBackgroundView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD09B7E228865FDA00ED0B66 /* HighlightMentionBackgroundView.swift */; };
|
||||
FD09B7E5288670BB00ED0B66 /* _007_EmojiReacts.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD09B7E4288670BB00ED0B66 /* _007_EmojiReacts.swift */; };
|
||||
FD09B7E5288670BB00ED0B66 /* _008_EmojiReacts.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD09B7E4288670BB00ED0B66 /* _008_EmojiReacts.swift */; };
|
||||
FD09B7E7288670FD00ED0B66 /* Reaction.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD09B7E6288670FD00ED0B66 /* Reaction.swift */; };
|
||||
FD09C5E2282212B3000CE219 /* JobDependencies.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD09C5E1282212B3000CE219 /* JobDependencies.swift */; };
|
||||
FD09C5E628260FF9000CE219 /* MediaGalleryViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD09C5E528260FF9000CE219 /* MediaGalleryViewModel.swift */; };
|
||||
|
@ -667,6 +666,7 @@
|
|||
FD37EA0F28AB3330003AE748 /* _006_FixHiddenModAdminSupport.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD37EA0E28AB3330003AE748 /* _006_FixHiddenModAdminSupport.swift */; };
|
||||
FD37EA1128AB34B3003AE748 /* TypedTableAlteration.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD37EA1028AB34B3003AE748 /* TypedTableAlteration.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 */; };
|
||||
FD3C905C27E3FBEF00CD579F /* BatchRequestInfoSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD3C905B27E3FBEF00CD579F /* BatchRequestInfoSpec.swift */; };
|
||||
FD3C906027E410F700CD579F /* FileUploadResponseSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD3C905F27E410F700CD579F /* FileUploadResponseSpec.swift */; };
|
||||
|
@ -680,6 +680,8 @@
|
|||
FD3E0C84283B5835002A425C /* SessionThreadViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD3E0C83283B5835002A425C /* SessionThreadViewModel.swift */; };
|
||||
FD42F9A8285064B800A0C77D /* PushNotificationAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33FDBDE255A581900E217F9 /* PushNotificationAPI.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 */; };
|
||||
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 */; };
|
||||
|
@ -799,7 +801,6 @@
|
|||
FDCDB8DE2810F73B00352A0C /* Differentiable+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDCDB8DD2810F73B00352A0C /* Differentiable+Utilities.swift */; };
|
||||
FDCDB8E02811007F00352A0C /* HomeViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDCDB8DF2811007F00352A0C /* HomeViewModel.swift */; };
|
||||
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 */; };
|
||||
FDD250702837199200198BDA /* GarbageCollectionJob.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDD2506F2837199200198BDA /* GarbageCollectionJob.swift */; };
|
||||
FDD250722837234B00198BDA /* MediaGalleryNavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDD250712837234B00198BDA /* MediaGalleryNavigationController.swift */; };
|
||||
|
@ -1177,6 +1178,7 @@
|
|||
7B7CB18F270FB2150079FF93 /* MiniCallView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MiniCallView.swift; sourceTree = "<group>"; };
|
||||
7B7CB191271508AD0079FF93 /* CallRingTonePlayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallRingTonePlayer.swift; sourceTree = "<group>"; };
|
||||
7B81682228A4C1210069F315 /* UpdateTypes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateTypes.swift; sourceTree = "<group>"; };
|
||||
7B81682728B310D50069F315 /* _007_HomeQueryOptimisationIndexes.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = _007_HomeQueryOptimisationIndexes.swift; sourceTree = "<group>"; };
|
||||
7B8D5FC328332600008324D9 /* VisibleMessage+Reaction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "VisibleMessage+Reaction.swift"; sourceTree = "<group>"; };
|
||||
7B93D06927CF173D00811CB6 /* MessageRequestsViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageRequestsViewController.swift; sourceTree = "<group>"; };
|
||||
7B93D06E27CF194000811CB6 /* ConfigurationMessage+Convenience.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "ConfigurationMessage+Convenience.swift"; sourceTree = "<group>"; };
|
||||
|
@ -1687,7 +1689,7 @@
|
|||
FD09799827FFC1A300936362 /* Attachment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Attachment.swift; sourceTree = "<group>"; };
|
||||
FD09799A27FFC82D00936362 /* Quote.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Quote.swift; sourceTree = "<group>"; };
|
||||
FD09B7E228865FDA00ED0B66 /* HighlightMentionBackgroundView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HighlightMentionBackgroundView.swift; sourceTree = "<group>"; };
|
||||
FD09B7E4288670BB00ED0B66 /* _007_EmojiReacts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _007_EmojiReacts.swift; sourceTree = "<group>"; };
|
||||
FD09B7E4288670BB00ED0B66 /* _008_EmojiReacts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _008_EmojiReacts.swift; sourceTree = "<group>"; };
|
||||
FD09B7E6288670FD00ED0B66 /* Reaction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Reaction.swift; sourceTree = "<group>"; };
|
||||
FD09C5E1282212B3000CE219 /* JobDependencies.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JobDependencies.swift; sourceTree = "<group>"; };
|
||||
FD09C5E528260FF9000CE219 /* MediaGalleryViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaGalleryViewModel.swift; sourceTree = "<group>"; };
|
||||
|
@ -1728,6 +1730,7 @@
|
|||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
|
@ -1853,7 +1856,6 @@
|
|||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
|
@ -2194,6 +2196,14 @@
|
|||
path = "Views & Modals";
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
7B81682428B30BEC0069F315 /* Recovered References */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
FD37EA1A28ACB51F003AE748 /* _007_HomeQueryOptimisationIndexes.swift */,
|
||||
);
|
||||
name = "Recovered References";
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
7B93D06827CF173D00811CB6 /* Message Requests */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
|
@ -3122,8 +3132,6 @@
|
|||
C38EF2F5255B6DBC007E1867 /* OWSAudioPlayer.h */,
|
||||
C38EF2F7255B6DBC007E1867 /* OWSAudioPlayer.m */,
|
||||
C38EF281255B6D84007E1867 /* OWSAudioSession.swift */,
|
||||
C33FDB38255A580B00E217F9 /* OWSBackgroundTask.h */,
|
||||
C33FDC1B255A581F00E217F9 /* OWSBackgroundTask.m */,
|
||||
FDF0B75D280AAF35004C14C5 /* Preferences.swift */,
|
||||
C38EF2FB255B6DBD007E1867 /* OWSWindowManager.h */,
|
||||
C38EF306255B6DBE007E1867 /* OWSWindowManager.m */,
|
||||
|
@ -3191,7 +3199,6 @@
|
|||
B8A582AE258C65D000AFD84C /* Networking */,
|
||||
B8A582AD258C655E00AFD84C /* PromiseKit */,
|
||||
FD09796527F6B0A800936362 /* Utilities */,
|
||||
FDCDB8EF2817ABCE00352A0C /* Utilities */,
|
||||
C3D9E43025676D3D0040E4F3 /* Configuration.swift */,
|
||||
);
|
||||
path = SessionUtilitiesKit;
|
||||
|
@ -3388,6 +3395,7 @@
|
|||
D221A08C169C9E5E00537ABF /* Frameworks */,
|
||||
D221A08A169C9E5E00537ABF /* Products */,
|
||||
2BADBA206E0B8D297E313FBA /* Pods */,
|
||||
7B81682428B30BEC0069F315 /* Recovered References */,
|
||||
);
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
|
@ -3492,6 +3500,8 @@
|
|||
FD09797127FAA2F500936362 /* Optional+Utilities.swift */,
|
||||
FD09797C27FBDB2000936362 /* Notification+Utilities.swift */,
|
||||
FDF222082818D2B0000A4995 /* NSAttributedString+Utilities.swift */,
|
||||
C33FDB38255A580B00E217F9 /* OWSBackgroundTask.h */,
|
||||
C33FDC1B255A581F00E217F9 /* OWSBackgroundTask.m */,
|
||||
);
|
||||
path = Utilities;
|
||||
sourceTree = "<group>";
|
||||
|
@ -3531,7 +3541,8 @@
|
|||
FDF40CDD2897A1BC006A0CC4 /* _004_RemoveLegacyYDB.swift */,
|
||||
FD37EA0C28AB2A45003AE748 /* _005_FixDeletedMessageReadState.swift */,
|
||||
FD37EA0E28AB3330003AE748 /* _006_FixHiddenModAdminSupport.swift */,
|
||||
FD09B7E4288670BB00ED0B66 /* _007_EmojiReacts.swift */,
|
||||
7B81682728B310D50069F315 /* _007_HomeQueryOptimisationIndexes.swift */,
|
||||
FD09B7E4288670BB00ED0B66 /* _008_EmojiReacts.swift */,
|
||||
);
|
||||
path = Migrations;
|
||||
sourceTree = "<group>";
|
||||
|
@ -3946,14 +3957,6 @@
|
|||
path = Models;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
FDCDB8EF2817ABCE00352A0C /* Utilities */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
FDCDB8F02817ABE600352A0C /* Optional+Utilities.swift */,
|
||||
);
|
||||
path = Utilities;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
FDE7214E287E50D50093DF33 /* Scripts */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
|
@ -4068,6 +4071,7 @@
|
|||
C3C2A67D255388CC00C340D1 /* SessionUtilitiesKit.h in Headers */,
|
||||
C32C6018256E07F9003C73A2 /* NSUserDefaults+OWS.h in Headers */,
|
||||
B8856D8D256F1502001CE70E /* UIView+OWS.h in Headers */,
|
||||
FD52090128AF61BA006098F6 /* OWSBackgroundTask.h in Headers */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
|
@ -4077,7 +4081,6 @@
|
|||
files = (
|
||||
C3C2A6F425539DE700C340D1 /* SessionMessagingKit.h in Headers */,
|
||||
C32C5C46256DCBB2003C73A2 /* AppReadiness.h in Headers */,
|
||||
C32C5FC4256E0209003C73A2 /* OWSBackgroundTask.h in Headers */,
|
||||
FD716E732850647900C96BF4 /* NSData+messagePadding.h in Headers */,
|
||||
B8856D72256F1421001CE70E /* OWSWindowManager.h in Headers */,
|
||||
B8856CF7256F105E001CE70E /* OWSAudioPlayer.h in Headers */,
|
||||
|
@ -5086,7 +5089,6 @@
|
|||
FDA8EB10280F8238002B68E5 /* Codable+Utilities.swift in Sources */,
|
||||
C352A36D2557858E00338F3E /* NSTimer+Proxying.m in Sources */,
|
||||
FD09797B27FBB25900936362 /* Updatable.swift in Sources */,
|
||||
FDCDB8F12817ABE600352A0C /* Optional+Utilities.swift in Sources */,
|
||||
7B7CB192271508AD0079FF93 /* CallRingTonePlayer.swift in Sources */,
|
||||
C3C2ABD22553C6C900C340D1 /* Data+SecureRandom.swift in Sources */,
|
||||
FD848B8B283DC509000E298B /* PagedDatabaseObserver.swift in Sources */,
|
||||
|
@ -5132,6 +5134,7 @@
|
|||
FD17D7B827F51ECA00122BE0 /* Migration.swift in Sources */,
|
||||
FD7728982849E8110018502F /* UITableView+ReusableView.swift in Sources */,
|
||||
7B0EFDEE274F598600FFAAE7 /* TimestampUtils.swift in Sources */,
|
||||
FD52090028AF6153006098F6 /* OWSBackgroundTask.m in Sources */,
|
||||
C32C5DDB256DD9FF003C73A2 /* ContentProxy.swift in Sources */,
|
||||
C3A71F892558BA9F0043A11F /* Mnemonic.swift in Sources */,
|
||||
B8F5F58325EC94A6003BF8D4 /* Collection+Subscripting.swift in Sources */,
|
||||
|
@ -5176,6 +5179,7 @@
|
|||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
7B81682828B310D50069F315 /* _007_HomeQueryOptimisationIndexes.swift in Sources */,
|
||||
FD86585828507B24008B6CF9 /* NSData+messagePadding.m in Sources */,
|
||||
FD245C52285065D500B966DD /* SignalAttachment.swift in Sources */,
|
||||
B8856D08256F10F1001CE70E /* DeviceSleepManager.swift in Sources */,
|
||||
|
@ -5212,7 +5216,7 @@
|
|||
FD245C6A2850666F00B966DD /* FileServerAPI.swift in Sources */,
|
||||
FDC4386927B4E6B800C60D73 /* String+Utlities.swift in Sources */,
|
||||
FD716E6628502EE200C96BF4 /* CurrentCallProtocol.swift in Sources */,
|
||||
FD09B7E5288670BB00ED0B66 /* _007_EmojiReacts.swift in Sources */,
|
||||
FD09B7E5288670BB00ED0B66 /* _008_EmojiReacts.swift in Sources */,
|
||||
FDC4385F27B4C4A200C60D73 /* PinnedMessage.swift in Sources */,
|
||||
7BFD1A8C2747150E00FB91B9 /* TurnServerInfo.swift in Sources */,
|
||||
C3D9E3BF25676AD70040E4F3 /* (null) in Sources */,
|
||||
|
@ -5312,7 +5316,6 @@
|
|||
FD245C5C2850660A00B966DD /* ConfigurationMessage.swift in Sources */,
|
||||
FD5C7301284F0F7A0029977D /* MessageReceiver+UnsendRequests.swift in Sources */,
|
||||
C3C2A75F2553A3C500C340D1 /* VisibleMessage+LinkPreview.swift in Sources */,
|
||||
C32C5FBB256E0206003C73A2 /* OWSBackgroundTask.m in Sources */,
|
||||
FD245C642850664F00B966DD /* Threading.swift in Sources */,
|
||||
FD848B8D283E0B26000E298B /* MessageInputTypes.swift in Sources */,
|
||||
C32C5C3D256DCBAF003C73A2 /* AppReadiness.m in Sources */,
|
||||
|
|
|
@ -697,6 +697,17 @@ final class ConversationVC: BaseVC, OWSConversationSettingsViewDelegate, Convers
|
|||
let wasLoadingMore: Bool = self.isLoadingMore
|
||||
let wasOffsetCloseToBottom: Bool = self.isCloseToBottom
|
||||
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? = {
|
||||
guard
|
||||
isInsert,
|
||||
|
@ -729,7 +740,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.tableView.reloadData()
|
||||
|
||||
|
@ -738,16 +749,27 @@ final class ConversationVC: BaseVC, OWSConversationSettingsViewDelegate, Convers
|
|||
if let focusedInteractionId: Int64 = self.focusedInteractionId {
|
||||
// If we had a focusedInteractionId then scroll to it (and hide the search
|
||||
// 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?.scrollToInteractionIfNeeded(
|
||||
with: focusedInteractionId,
|
||||
isAnimated: 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
|
||||
// just sent a message or are close enough to the bottom (wait a tiny fraction
|
||||
// to avoid buggy animation behaviour)
|
||||
|
@ -755,6 +777,11 @@ final class ConversationVC: BaseVC, OWSConversationSettingsViewDelegate, Convers
|
|||
self?.scrollToBottom(isAnimated: true)
|
||||
}
|
||||
}
|
||||
else if wasLoadingMore {
|
||||
// Complete page loading
|
||||
self.isLoadingMore = false
|
||||
self.autoLoadNextPageIfNeeded()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -764,7 +791,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
|
||||
/// 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
|
||||
|
||||
// The the user triggered the 'scrollToTop' animation (by tapping in the nav bar) then we
|
||||
|
@ -798,7 +825,7 @@ final class ConversationVC: BaseVC, OWSConversationSettingsViewDelegate, Convers
|
|||
.rectForRow(at: itemChangeInfo.visibleIndexPath)
|
||||
.height
|
||||
let heightDiff: CGFloat = (oldCellHeight - (newTargetHeight ?? oldCellHeight))
|
||||
|
||||
|
||||
self?.tableView.contentOffset.y += (calculatedRowHeights - heightDiff)
|
||||
}
|
||||
|
||||
|
@ -814,13 +841,36 @@ final class ConversationVC: BaseVC, OWSConversationSettingsViewDelegate, Convers
|
|||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Complete page loading
|
||||
self?.isLoadingMore = false
|
||||
self?.autoLoadNextPageIfNeeded()
|
||||
}
|
||||
)
|
||||
}
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
// Update the messages
|
||||
self.tableView.reload(
|
||||
|
@ -849,13 +899,13 @@ final class ConversationVC: BaseVC, OWSConversationSettingsViewDelegate, Convers
|
|||
// the screen will scroll to the bottom instead of the first unread message
|
||||
if let focusedInteractionId: Int64 = self.viewModel.focusedInteractionId {
|
||||
self.scrollToInteractionIfNeeded(with: focusedInteractionId, isAnimated: false, highlight: true)
|
||||
self.unreadCountView.alpha = self.scrollButton.alpha
|
||||
}
|
||||
else {
|
||||
self.scrollToBottom(isAnimated: false)
|
||||
}
|
||||
|
||||
self.scrollButton.alpha = self.getScrollButtonOpacity()
|
||||
self.unreadCountView.alpha = self.scrollButton.alpha
|
||||
self.hasPerformedInitialScroll = true
|
||||
|
||||
// Now that the data has loaded we need to check if either of the "load more" sections are
|
||||
|
@ -1030,6 +1080,7 @@ final class ConversationVC: BaseVC, OWSConversationSettingsViewDelegate, Convers
|
|||
|
||||
let scrollButtonOpacity: CGFloat = (self?.getScrollButtonOpacity() ?? 0)
|
||||
self?.scrollButton.alpha = scrollButtonOpacity
|
||||
self?.unreadCountView.alpha = scrollButtonOpacity
|
||||
|
||||
self?.view.setNeedsLayout()
|
||||
self?.view.layoutIfNeeded()
|
||||
|
@ -1239,6 +1290,7 @@ final class ConversationVC: BaseVC, OWSConversationSettingsViewDelegate, Convers
|
|||
self.scrollToInteractionIfNeeded(
|
||||
with: lastInteractionId,
|
||||
position: .bottom,
|
||||
isJumpingToLastInteraction: true,
|
||||
isAnimated: true
|
||||
)
|
||||
return
|
||||
|
@ -1297,7 +1349,7 @@ final class ConversationVC: BaseVC, OWSConversationSettingsViewDelegate, Convers
|
|||
let contentOffsetY = tableView.contentOffset.y
|
||||
let x = (lastPageTop - ConversationVC.bottomInset - contentOffsetY).clamp(0, .greatestFiniteMagnitude)
|
||||
let a = 1 / (ConversationVC.scrollButtonFullVisibilityThreshold - ConversationVC.scrollButtonNoVisibilityThreshold)
|
||||
return a * x
|
||||
return max(0, min(1, a * x))
|
||||
}
|
||||
|
||||
// MARK: - Search
|
||||
|
@ -1408,6 +1460,7 @@ final class ConversationVC: BaseVC, OWSConversationSettingsViewDelegate, Convers
|
|||
func scrollToInteractionIfNeeded(
|
||||
with interactionId: Int64,
|
||||
position: UITableView.ScrollPosition = .middle,
|
||||
isJumpingToLastInteraction: Bool = false,
|
||||
isAnimated: Bool = true,
|
||||
highlight: Bool = false
|
||||
) {
|
||||
|
@ -1431,10 +1484,18 @@ final class ConversationVC: BaseVC, OWSConversationSettingsViewDelegate, Convers
|
|||
self.searchController.resultsBar.startLoading()
|
||||
|
||||
DispatchQueue.global(qos: .default).async { [weak self] in
|
||||
self?.viewModel.pagedDataObserver?.load(.untilInclusive(
|
||||
id: interactionId,
|
||||
padding: 5
|
||||
))
|
||||
if isJumpingToLastInteraction {
|
||||
self?.viewModel.pagedDataObserver?.load(.jumpTo(
|
||||
id: interactionId,
|
||||
paddingForInclusive: 5
|
||||
))
|
||||
}
|
||||
else {
|
||||
self?.viewModel.pagedDataObserver?.load(.untilInclusive(
|
||||
id: interactionId,
|
||||
padding: 5
|
||||
))
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
|
|
@ -214,9 +214,7 @@ final class HomeVC: BaseVC, UITableViewDataSource, UITableViewDelegate, NewConve
|
|||
}
|
||||
|
||||
// Onion request path countries cache
|
||||
DispatchQueue.global(qos: .utility).sync {
|
||||
let _ = IP2Country.shared.populateCacheIfNeeded()
|
||||
}
|
||||
IP2Country.shared.populateCacheIfNeededAsync()
|
||||
}
|
||||
|
||||
override func viewWillAppear(_ animated: Bool) {
|
||||
|
|
|
@ -43,8 +43,7 @@ public class HomeViewModel {
|
|||
// MARK: - Initialization
|
||||
|
||||
init() {
|
||||
self.state = Storage.shared.read { db in try HomeViewModel.retrieveState(db) }
|
||||
.defaulting(to: State())
|
||||
self.state = State()
|
||||
self.pagedDataObserver = nil
|
||||
|
||||
// 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,
|
||||
filterSQL: SessionThreadViewModel.homeFilterSQL(userPublicKey: userPublicKey),
|
||||
groupSQL: SessionThreadViewModel.groupSQL,
|
||||
orderSQL: SessionThreadViewModel.homeOrderSQL,
|
||||
dataQuery: SessionThreadViewModel.baseQuery(
|
||||
userPublicKey: userPublicKey,
|
||||
filterSQL: SessionThreadViewModel.homeFilterSQL(userPublicKey: userPublicKey),
|
||||
groupSQL: SessionThreadViewModel.groupSQL,
|
||||
orderSQL: SessionThreadViewModel.homeOrderSQL
|
||||
),
|
||||
|
@ -194,8 +193,9 @@ public class HomeViewModel {
|
|||
let hasHiddenMessageRequests: Bool = db[.hasHiddenMessageRequests]
|
||||
let userProfile: Profile = Profile.fetchOrCreateCurrentUser(db)
|
||||
let unreadMessageRequestThreadCount: Int = try SessionThread
|
||||
.unreadMessageRequestsThreadIdQuery(userPublicKey: userProfile.id)
|
||||
.fetchCount(db)
|
||||
.unreadMessageRequestsCountQuery(userPublicKey: userProfile.id)
|
||||
.fetchOne(db)
|
||||
.defaulting(to: 0)
|
||||
|
||||
return State(
|
||||
showViewedSeedBanner: !hasViewedSeed,
|
||||
|
@ -219,7 +219,8 @@ public class HomeViewModel {
|
|||
else { return }
|
||||
|
||||
/// **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)
|
||||
|
||||
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,
|
||||
filterSQL: SessionThreadViewModel.messageRequestsFilterSQL(userPublicKey: userPublicKey),
|
||||
groupSQL: SessionThreadViewModel.groupSQL,
|
||||
orderSQL: SessionThreadViewModel.messageRequetsOrderSQL,
|
||||
dataQuery: SessionThreadViewModel.baseQuery(
|
||||
userPublicKey: userPublicKey,
|
||||
filterSQL: SessionThreadViewModel.messageRequestsFilterSQL(userPublicKey: userPublicKey),
|
||||
groupSQL: SessionThreadViewModel.groupSQL,
|
||||
orderSQL: SessionThreadViewModel.messageRequetsOrderSQL
|
||||
),
|
||||
|
|
|
@ -367,7 +367,7 @@ public class MediaGalleryViewModel {
|
|||
.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?)
|
||||
|
||||
// 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
|
||||
.baseQuery(
|
||||
orderSQL: Item.galleryReverseOrderSQL,
|
||||
customFilters: SQL("\(interaction[.timestampMs]) > \(albumTimestampMs)")
|
||||
customFilters: SQL("""
|
||||
\(interaction[.timestampMs]) > \(albumTimestampMs) AND
|
||||
\(interaction[.threadId]) = \(threadId)
|
||||
""")
|
||||
)
|
||||
.fetchOne(db)
|
||||
let itemAfter: Item? = try Item
|
||||
.baseQuery(
|
||||
orderSQL: Item.galleryOrderSQL,
|
||||
customFilters: SQL("\(interaction[.timestampMs]) < \(albumTimestampMs)")
|
||||
customFilters: SQL("""
|
||||
\(interaction[.timestampMs]) < \(albumTimestampMs) AND
|
||||
\(interaction[.threadId]) = \(threadId)
|
||||
""")
|
||||
)
|
||||
.fetchOne(db)
|
||||
|
||||
|
@ -505,7 +511,7 @@ public class MediaGalleryViewModel {
|
|||
threadVariant: threadVariant,
|
||||
isPagedData: false
|
||||
)
|
||||
viewModel.loadAndCacheAlbumData(for: interactionId)
|
||||
viewModel.loadAndCacheAlbumData(for: interactionId, in: threadId)
|
||||
viewModel.replaceAlbumObservation(toObservationFor: interactionId)
|
||||
|
||||
guard
|
||||
|
|
|
@ -681,10 +681,15 @@ class MediaPageViewController: UIPageViewController, UIPageViewControllerDataSou
|
|||
}
|
||||
|
||||
// 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
|
||||
let newAlbumItems: [MediaGalleryViewModel.Item] = viewModel.loadAndCacheAlbumData(for: interactionIdAfter)
|
||||
let newAlbumItems: [MediaGalleryViewModel.Item] = viewModel.loadAndCacheAlbumData(
|
||||
for: interactionIdAfter,
|
||||
in: self.viewModel.threadId
|
||||
)
|
||||
|
||||
guard
|
||||
!newAlbumItems.isEmpty,
|
||||
|
@ -723,10 +728,15 @@ class MediaPageViewController: UIPageViewController, UIPageViewControllerDataSou
|
|||
}
|
||||
|
||||
// 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
|
||||
let newAlbumItems: [MediaGalleryViewModel.Item] = viewModel.loadAndCacheAlbumData(for: interactionIdBefore)
|
||||
let newAlbumItems: [MediaGalleryViewModel.Item] = viewModel.loadAndCacheAlbumData(
|
||||
for: interactionIdBefore,
|
||||
in: self.viewModel.threadId
|
||||
)
|
||||
|
||||
guard
|
||||
!newAlbumItems.isEmpty,
|
||||
|
|
|
@ -133,10 +133,11 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
|
|||
// NOTE: Fix an edge case where user taps on the callkit notification
|
||||
// but answers the call on another device
|
||||
stopPollers(shouldStopUserPoller: !self.hasIncomingCallWaiting())
|
||||
JobRunner.stopAndClearPendingJobs()
|
||||
|
||||
// Suspend database
|
||||
NotificationCenter.default.post(name: Database.suspendNotification, object: self)
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
|
||||
func applicationDidReceiveMemoryWarning(_ application: UIApplication) {
|
||||
|
|
|
@ -44,7 +44,6 @@
|
|||
#import <SessionUtilitiesKit/NSData+Image.h>
|
||||
#import <SessionUtilitiesKit/NSNotificationCenter+OWS.h>
|
||||
#import <SessionUtilitiesKit/NSString+SSK.h>
|
||||
#import <SessionMessagingKit/OWSBackgroundTask.h>
|
||||
#import <SignalUtilitiesKit/OWSDispatch.h>
|
||||
#import <SignalUtilitiesKit/OWSError.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 kAudioNotificationsThrottleInterval: TimeInterval = 5
|
||||
|
||||
|
@ -93,14 +89,48 @@ protocol NotificationPresenterAdaptee: AnyObject {
|
|||
|
||||
func registerNotificationSettings() -> Promise<Void>
|
||||
|
||||
func notify(category: AppNotificationCategory, title: String?, body: String, userInfo: [AnyHashable: Any], sound: Preferences.Sound?)
|
||||
func notify(category: AppNotificationCategory, title: String?, body: String, userInfo: [AnyHashable: Any], sound: Preferences.Sound?, replacingIdentifier: String?)
|
||||
func notify(
|
||||
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(identifiers: [String])
|
||||
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)
|
||||
public class NotificationPresenter: NSObject, NotificationsProtocol {
|
||||
|
||||
|
@ -141,7 +171,7 @@ public class NotificationPresenter: NSObject, NotificationsProtocol {
|
|||
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)
|
||||
|
||||
// Ensure we should be showing a notification for the thread
|
||||
|
@ -149,7 +179,10 @@ public class NotificationPresenter: NSObject, NotificationsProtocol {
|
|||
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.
|
||||
let rawMessageText = interaction.previewText(db)
|
||||
|
@ -166,6 +199,18 @@ public class NotificationPresenter: NSObject, NotificationsProtocol {
|
|||
let senderName = Profile.displayName(db, id: interaction.authorId, threadVariant: thread.variant)
|
||||
let previewType: Preferences.NotificationPreviewType = db[.preferencesNotificationPreviewType]
|
||||
.defaulting(to: .nameAndPreview)
|
||||
let groupName: String = SessionThread.displayName(
|
||||
threadId: thread.id,
|
||||
variant: thread.variant,
|
||||
closedGroupName: try? thread.closedGroup
|
||||
.select(.name)
|
||||
.asRequest(of: String.self)
|
||||
.fetchOne(db),
|
||||
openGroupName: try? thread.openGroup
|
||||
.select(.name)
|
||||
.asRequest(of: String.self)
|
||||
.fetchOne(db)
|
||||
)
|
||||
|
||||
switch previewType {
|
||||
case .noNameNoPreview:
|
||||
|
@ -177,26 +222,10 @@ public class NotificationPresenter: NSObject, NotificationsProtocol {
|
|||
notificationTitle = (isMessageRequest ? "Session" : senderName)
|
||||
|
||||
case .closedGroup, .openGroup:
|
||||
let groupName: String = SessionThread
|
||||
.displayName(
|
||||
threadId: thread.id,
|
||||
variant: thread.variant,
|
||||
closedGroupName: try? thread.closedGroup
|
||||
.select(.name)
|
||||
.asRequest(of: String.self)
|
||||
.fetchOne(db),
|
||||
openGroupName: try? thread.openGroup
|
||||
.select(.name)
|
||||
.asRequest(of: String.self)
|
||||
.fetchOne(db)
|
||||
)
|
||||
|
||||
notificationTitle = (isBackgroundPoll ? groupName :
|
||||
String(
|
||||
format: NotificationStrings.incomingGroupMessageTitleFormat,
|
||||
senderName,
|
||||
groupName
|
||||
)
|
||||
notificationTitle = String(
|
||||
format: NotificationStrings.incomingGroupMessageTitleFormat,
|
||||
senderName,
|
||||
groupName
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -243,9 +272,12 @@ public class NotificationPresenter: NSObject, NotificationsProtocol {
|
|||
self.adaptee.notify(
|
||||
category: category,
|
||||
title: notificationTitle,
|
||||
body: notificationBody ?? "",
|
||||
body: (notificationBody ?? ""),
|
||||
userInfo: userInfo,
|
||||
previewType: previewType,
|
||||
sound: sound,
|
||||
threadVariant: thread.variant,
|
||||
threadName: groupName,
|
||||
replacingIdentifier: identifier
|
||||
)
|
||||
}
|
||||
|
@ -268,23 +300,26 @@ public class NotificationPresenter: NSObject, NotificationsProtocol {
|
|||
guard messageInfo.state == .missed || messageInfo.state == .permissionDenied else { return }
|
||||
|
||||
let category = AppNotificationCategory.errorMessage
|
||||
|
||||
let previewType: Preferences.NotificationPreviewType = db[.preferencesNotificationPreviewType]
|
||||
.defaulting(to: .nameAndPreview)
|
||||
|
||||
let userInfo = [
|
||||
AppNotificationUserInfoKey.threadId: thread.id
|
||||
]
|
||||
|
||||
let notificationTitle = interaction.previewText(db)
|
||||
let notificationTitle: String = interaction.previewText(db)
|
||||
let threadName: String = SessionThread.displayName(
|
||||
threadId: thread.id,
|
||||
variant: thread.variant,
|
||||
closedGroupName: nil, // Not supported
|
||||
openGroupName: nil // Not supported
|
||||
)
|
||||
var notificationBody: String?
|
||||
|
||||
if messageInfo.state == .permissionDenied {
|
||||
notificationBody = String(
|
||||
format: "modal_call_missed_tips_explanation".localized(),
|
||||
SessionThread.displayName(
|
||||
threadId: thread.id,
|
||||
variant: thread.variant,
|
||||
closedGroupName: nil, // Not supported
|
||||
openGroupName: nil // Not supported
|
||||
)
|
||||
threadName
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -294,9 +329,12 @@ public class NotificationPresenter: NSObject, NotificationsProtocol {
|
|||
self.adaptee.notify(
|
||||
category: category,
|
||||
title: notificationTitle,
|
||||
body: notificationBody ?? "",
|
||||
body: (notificationBody ?? ""),
|
||||
userInfo: userInfo,
|
||||
previewType: previewType,
|
||||
sound: sound,
|
||||
threadVariant: thread.variant,
|
||||
threadName: threadName,
|
||||
replacingIdentifier: UUID().uuidString
|
||||
)
|
||||
}
|
||||
|
@ -328,6 +366,13 @@ public class NotificationPresenter: NSObject, NotificationsProtocol {
|
|||
let userInfo = [
|
||||
AppNotificationUserInfoKey.threadId: thread.id
|
||||
]
|
||||
|
||||
let threadName: String = SessionThread.displayName(
|
||||
threadId: thread.id,
|
||||
variant: thread.variant,
|
||||
closedGroupName: nil, // Not supported
|
||||
openGroupName: nil // Not supported
|
||||
)
|
||||
|
||||
DispatchQueue.main.async {
|
||||
let sound = self.requestSound(thread: thread)
|
||||
|
@ -337,7 +382,10 @@ public class NotificationPresenter: NSObject, NotificationsProtocol {
|
|||
title: notificationTitle,
|
||||
body: notificationBody,
|
||||
userInfo: userInfo,
|
||||
previewType: previewType,
|
||||
sound: sound,
|
||||
threadVariant: thread.variant,
|
||||
threadName: threadName,
|
||||
replacingIdentifier: UUID().uuidString
|
||||
)
|
||||
}
|
||||
|
@ -347,24 +395,24 @@ public class NotificationPresenter: NSObject, NotificationsProtocol {
|
|||
let notificationTitle: String?
|
||||
let previewType: Preferences.NotificationPreviewType = db[.preferencesNotificationPreviewType]
|
||||
.defaulting(to: .nameAndPreview)
|
||||
let threadName: String = SessionThread.displayName(
|
||||
threadId: thread.id,
|
||||
variant: thread.variant,
|
||||
closedGroupName: try? thread.closedGroup
|
||||
.select(.name)
|
||||
.asRequest(of: String.self)
|
||||
.fetchOne(db),
|
||||
openGroupName: try? thread.openGroup
|
||||
.select(.name)
|
||||
.asRequest(of: String.self)
|
||||
.fetchOne(db),
|
||||
isNoteToSelf: (thread.isNoteToSelf(db) == true),
|
||||
profile: try? Profile.fetchOne(db, id: thread.id)
|
||||
)
|
||||
|
||||
switch previewType {
|
||||
case .noNameNoPreview: notificationTitle = nil
|
||||
case .nameNoPreview, .nameAndPreview:
|
||||
notificationTitle = SessionThread.displayName(
|
||||
threadId: thread.id,
|
||||
variant: thread.variant,
|
||||
closedGroupName: try? thread.closedGroup
|
||||
.select(.name)
|
||||
.asRequest(of: String.self)
|
||||
.fetchOne(db),
|
||||
openGroupName: try? thread.openGroup
|
||||
.select(.name)
|
||||
.asRequest(of: String.self)
|
||||
.fetchOne(db),
|
||||
isNoteToSelf: (thread.isNoteToSelf(db) == true),
|
||||
profile: try? Profile.fetchOne(db, id: thread.id)
|
||||
)
|
||||
case .nameNoPreview, .nameAndPreview: notificationTitle = threadName
|
||||
}
|
||||
|
||||
let notificationBody = NotificationStrings.failedToSendBody
|
||||
|
@ -381,7 +429,10 @@ public class NotificationPresenter: NSObject, NotificationsProtocol {
|
|||
title: notificationTitle,
|
||||
body: notificationBody,
|
||||
userInfo: userInfo,
|
||||
sound: sound
|
||||
previewType: previewType,
|
||||
sound: sound,
|
||||
threadVariant: thread.variant,
|
||||
threadName: threadName
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -57,8 +57,9 @@ class UserNotificationPresenterAdaptee: NSObject, UNUserNotificationCenterDelega
|
|||
|
||||
override init() {
|
||||
self.notificationCenter = UNUserNotificationCenter.current()
|
||||
|
||||
super.init()
|
||||
notificationCenter.delegate = 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?) {
|
||||
AssertIsOnMainThread()
|
||||
notify(category: category, title: title, body: body, userInfo: userInfo, sound: sound, replacingIdentifier: nil)
|
||||
}
|
||||
|
||||
func notify(category: AppNotificationCategory, title: String?, body: String, userInfo: [AnyHashable: Any], sound: Preferences.Sound?, replacingIdentifier: String?) {
|
||||
func notify(
|
||||
category: AppNotificationCategory,
|
||||
title: String?,
|
||||
body: String,
|
||||
userInfo: [AnyHashable: Any],
|
||||
previewType: Preferences.NotificationPreviewType,
|
||||
sound: Preferences.Sound?,
|
||||
threadVariant: SessionThread.Variant,
|
||||
threadName: String,
|
||||
replacingIdentifier: String?
|
||||
) {
|
||||
AssertIsOnMainThread()
|
||||
|
||||
let threadIdentifier: String? = (userInfo[AppNotificationUserInfoKey.threadId] as? String)
|
||||
let content = UNMutableNotificationContent()
|
||||
content.categoryIdentifier = category.identifier
|
||||
content.userInfo = userInfo
|
||||
let isReplacingNotification = replacingIdentifier != nil
|
||||
var isBackgroudPoll = false
|
||||
if let threadIdentifier = userInfo[AppNotificationUserInfoKey.threadId] as? String {
|
||||
content.threadIdentifier = threadIdentifier
|
||||
isBackgroudPoll = replacingIdentifier == threadIdentifier
|
||||
}
|
||||
content.threadIdentifier = (threadIdentifier ?? content.threadIdentifier)
|
||||
|
||||
let shouldGroupNotification: Bool = (
|
||||
threadVariant == .openGroup &&
|
||||
replacingIdentifier == threadIdentifier
|
||||
)
|
||||
let isAppActive = UIApplication.shared.applicationState == .active
|
||||
if let sound = sound, sound != .none {
|
||||
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 let displayableTitle = title?.filterForDisplay {
|
||||
|
@ -117,30 +126,50 @@ extension UserNotificationPresenterAdaptee: NotificationPresenterAdaptee {
|
|||
if let displayableBody = body.filterForDisplay {
|
||||
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.
|
||||
Logger.debug("supressing notification body")
|
||||
}
|
||||
|
||||
let trigger: UNNotificationTrigger?
|
||||
if isBackgroudPoll {
|
||||
trigger = UNTimeIntervalNotificationTrigger(timeInterval: kNotificationDelayForBackgroumdPoll, repeats: false)
|
||||
let numberOfNotifications: Int
|
||||
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)
|
||||
let request = UNNotificationRequest(
|
||||
identifier: notificationIdentifier,
|
||||
content: content,
|
||||
trigger: trigger
|
||||
)
|
||||
|
||||
Logger.debug("presenting notification with identifier: \(notificationIdentifier)")
|
||||
|
||||
if isReplacingNotification { cancelNotifications(identifiers: [notificationIdentifier]) }
|
||||
|
||||
notificationCenter.add(request)
|
||||
notifications[notificationIdentifier] = request
|
||||
}
|
||||
|
@ -196,7 +225,7 @@ extension UserNotificationPresenterAdaptee: NotificationPresenterAdaptee {
|
|||
guard let conversationViewController = UIApplication.shared.frontmostViewController as? ConversationVC else {
|
||||
return true
|
||||
}
|
||||
|
||||
|
||||
/// Show notifications for any **other** threads
|
||||
return (conversationViewController.viewModel.threadData.threadId != notificationThreadId)
|
||||
}
|
||||
|
|
|
@ -164,12 +164,14 @@ public final class FullConversationCell: UITableViewCell {
|
|||
|
||||
// Unread count view
|
||||
unreadCountView.addSubview(unreadCountLabel)
|
||||
unreadCountLabel.setCompressionResistanceHigh()
|
||||
unreadCountLabel.pin([ VerticalEdge.top, VerticalEdge.bottom ], to: unreadCountView)
|
||||
unreadCountView.pin(.leading, to: .leading, of: unreadCountLabel, withInset: -4)
|
||||
unreadCountView.pin(.trailing, to: .trailing, of: unreadCountLabel, withInset: 4)
|
||||
|
||||
// Has mention view
|
||||
hasMentionView.addSubview(hasMentionLabel)
|
||||
hasMentionLabel.setCompressionResistanceHigh()
|
||||
hasMentionLabel.pin(to: hasMentionView)
|
||||
|
||||
// Label stack view
|
||||
|
|
|
@ -34,7 +34,7 @@ public final class BackgroundPoller {
|
|||
poller.stop()
|
||||
|
||||
return poller.poll(
|
||||
isBackgroundPoll: true,
|
||||
calledFromBackgroundPoller: true,
|
||||
isBackgroundPollerValid: { BackgroundPoller.isValid },
|
||||
isPostCapabilitiesRetry: false
|
||||
)
|
||||
|
@ -82,7 +82,7 @@ public final class BackgroundPoller {
|
|||
groupPublicKey,
|
||||
on: DispatchQueue.main,
|
||||
maxRetryCount: 0,
|
||||
isBackgroundPoll: true,
|
||||
calledFromBackgroundPoller: true,
|
||||
isBackgroundPollValid: { BackgroundPoller.isValid }
|
||||
)
|
||||
}
|
||||
|
@ -134,7 +134,7 @@ public final class BackgroundPoller {
|
|||
threadId: threadId,
|
||||
details: MessageReceiveJob.Details(
|
||||
messages: threadMessages.map { $0.messageInfo },
|
||||
isBackgroundPoll: true
|
||||
calledFromBackgroundPoller: true
|
||||
)
|
||||
)
|
||||
|
||||
|
|
|
@ -50,8 +50,8 @@ final class IP2Country {
|
|||
|
||||
@objc func populateCacheIfNeededAsync() {
|
||||
// This has to be sync since the `countryNamesCache` dict doesn't like async access
|
||||
IP2Country.workQueue.sync {
|
||||
let _ = self.populateCacheIfNeeded()
|
||||
IP2Country.workQueue.sync { [weak self] in
|
||||
_ = self?.populateCacheIfNeeded()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -18,10 +18,9 @@ public enum SNMessagingKit { // Just to make the external API nice
|
|||
],
|
||||
[
|
||||
_005_FixDeletedMessageReadState.self,
|
||||
_006_FixHiddenModAdminSupport.self
|
||||
],
|
||||
[
|
||||
_007_EmojiReacts.self
|
||||
_006_FixHiddenModAdminSupport.self,
|
||||
_007_HomeQueryOptimisationIndexes.self,
|
||||
_008_EmojiReacts.self
|
||||
]
|
||||
]
|
||||
)
|
||||
|
|
|
@ -1250,7 +1250,7 @@ enum _003_YDBToGRDBMigration: Migration {
|
|||
threadId: processedMessage.threadId,
|
||||
details: MessageReceiveJob.Details(
|
||||
messages: [processedMessage.messageInfo],
|
||||
isBackgroundPoll: legacyJob.isBackgroundPoll
|
||||
calledFromBackgroundPoller: legacyJob.isBackgroundPoll
|
||||
)
|
||||
)?.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
|
||||
}
|
||||
}
|
|
@ -5,7 +5,7 @@ import GRDB
|
|||
import SessionUtilitiesKit
|
||||
|
||||
/// This migration adds the new types needed for Emoji Reacts
|
||||
enum _007_EmojiReacts: Migration {
|
||||
enum _008_EmojiReacts: Migration {
|
||||
static let target: TargetMigrations.Identifier = .messagingKit
|
||||
static let identifier: String = "EmojiReacts"
|
||||
static let needsConfigSync: Bool = false
|
|
@ -262,7 +262,7 @@ public struct Interaction: Codable, Identifiable, Equatable, FetchableRecord, Mu
|
|||
self.body = body
|
||||
self.timestampMs = timestampMs
|
||||
self.receivedAtTimestampMs = receivedAtTimestampMs
|
||||
self.wasRead = (wasRead && variant.canBeUnread)
|
||||
self.wasRead = (wasRead || !variant.canBeUnread)
|
||||
self.hasMention = hasMention
|
||||
self.expiresInSeconds = expiresInSeconds
|
||||
self.expiresStartedAtMs = expiresStartedAtMs
|
||||
|
@ -304,7 +304,7 @@ public struct Interaction: Codable, Identifiable, Equatable, FetchableRecord, Mu
|
|||
default: return timestampMs
|
||||
}
|
||||
}()
|
||||
self.wasRead = (wasRead && variant.canBeUnread)
|
||||
self.wasRead = (wasRead || !variant.canBeUnread)
|
||||
self.hasMention = hasMention
|
||||
self.expiresInSeconds = expiresInSeconds
|
||||
self.expiresStartedAtMs = expiresStartedAtMs
|
||||
|
@ -409,7 +409,7 @@ public extension Interaction {
|
|||
body: (body ?? self.body),
|
||||
timestampMs: (timestampMs ?? self.timestampMs),
|
||||
receivedAtTimestampMs: self.receivedAtTimestampMs,
|
||||
wasRead: (wasRead ?? self.wasRead),
|
||||
wasRead: ((wasRead ?? self.wasRead) || !self.variant.canBeUnread),
|
||||
hasMention: (hasMention ?? self.hasMention),
|
||||
expiresInSeconds: (expiresInSeconds ?? self.expiresInSeconds),
|
||||
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 trySendReadReceipt {
|
||||
JobRunner.upsert(
|
||||
|
@ -573,18 +590,27 @@ public extension Interaction {
|
|||
|
||||
var notificationIdentifiers: [String] {
|
||||
[
|
||||
notificationIdentifier(isBackgroundPoll: true),
|
||||
notificationIdentifier(isBackgroundPoll: false)
|
||||
notificationIdentifier(shouldGroupMessagesForThread: true),
|
||||
notificationIdentifier(shouldGroupMessagesForThread: false)
|
||||
]
|
||||
}
|
||||
|
||||
// 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
|
||||
guard !isBackgroundPoll else { return threadId }
|
||||
return Interaction.notificationIdentifier(
|
||||
for: (id ?? 0),
|
||||
threadId: threadId,
|
||||
shouldGroupMessagesForThread: shouldGroupMessagesForThread
|
||||
)
|
||||
}
|
||||
|
||||
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 ?? 0)"
|
||||
return "\(threadId)-\(id)"
|
||||
}
|
||||
|
||||
func markingAsDeleted() -> Interaction {
|
||||
|
@ -598,7 +624,7 @@ public extension Interaction {
|
|||
body: nil,
|
||||
timestampMs: timestampMs,
|
||||
receivedAtTimestampMs: receivedAtTimestampMs,
|
||||
wasRead: (wasRead && Variant.standardIncomingDeleted.canBeUnread),
|
||||
wasRead: (wasRead || !Variant.standardIncomingDeleted.canBeUnread),
|
||||
hasMention: hasMention,
|
||||
expiresInSeconds: expiresInSeconds,
|
||||
expiresStartedAtMs: expiresStartedAtMs,
|
||||
|
|
|
@ -202,23 +202,24 @@ 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 interaction: TypedTableAlias<Interaction> = TypedTableAlias()
|
||||
let contact: TypedTableAlias<Contact> = TypedTableAlias()
|
||||
|
||||
return """
|
||||
SELECT \(thread[.id])
|
||||
FROM \(SessionThread.self)
|
||||
JOIN \(Interaction.self) ON (
|
||||
\(interaction[.threadId]) = \(thread[.id]) AND
|
||||
\(interaction[.wasRead]) = false
|
||||
SELECT COUNT(DISTINCT id) FROM (
|
||||
SELECT \(thread[.id]) AS id
|
||||
FROM \(SessionThread.self)
|
||||
JOIN \(Interaction.self) ON (
|
||||
\(interaction[.threadId]) = \(thread[.id]) AND
|
||||
\(interaction[.wasRead]) = false
|
||||
)
|
||||
LEFT JOIN \(Contact.self) ON \(contact[.id]) = \(thread[.id])
|
||||
WHERE (
|
||||
\(SessionThread.isMessageRequest(userPublicKey: userPublicKey, includeNonVisible: includeNonVisible))
|
||||
)
|
||||
)
|
||||
LEFT JOIN \(Contact.self) ON \(contact[.id]) = \(thread[.id])
|
||||
WHERE (
|
||||
\(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
|
||||
if !hasHiddenMessageRequests {
|
||||
let numUnreadMessageRequestThreads: Int = (try? SessionThread
|
||||
.unreadMessageRequestsThreadIdQuery(userPublicKey: userPublicKey, includeNonVisible: true)
|
||||
.fetchCount(db))
|
||||
.unreadMessageRequestsCountQuery(userPublicKey: userPublicKey, includeNonVisible: true)
|
||||
.fetchOne(db))
|
||||
.defaulting(to: 1)
|
||||
|
||||
guard numUnreadMessageRequestThreads == 1 else { return false }
|
||||
|
|
|
@ -37,8 +37,7 @@ public enum MessageReceiveJob: JobExecutor {
|
|||
db,
|
||||
message: messageInfo.message,
|
||||
associatedWithProto: try SNProtoContent.parseData(messageInfo.serializedProtoData),
|
||||
openGroupId: nil,
|
||||
isBackgroundPoll: details.isBackgroundPoll
|
||||
openGroupId: nil
|
||||
)
|
||||
}
|
||||
catch {
|
||||
|
@ -76,7 +75,7 @@ public enum MessageReceiveJob: JobExecutor {
|
|||
.with(
|
||||
details: Details(
|
||||
messages: remainingMessagesToProcess,
|
||||
isBackgroundPoll: details.isBackgroundPoll
|
||||
calledFromBackgroundPoller: details.calledFromBackgroundPoller
|
||||
)
|
||||
)
|
||||
.defaulting(to: job)
|
||||
|
@ -164,14 +163,18 @@ extension MessageReceiveJob {
|
|||
}
|
||||
|
||||
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(
|
||||
messages: [MessageInfo],
|
||||
isBackgroundPoll: Bool
|
||||
calledFromBackgroundPoller: Bool
|
||||
) {
|
||||
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/NSData+messagePadding.h>
|
||||
#import <SessionMessagingKit/OWSAudioPlayer.h>
|
||||
#import <SessionMessagingKit/OWSBackgroundTask.h>
|
||||
#import <SessionMessagingKit/OWSWindowManager.h>
|
||||
|
|
|
@ -512,7 +512,6 @@ public final class OpenGroupManager: NSObject {
|
|||
messages: [OpenGroupAPI.Message],
|
||||
for roomToken: String,
|
||||
on server: String,
|
||||
isBackgroundPoll: Bool,
|
||||
dependencies: OGMDependencies = OGMDependencies()
|
||||
) {
|
||||
// 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,
|
||||
associatedWithProto: try SNProtoContent.parseData(messageInfo.serializedProtoData),
|
||||
openGroupId: openGroup.id,
|
||||
isBackgroundPoll: isBackgroundPoll,
|
||||
dependencies: dependencies
|
||||
)
|
||||
}
|
||||
|
@ -619,7 +617,6 @@ public final class OpenGroupManager: NSObject {
|
|||
messages: [OpenGroupAPI.DirectMessage],
|
||||
fromOutbox: Bool,
|
||||
on server: String,
|
||||
isBackgroundPoll: Bool,
|
||||
dependencies: OGMDependencies = OGMDependencies()
|
||||
) {
|
||||
// Don't need to do anything if we have no messages (it's a valid case)
|
||||
|
@ -716,7 +713,6 @@ public final class OpenGroupManager: NSObject {
|
|||
message: messageInfo.message,
|
||||
associatedWithProto: try SNProtoContent.parseData(messageInfo.serializedProtoData),
|
||||
openGroupId: nil, // Intentionally nil as they are technically not open group messages
|
||||
isBackgroundPoll: isBackgroundPoll,
|
||||
dependencies: dependencies
|
||||
)
|
||||
}
|
||||
|
|
|
@ -12,7 +12,6 @@ extension MessageReceiver {
|
|||
message: VisibleMessage,
|
||||
associatedWithProto proto: SNProtoContent,
|
||||
openGroupId: String?,
|
||||
isBackgroundPoll: Bool,
|
||||
dependencies: Dependencies = Dependencies()
|
||||
) throws -> Int64 {
|
||||
guard let sender: String = message.sender, let dataMessage = proto.dataMessage else {
|
||||
|
@ -290,8 +289,7 @@ extension MessageReceiver {
|
|||
.notifyUser(
|
||||
db,
|
||||
for: interaction,
|
||||
in: thread,
|
||||
isBackgroundPoll: isBackgroundPoll
|
||||
in: thread
|
||||
)
|
||||
|
||||
return interactionId
|
||||
|
|
|
@ -180,7 +180,6 @@ public enum MessageReceiver {
|
|||
message: Message,
|
||||
associatedWithProto proto: SNProtoContent,
|
||||
openGroupId: String?,
|
||||
isBackgroundPoll: Bool,
|
||||
dependencies: SMKDependencies = SMKDependencies()
|
||||
) throws {
|
||||
switch message {
|
||||
|
@ -206,7 +205,7 @@ public enum MessageReceiver {
|
|||
try MessageReceiver.handleUnsendRequest(db, message: message)
|
||||
|
||||
case let message as CallMessage:
|
||||
try MessageReceiver.handleCallMessage(db, message: message)
|
||||
try MessageReceiver.handleCallMessage(db, message: message)
|
||||
|
||||
case let message as MessageRequestResponse:
|
||||
try MessageReceiver.handleMessageRequestResponse(db, message: message, dependencies: dependencies)
|
||||
|
@ -216,8 +215,7 @@ public enum MessageReceiver {
|
|||
db,
|
||||
message: message,
|
||||
associatedWithProto: proto,
|
||||
openGroupId: openGroupId,
|
||||
isBackgroundPoll: isBackgroundPoll
|
||||
openGroupId: openGroupId
|
||||
)
|
||||
|
||||
default: fatalError()
|
||||
|
|
|
@ -4,9 +4,15 @@ import Foundation
|
|||
import GRDB
|
||||
|
||||
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, forReaction reaction: Reaction, in thread: SessionThread)
|
||||
func cancelNotifications(identifiers: [String])
|
||||
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,
|
||||
on queue: DispatchQueue = SessionSnodeKit.Threading.workQueue,
|
||||
maxRetryCount: UInt = 0,
|
||||
isBackgroundPoll: Bool = false,
|
||||
calledFromBackgroundPoller: Bool = false,
|
||||
isBackgroundPollValid: @escaping (() -> Bool) = { true },
|
||||
poller: ClosedGroupPoller? = nil
|
||||
) -> Promise<Void> {
|
||||
|
@ -156,7 +156,7 @@ public final class ClosedGroupPoller {
|
|||
|
||||
return attempt(maxRetryCount: maxRetryCount, recoveringOn: queue) {
|
||||
guard
|
||||
(isBackgroundPoll && isBackgroundPollValid()) ||
|
||||
(calledFromBackgroundPoller && isBackgroundPollValid()) ||
|
||||
poller?.isPolling.wrappedValue[groupPublicKey] == true
|
||||
else { return Promise(error: Error.pollingCanceled) }
|
||||
|
||||
|
@ -178,7 +178,7 @@ public final class ClosedGroupPoller {
|
|||
return when(resolved: promises)
|
||||
.then(on: queue) { messageResults -> Promise<Void> in
|
||||
guard
|
||||
(isBackgroundPoll && isBackgroundPollValid()) ||
|
||||
(calledFromBackgroundPoller && isBackgroundPollValid()) ||
|
||||
poller?.isPolling.wrappedValue[groupPublicKey] == true
|
||||
else { return Promise.value(()) }
|
||||
|
||||
|
@ -195,7 +195,7 @@ public final class ClosedGroupPoller {
|
|||
|
||||
// No need to do anything if there are no messages
|
||||
guard !allMessages.isEmpty else {
|
||||
if !isBackgroundPoll {
|
||||
if !calledFromBackgroundPoller {
|
||||
SNLog("Received no new messages in closed group with public key: \(groupPublicKey)")
|
||||
}
|
||||
return Promise.value(())
|
||||
|
@ -221,7 +221,7 @@ public final class ClosedGroupPoller {
|
|||
// In the background ignore 'SQLITE_ABORT' (it generally means
|
||||
// the BackgroundPoller has timed out
|
||||
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).")
|
||||
break
|
||||
|
@ -241,16 +241,16 @@ public final class ClosedGroupPoller {
|
|||
threadId: groupPublicKey,
|
||||
details: MessageReceiveJob.Details(
|
||||
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
|
||||
// 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
|
||||
promises = promises.appending(
|
||||
jobToRun.map { job -> Promise<Void> in
|
||||
|
@ -278,7 +278,7 @@ public final class ClosedGroupPoller {
|
|||
}
|
||||
}
|
||||
|
||||
if !isBackgroundPoll {
|
||||
if !calledFromBackgroundPoller {
|
||||
promise.catch2 { error in
|
||||
SNLog("Polling failed for closed group with public key: \(groupPublicKey) due to error: \(error).")
|
||||
}
|
||||
|
|
|
@ -67,12 +67,12 @@ extension OpenGroupAPI {
|
|||
|
||||
@discardableResult
|
||||
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
|
||||
public func poll(
|
||||
isBackgroundPoll: Bool,
|
||||
calledFromBackgroundPoller: Bool,
|
||||
isBackgroundPollerValid: @escaping (() -> Bool) = { true },
|
||||
isPostCapabilitiesRetry: Bool,
|
||||
using dependencies: OpenGroupManager.OGMDependencies = OpenGroupManager.OGMDependencies()
|
||||
|
@ -107,7 +107,7 @@ extension OpenGroupAPI {
|
|||
.map(on: OpenGroupAPI.workQueue) { (failureCount, $0) }
|
||||
}
|
||||
.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
|
||||
// then just stop
|
||||
self?.isPolling = false
|
||||
|
@ -119,7 +119,6 @@ extension OpenGroupAPI {
|
|||
self?.handlePollResponse(
|
||||
response,
|
||||
failureCount: failureCount,
|
||||
isBackgroundPoll: isBackgroundPoll,
|
||||
using: dependencies
|
||||
)
|
||||
|
||||
|
@ -133,7 +132,7 @@ extension OpenGroupAPI {
|
|||
seal.fulfill(())
|
||||
}
|
||||
.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
|
||||
// then just stop
|
||||
self?.isPolling = false
|
||||
|
@ -145,7 +144,8 @@ extension OpenGroupAPI {
|
|||
// method will always resolve)
|
||||
self?.updateCapabilitiesAndRetryIfNeeded(
|
||||
server: server,
|
||||
isBackgroundPoll: isBackgroundPoll,
|
||||
calledFromBackgroundPoller: calledFromBackgroundPoller,
|
||||
isBackgroundPollerValid: isBackgroundPollerValid,
|
||||
isPostCapabilitiesRetry: isPostCapabilitiesRetry,
|
||||
error: error
|
||||
)
|
||||
|
@ -186,7 +186,8 @@ extension OpenGroupAPI {
|
|||
|
||||
private func updateCapabilitiesAndRetryIfNeeded(
|
||||
server: String,
|
||||
isBackgroundPoll: Bool,
|
||||
calledFromBackgroundPoller: Bool,
|
||||
isBackgroundPollerValid: @escaping (() -> Bool) = { true },
|
||||
isPostCapabilitiesRetry: Bool,
|
||||
error: Error,
|
||||
using dependencies: OpenGroupManager.OGMDependencies = OpenGroupManager.OGMDependencies()
|
||||
|
@ -233,7 +234,8 @@ extension OpenGroupAPI {
|
|||
// Regardless of the outcome we can just resolve this
|
||||
// immediately as it'll handle it's own response
|
||||
return strongSelf.poll(
|
||||
isBackgroundPoll: isBackgroundPoll,
|
||||
calledFromBackgroundPoller: calledFromBackgroundPoller,
|
||||
isBackgroundPollerValid: isBackgroundPollerValid,
|
||||
isPostCapabilitiesRetry: true,
|
||||
using: dependencies
|
||||
)
|
||||
|
@ -251,7 +253,6 @@ extension OpenGroupAPI {
|
|||
private func handlePollResponse(
|
||||
_ response: PollResponse,
|
||||
failureCount: Int64,
|
||||
isBackgroundPoll: Bool,
|
||||
using dependencies: OpenGroupManager.OGMDependencies = OpenGroupManager.OGMDependencies()
|
||||
) {
|
||||
let server: String = self.server
|
||||
|
@ -440,7 +441,6 @@ extension OpenGroupAPI {
|
|||
messages: responseBody.compactMap { $0.value },
|
||||
for: roomToken,
|
||||
on: server,
|
||||
isBackgroundPoll: isBackgroundPoll,
|
||||
dependencies: dependencies
|
||||
)
|
||||
|
||||
|
@ -464,7 +464,6 @@ extension OpenGroupAPI {
|
|||
messages: messages,
|
||||
fromOutbox: fromOutbox,
|
||||
on: server,
|
||||
isBackgroundPoll: isBackgroundPoll,
|
||||
dependencies: dependencies
|
||||
)
|
||||
|
||||
|
|
|
@ -173,7 +173,7 @@ public final class Poller {
|
|||
threadId: threadId,
|
||||
details: MessageReceiveJob.Details(
|
||||
messages: threadMessages.map { $0.messageInfo },
|
||||
isBackgroundPoll: false
|
||||
calledFromBackgroundPoller: false
|
||||
)
|
||||
)
|
||||
)
|
||||
|
|
|
@ -404,7 +404,6 @@ public extension SessionThreadViewModel {
|
|||
/// but including this warning just in case there is a discrepancy)
|
||||
static func baseQuery(
|
||||
userPublicKey: String,
|
||||
filterSQL: SQL,
|
||||
groupSQL: SQL,
|
||||
orderSQL: SQL
|
||||
) -> (([Int64]) -> AdaptedFetchRequest<SQLRequest<SessionThreadViewModel>>) {
|
||||
|
@ -422,6 +421,7 @@ public extension SessionThreadViewModel {
|
|||
let interactionAttachment: TypedTableAlias<InteractionAttachment> = 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 profileNicknameColumnLiteral: SQL = SQL(stringLiteral: Profile.Columns.nickname.name)
|
||||
let profileNameColumnLiteral: SQL = SQL(stringLiteral: Profile.Columns.name.name)
|
||||
|
@ -466,7 +466,7 @@ public extension SessionThreadViewModel {
|
|||
|
||||
\(Interaction.self).\(ViewModel.interactionIdKey),
|
||||
\(Interaction.self).\(ViewModel.interactionVariantKey),
|
||||
\(Interaction.self).\(ViewModel.interactionTimestampMsKey),
|
||||
\(Interaction.self).\(interactionTimestampMsColumnLiteral) AS \(ViewModel.interactionTimestampMsKey),
|
||||
\(Interaction.self).\(ViewModel.interactionBodyKey),
|
||||
|
||||
-- Default to 'sending' assuming non-processed interaction when null
|
||||
|
@ -494,7 +494,7 @@ public extension SessionThreadViewModel {
|
|||
\(interaction[.id]) AS \(ViewModel.interactionIdKey),
|
||||
\(interaction[.threadId]),
|
||||
\(interaction[.variant]) AS \(ViewModel.interactionVariantKey),
|
||||
MAX(\(interaction[.timestampMs])) AS \(ViewModel.interactionTimestampMsKey),
|
||||
MAX(\(interaction[.timestampMs])) AS \(interactionTimestampMsColumnLiteral),
|
||||
\(interaction[.body]) AS \(ViewModel.interactionBodyKey),
|
||||
\(interaction[.authorId]),
|
||||
\(interaction[.linkPreviewUrl]),
|
||||
|
@ -515,7 +515,7 @@ public extension SessionThreadViewModel {
|
|||
LEFT JOIN \(LinkPreview.self) ON (
|
||||
\(linkPreview[.url]) = \(interaction[.linkPreviewUrl]) AND
|
||||
\(SQL("\(linkPreview[.variant]) = \(LinkPreview.Variant.openGroupInvitation)")) AND
|
||||
\(Interaction.linkPreviewFilterLiteral(timestampColumn: ViewModel.interactionTimestampMsKey))
|
||||
\(Interaction.linkPreviewFilterLiteral(timestampColumn: interactionTimestampMsColumnLiteral))
|
||||
)
|
||||
LEFT JOIN \(InteractionAttachment.self) AS \(firstInteractionAttachmentLiteral) ON (
|
||||
\(firstInteractionAttachmentLiteral).\(interactionAttachmentAlbumIndexColumnLiteral) = 0 AND
|
||||
|
@ -599,12 +599,14 @@ public extension SessionThreadViewModel {
|
|||
let contact: TypedTableAlias<Contact> = TypedTableAlias()
|
||||
let interaction: TypedTableAlias<Interaction> = TypedTableAlias()
|
||||
|
||||
let interactionTimestampMsColumnLiteral: SQL = SQL(stringLiteral: Interaction.Columns.timestampMs.name)
|
||||
|
||||
return """
|
||||
LEFT JOIN \(Contact.self) ON \(contact[.id]) = \(thread[.id])
|
||||
LEFT JOIN (
|
||||
SELECT
|
||||
\(interaction[.threadId]),
|
||||
MAX(\(interaction[.timestampMs])) AS \(ViewModel.interactionTimestampMsKey)
|
||||
MAX(\(interaction[.timestampMs])) AS \(interactionTimestampMsColumnLiteral)
|
||||
FROM \(Interaction.self)
|
||||
WHERE \(SQL("\(interaction[.variant]) != \(Interaction.Variant.standardIncomingDeleted)"))
|
||||
GROUP BY \(interaction[.threadId])
|
||||
|
@ -615,6 +617,7 @@ public extension SessionThreadViewModel {
|
|||
static func homeFilterSQL(userPublicKey: String) -> SQL {
|
||||
let thread: TypedTableAlias<SessionThread> = TypedTableAlias()
|
||||
let contact: TypedTableAlias<Contact> = TypedTableAlias()
|
||||
let interaction: TypedTableAlias<Interaction> = TypedTableAlias()
|
||||
|
||||
return """
|
||||
\(thread[.shouldBeVisible]) = true AND (
|
||||
|
@ -625,7 +628,7 @@ public extension SessionThreadViewModel {
|
|||
) AND (
|
||||
-- Only show the 'Note to Self' thread if it has an interaction
|
||||
\(SQL("\(thread[.id]) != \(userPublicKey)")) OR
|
||||
\(Interaction.self).\(ViewModel.interactionTimestampMsKey) IS NOT NULL
|
||||
\(interaction[.timestampMs]) IS NOT NULL
|
||||
)
|
||||
"""
|
||||
}
|
||||
|
@ -652,14 +655,16 @@ public extension SessionThreadViewModel {
|
|||
|
||||
static let homeOrderSQL: SQL = {
|
||||
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 = {
|
||||
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")
|
||||
}()
|
||||
}
|
||||
|
||||
|
@ -738,7 +743,7 @@ public extension SessionThreadViewModel {
|
|||
SUM(\(interaction[.wasRead]) = false) AS \(ViewModel.threadUnreadCountKey)
|
||||
|
||||
FROM \(Interaction.self)
|
||||
GROUP BY \(interaction[.threadId])
|
||||
WHERE \(SQL("\(interaction[.threadId]) = \(threadId)"))
|
||||
) AS \(Interaction.self) ON \(interaction[.threadId]) = \(thread[.id])
|
||||
|
||||
LEFT JOIN \(Profile.self) AS \(ViewModel.contactProfileKey) ON \(ViewModel.contactProfileKey).\(profileIdColumnLiteral) = \(thread[.id])
|
||||
|
@ -752,17 +757,15 @@ public extension SessionThreadViewModel {
|
|||
LEFT JOIN (
|
||||
SELECT
|
||||
\(groupMember[.groupId]),
|
||||
COUNT(*) AS \(ViewModel.closedGroupUserCountKey)
|
||||
COUNT(\(groupMember.alias[Column.rowID])) AS \(ViewModel.closedGroupUserCountKey)
|
||||
FROM \(GroupMember.self)
|
||||
WHERE (
|
||||
\(SQL("\(groupMember[.groupId]) = \(threadId)")) AND
|
||||
\(SQL("\(groupMember[.role]) = \(GroupMember.Role.standard)"))
|
||||
)
|
||||
GROUP BY \(groupMember[.groupId])
|
||||
) AS \(closedGroupUserCountTableLiteral) ON \(SQL("\(closedGroupUserCountTableLiteral).\(groupMemberGroupIdColumnLiteral) = \(threadId)"))
|
||||
|
||||
WHERE \(SQL("\(thread[.id]) = \(threadId)"))
|
||||
GROUP BY \(thread[.id])
|
||||
"""
|
||||
|
||||
return request.adapted { db in
|
||||
|
|
|
@ -2120,7 +2120,6 @@ class OpenGroupManagerSpec: QuickSpec {
|
|||
],
|
||||
for: "testRoom",
|
||||
on: "testServer",
|
||||
isBackgroundPoll: false,
|
||||
dependencies: dependencies
|
||||
)
|
||||
}
|
||||
|
@ -2142,7 +2141,6 @@ class OpenGroupManagerSpec: QuickSpec {
|
|||
messages: [],
|
||||
for: "testRoom",
|
||||
on: "testServer",
|
||||
isBackgroundPoll: false,
|
||||
dependencies: dependencies
|
||||
)
|
||||
}
|
||||
|
@ -2182,7 +2180,6 @@ class OpenGroupManagerSpec: QuickSpec {
|
|||
],
|
||||
for: "testRoom",
|
||||
on: "testServer",
|
||||
isBackgroundPoll: false,
|
||||
dependencies: dependencies
|
||||
)
|
||||
}
|
||||
|
@ -2215,7 +2212,6 @@ class OpenGroupManagerSpec: QuickSpec {
|
|||
],
|
||||
for: "testRoom",
|
||||
on: "testServer",
|
||||
isBackgroundPoll: false,
|
||||
dependencies: dependencies
|
||||
)
|
||||
}
|
||||
|
@ -2230,7 +2226,6 @@ class OpenGroupManagerSpec: QuickSpec {
|
|||
messages: [testMessage],
|
||||
for: "testRoom",
|
||||
on: "testServer",
|
||||
isBackgroundPoll: false,
|
||||
dependencies: dependencies
|
||||
)
|
||||
}
|
||||
|
@ -2260,7 +2255,6 @@ class OpenGroupManagerSpec: QuickSpec {
|
|||
],
|
||||
for: "testRoom",
|
||||
on: "testServer",
|
||||
isBackgroundPoll: false,
|
||||
dependencies: dependencies
|
||||
)
|
||||
}
|
||||
|
@ -2298,7 +2292,6 @@ class OpenGroupManagerSpec: QuickSpec {
|
|||
],
|
||||
for: "testRoom",
|
||||
on: "testServer",
|
||||
isBackgroundPoll: false,
|
||||
dependencies: dependencies
|
||||
)
|
||||
}
|
||||
|
@ -2327,7 +2320,6 @@ class OpenGroupManagerSpec: QuickSpec {
|
|||
],
|
||||
for: "testRoom",
|
||||
on: "testServer",
|
||||
isBackgroundPoll: false,
|
||||
dependencies: dependencies
|
||||
)
|
||||
}
|
||||
|
@ -2379,7 +2371,6 @@ class OpenGroupManagerSpec: QuickSpec {
|
|||
messages: [],
|
||||
fromOutbox: false,
|
||||
on: "testServer",
|
||||
isBackgroundPoll: false,
|
||||
dependencies: dependencies
|
||||
)
|
||||
}
|
||||
|
@ -2413,7 +2404,6 @@ class OpenGroupManagerSpec: QuickSpec {
|
|||
messages: [testDirectMessage],
|
||||
fromOutbox: false,
|
||||
on: "testServer",
|
||||
isBackgroundPoll: false,
|
||||
dependencies: dependencies
|
||||
)
|
||||
}
|
||||
|
@ -2452,7 +2442,6 @@ class OpenGroupManagerSpec: QuickSpec {
|
|||
messages: [testDirectMessage],
|
||||
fromOutbox: false,
|
||||
on: "testServer",
|
||||
isBackgroundPoll: false,
|
||||
dependencies: dependencies
|
||||
)
|
||||
}
|
||||
|
@ -2478,7 +2467,6 @@ class OpenGroupManagerSpec: QuickSpec {
|
|||
messages: [testDirectMessage],
|
||||
fromOutbox: false,
|
||||
on: "testServer",
|
||||
isBackgroundPoll: false,
|
||||
dependencies: dependencies
|
||||
)
|
||||
}
|
||||
|
@ -2509,7 +2497,6 @@ class OpenGroupManagerSpec: QuickSpec {
|
|||
messages: [testDirectMessage],
|
||||
fromOutbox: false,
|
||||
on: "testServer",
|
||||
isBackgroundPoll: false,
|
||||
dependencies: dependencies
|
||||
)
|
||||
}
|
||||
|
@ -2524,7 +2511,6 @@ class OpenGroupManagerSpec: QuickSpec {
|
|||
messages: [testDirectMessage],
|
||||
fromOutbox: false,
|
||||
on: "testServer",
|
||||
isBackgroundPoll: false,
|
||||
dependencies: dependencies
|
||||
)
|
||||
}
|
||||
|
@ -2549,7 +2535,6 @@ class OpenGroupManagerSpec: QuickSpec {
|
|||
],
|
||||
fromOutbox: false,
|
||||
on: "testServer",
|
||||
isBackgroundPoll: false,
|
||||
dependencies: dependencies
|
||||
)
|
||||
}
|
||||
|
@ -2576,7 +2561,6 @@ class OpenGroupManagerSpec: QuickSpec {
|
|||
messages: [testDirectMessage],
|
||||
fromOutbox: true,
|
||||
on: "testServer",
|
||||
isBackgroundPoll: false,
|
||||
dependencies: dependencies
|
||||
)
|
||||
}
|
||||
|
@ -2607,7 +2591,6 @@ class OpenGroupManagerSpec: QuickSpec {
|
|||
messages: [testDirectMessage],
|
||||
fromOutbox: true,
|
||||
on: "testServer",
|
||||
isBackgroundPoll: false,
|
||||
dependencies: dependencies
|
||||
)
|
||||
}
|
||||
|
@ -2623,7 +2606,6 @@ class OpenGroupManagerSpec: QuickSpec {
|
|||
messages: [testDirectMessage],
|
||||
fromOutbox: true,
|
||||
on: "testServer",
|
||||
isBackgroundPoll: false,
|
||||
dependencies: dependencies
|
||||
)
|
||||
}
|
||||
|
@ -2659,7 +2641,6 @@ class OpenGroupManagerSpec: QuickSpec {
|
|||
messages: [testDirectMessage],
|
||||
fromOutbox: true,
|
||||
on: "testServer",
|
||||
isBackgroundPoll: false,
|
||||
dependencies: dependencies
|
||||
)
|
||||
}
|
||||
|
@ -2674,7 +2655,6 @@ class OpenGroupManagerSpec: QuickSpec {
|
|||
messages: [testDirectMessage],
|
||||
fromOutbox: true,
|
||||
on: "testServer",
|
||||
isBackgroundPoll: false,
|
||||
dependencies: dependencies
|
||||
)
|
||||
}
|
||||
|
@ -2699,7 +2679,6 @@ class OpenGroupManagerSpec: QuickSpec {
|
|||
],
|
||||
fromOutbox: true,
|
||||
on: "testServer",
|
||||
isBackgroundPoll: false,
|
||||
dependencies: dependencies
|
||||
)
|
||||
}
|
||||
|
|
|
@ -9,7 +9,7 @@ import SessionMessagingKit
|
|||
public class NSENotificationPresenter: NSObject, NotificationsProtocol {
|
||||
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)
|
||||
|
||||
// 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 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
|
||||
|
||||
if thread.variant == .closedGroup || thread.variant == .openGroup {
|
||||
|
@ -26,22 +32,11 @@ public class NSENotificationPresenter: NSObject, NotificationsProtocol {
|
|||
return
|
||||
}
|
||||
|
||||
notificationTitle = {
|
||||
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,
|
||||
senderName,
|
||||
groupName
|
||||
)
|
||||
}()
|
||||
notificationTitle = String(
|
||||
format: NotificationStrings.incomingGroupMessageTitleFormat,
|
||||
senderName,
|
||||
groupName
|
||||
)
|
||||
}
|
||||
|
||||
let snippet: String = (interaction.previewText(db)
|
||||
|
@ -88,21 +83,31 @@ public class NSENotificationPresenter: NSObject, NotificationsProtocol {
|
|||
notificationContent.body = "MESSAGE_REQUESTS_NOTIFICATION".localized()
|
||||
}
|
||||
|
||||
// Add request
|
||||
let identifier = interaction.notificationIdentifier(isBackgroundPoll: isBackgroundPoll)
|
||||
// Add request (try to group notifications for interactions from open groups)
|
||||
let identifier: String = interaction.notificationIdentifier(
|
||||
shouldGroupMessagesForThread: (thread.variant == .openGroup)
|
||||
)
|
||||
var trigger: UNNotificationTrigger?
|
||||
|
||||
if isBackgroundPoll {
|
||||
trigger = UNTimeIntervalNotificationTrigger(timeInterval: 5, repeats: false)
|
||||
if thread.variant == .openGroup {
|
||||
trigger = UNTimeIntervalNotificationTrigger(
|
||||
timeInterval: Notifications.delayForGroupedNotifications,
|
||||
repeats: false
|
||||
)
|
||||
|
||||
var numberOfNotifications: Int = (notifications[identifier]?
|
||||
let numberExistingNotifications: Int? = notifications[identifier]?
|
||||
.content
|
||||
.userInfo[NotificationServiceExtension.threadNotificationCounter]
|
||||
.asType(Int.self))
|
||||
.defaulting(to: 1)
|
||||
.asType(Int.self)
|
||||
var numberOfNotifications: Int = (numberExistingNotifications ?? 1)
|
||||
|
||||
if numberOfNotifications > 1 {
|
||||
if numberExistingNotifications != nil {
|
||||
numberOfNotifications += 1 // Add one for the current notification
|
||||
|
||||
notificationContent.title = (previewType == .noNameNoPreview ?
|
||||
notificationContent.title :
|
||||
groupName
|
||||
)
|
||||
notificationContent.body = String(
|
||||
format: NotificationStrings.incomingCollapsedMessagesBody,
|
||||
"\(numberOfNotifications)"
|
||||
|
@ -112,7 +117,11 @@ public class NSENotificationPresenter: NSObject, NotificationsProtocol {
|
|||
notificationContent.userInfo[NotificationServiceExtension.threadNotificationCounter] = numberOfNotifications
|
||||
}
|
||||
|
||||
addNotifcationRequest(identifier: identifier, notificationContent: notificationContent, trigger: trigger)
|
||||
addNotifcationRequest(
|
||||
identifier: identifier,
|
||||
notificationContent: notificationContent,
|
||||
trigger: trigger
|
||||
)
|
||||
}
|
||||
|
||||
public func notifyUser(_ db: Database, forIncomingCall interaction: Interaction, in thread: SessionThread) {
|
||||
|
@ -163,7 +172,11 @@ public class NSENotificationPresenter: NSObject, NotificationsProtocol {
|
|||
)
|
||||
}
|
||||
|
||||
addNotifcationRequest(identifier: UUID().uuidString, notificationContent: notificationContent, trigger: nil)
|
||||
addNotifcationRequest(
|
||||
identifier: UUID().uuidString,
|
||||
notificationContent: notificationContent,
|
||||
trigger: nil
|
||||
)
|
||||
}
|
||||
|
||||
public func notifyUser(_ db: Database, forReaction reaction: Reaction, in thread: SessionThread) {
|
||||
|
|
|
@ -83,8 +83,7 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension
|
|||
db,
|
||||
message: visibleMessage,
|
||||
associatedWithProto: processedMessage.proto,
|
||||
openGroupId: (isOpenGroup ? processedMessage.threadId : nil),
|
||||
isBackgroundPoll: false
|
||||
openGroupId: (isOpenGroup ? processedMessage.threadId : nil)
|
||||
)
|
||||
|
||||
// Remove the notifications if there is an outgoing messages from a linked device
|
||||
|
@ -329,7 +328,7 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension
|
|||
.defaulting(to: [])
|
||||
.map { server in
|
||||
OpenGroupAPI.Poller(for: server)
|
||||
.poll(isBackgroundPoll: true, isPostCapabilitiesRetry: false)
|
||||
.poll(calledFromBackgroundPoller: true, isPostCapabilitiesRetry: false)
|
||||
.timeout(
|
||||
seconds: 20,
|
||||
timeoutError: NotificationServiceError.timeout
|
||||
|
|
|
@ -379,7 +379,8 @@ public class PagedDatabaseObserver<ObservedTable, T>: TransactionObserver where
|
|||
let orderSQL: SQL = self.orderSQL
|
||||
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(
|
||||
db,
|
||||
tableName: pagedTableName,
|
||||
|
@ -387,7 +388,7 @@ public class PagedDatabaseObserver<ObservedTable, T>: TransactionObserver where
|
|||
filterSQL: filterSQL
|
||||
)
|
||||
|
||||
let queryInfo: (limit: Int, offset: Int, updatedCacheOffset: Int)? = {
|
||||
let (queryInfo, callback): (QueryInfo?, (() -> ())?) = {
|
||||
switch target {
|
||||
case .initialPageAround(let targetId):
|
||||
// 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
|
||||
guard let targetIndex: Int = maybeIndex else {
|
||||
return (currentPageInfo.pageSize, 0, 0)
|
||||
return ((currentPageInfo.pageSize, 0, 0), nil)
|
||||
}
|
||||
|
||||
let updatedOffset: Int = {
|
||||
|
@ -421,22 +422,28 @@ public class PagedDatabaseObserver<ObservedTable, T>: TransactionObserver where
|
|||
return (targetIndex - halfPageSize)
|
||||
}()
|
||||
|
||||
return (currentPageInfo.pageSize, updatedOffset, updatedOffset)
|
||||
return ((currentPageInfo.pageSize, updatedOffset, updatedOffset), nil)
|
||||
|
||||
case .pageBefore:
|
||||
let updatedOffset: Int = max(0, (currentPageInfo.pageOffset - currentPageInfo.pageSize))
|
||||
|
||||
return (
|
||||
currentPageInfo.pageSize,
|
||||
updatedOffset,
|
||||
updatedOffset
|
||||
(
|
||||
currentPageInfo.pageSize,
|
||||
updatedOffset,
|
||||
updatedOffset
|
||||
),
|
||||
nil
|
||||
)
|
||||
|
||||
case .pageAfter:
|
||||
return (
|
||||
currentPageInfo.pageSize,
|
||||
(currentPageInfo.pageOffset + currentPageInfo.currentCount),
|
||||
currentPageInfo.pageOffset
|
||||
(
|
||||
currentPageInfo.pageSize,
|
||||
(currentPageInfo.pageOffset + currentPageInfo.currentCount),
|
||||
currentPageInfo.pageOffset
|
||||
),
|
||||
nil
|
||||
)
|
||||
|
||||
case .untilInclusive(let targetId, let padding):
|
||||
|
@ -459,16 +466,19 @@ public class PagedDatabaseObserver<ObservedTable, T>: TransactionObserver where
|
|||
targetIndex < currentPageInfo.pageOffset ||
|
||||
targetIndex >= cacheCurrentEndIndex
|
||||
)
|
||||
else { return nil }
|
||||
else { return (nil, nil) }
|
||||
|
||||
// If the target is before the cached data then load before
|
||||
if targetIndex < currentPageInfo.pageOffset {
|
||||
let finalIndex: Int = max(0, (targetIndex - abs(padding)))
|
||||
|
||||
return (
|
||||
(currentPageInfo.pageOffset - finalIndex),
|
||||
finalIndex,
|
||||
finalIndex
|
||||
(
|
||||
(currentPageInfo.pageOffset - finalIndex),
|
||||
finalIndex,
|
||||
finalIndex
|
||||
),
|
||||
nil
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -477,23 +487,81 @@ public class PagedDatabaseObserver<ObservedTable, T>: TransactionObserver where
|
|||
let finalIndex: Int = min(totalCount, (targetIndex + 1 + abs(padding)))
|
||||
|
||||
return (
|
||||
(finalIndex - cacheCurrentEndIndex),
|
||||
cacheCurrentEndIndex,
|
||||
currentPageInfo.pageOffset
|
||||
(
|
||||
(finalIndex - cacheCurrentEndIndex),
|
||||
cacheCurrentEndIndex,
|
||||
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:
|
||||
return (
|
||||
currentPageInfo.currentCount,
|
||||
currentPageInfo.pageOffset,
|
||||
currentPageInfo.pageOffset
|
||||
(
|
||||
currentPageInfo.currentCount,
|
||||
currentPageInfo.pageOffset,
|
||||
currentPageInfo.pageOffset
|
||||
),
|
||||
nil
|
||||
)
|
||||
}
|
||||
}()
|
||||
|
||||
// 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)
|
||||
guard let queryInfo: (limit: Int, offset: Int, updatedCacheOffset: Int) = queryInfo else {
|
||||
guard let queryInfo: QueryInfo = queryInfo else {
|
||||
return (
|
||||
nil,
|
||||
PagedData.PageInfo(
|
||||
|
@ -501,7 +569,8 @@ public class PagedDatabaseObserver<ObservedTable, T>: TransactionObserver where
|
|||
pageOffset: currentPageInfo.pageOffset,
|
||||
currentCount: currentPageInfo.currentCount,
|
||||
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
|
||||
|
@ -554,6 +623,7 @@ public class PagedDatabaseObserver<ObservedTable, T>: TransactionObserver where
|
|||
self.pageInfo.mutate { $0 = updatedPageInfo }
|
||||
}
|
||||
self.isLoadingMoreData.mutate { $0 = false }
|
||||
loadedPage?.failureCallback?()
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -651,6 +721,7 @@ public protocol ErasedAssociatedRecord {
|
|||
pageInfo: PagedData.PageInfo
|
||||
) -> Bool
|
||||
@discardableResult func updateCache(_ db: Database, rowIds: [Int64], hasOtherChanges: Bool) -> Bool
|
||||
func clearCache(_ db: Database)
|
||||
func updateAssociatedData<O>(to unassociatedCache: DataCache<O>) -> DataCache<O>
|
||||
}
|
||||
|
||||
|
@ -735,6 +806,7 @@ public enum PagedData {
|
|||
case pageBefore
|
||||
case pageAfter
|
||||
case untilInclusive(id: SQLExpression, padding: Int)
|
||||
case jumpTo(id: SQLExpression, paddingForInclusive: Int)
|
||||
case reloadCurrent
|
||||
}
|
||||
|
||||
|
@ -757,6 +829,13 @@ public enum PagedData {
|
|||
/// the padding would mean more data should be loaded)
|
||||
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 {
|
||||
switch self {
|
||||
case .initialPageAround(let id): return .initialPageAround(id: id.sqlExpression)
|
||||
|
@ -764,6 +843,9 @@ public enum PagedData {
|
|||
case .pageAfter: return .pageAfter
|
||||
case .untilInclusive(let id, let padding):
|
||||
return .untilInclusive(id: id.sqlExpression, padding: padding)
|
||||
|
||||
case .jumpTo(let id, let paddingForInclusive):
|
||||
return .jumpTo(id: id.sqlExpression, paddingForInclusive: paddingForInclusive)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1146,6 +1228,10 @@ public class AssociatedRecord<T, PagedType>: ErasedAssociatedRecord where T: Fet
|
|||
return true
|
||||
}
|
||||
|
||||
public func clearCache(_ db: Database) {
|
||||
dataCache.mutate { $0 = DataCache() }
|
||||
}
|
||||
|
||||
public func updateAssociatedData<O>(to unassociatedCache: DataCache<O>) -> DataCache<O> {
|
||||
guard let typedCache: DataCache<PagedType> = unassociatedCache as? DataCache<PagedType> else {
|
||||
return unassociatedCache
|
||||
|
|
|
@ -103,6 +103,7 @@ public final class JobRunner {
|
|||
internal static var executorMap: Atomic<[Job.Variant: JobExecutor.Type]> = Atomic([:])
|
||||
fileprivate static var perSessionJobsCompleted: Atomic<Set<Int64>> = Atomic([])
|
||||
private static var hasCompletedInitialBecomeActive: Atomic<Bool> = Atomic(false)
|
||||
private static var shutdownBackgroundTask: Atomic<OWSBackgroundTask?> = Atomic(nil)
|
||||
|
||||
// MARK: - Configuration
|
||||
|
||||
|
@ -222,6 +223,14 @@ public final class JobRunner {
|
|||
}
|
||||
|
||||
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 jobsToRun: [Job] = Storage.shared
|
||||
.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
|
||||
/// 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)
|
||||
public static func stopAndClearPendingJobs() {
|
||||
queues.wrappedValue.values.forEach { queue in
|
||||
queue.stopAndClearPendingJobs()
|
||||
public static func stopAndClearPendingJobs(
|
||||
exceptForVariant: Job.Variant? = nil,
|
||||
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
|
||||
fileprivate let jobVariants: [Job.Variant]
|
||||
|
||||
private let onQueueDrained: (() -> ())?
|
||||
fileprivate var onQueueDrained: (() -> ())?
|
||||
|
||||
private lazy var internalQueue: DispatchQueue = {
|
||||
let result: DispatchQueue = DispatchQueue(
|
||||
|
|
|
@ -15,4 +15,5 @@ FOUNDATION_EXPORT const unsigned char SessionUtilitiesKitVersionString[];
|
|||
#import <SessionUtilitiesKit/OWSMath.h>
|
||||
#import <SessionUtilitiesKit/UIImage+OWS.h>
|
||||
#import <SessionUtilitiesKit/UIView+OWS.h>
|
||||
#import <SessionUtilitiesKit/OWSBackgroundTask.h>
|
||||
|
||||
|
|
|
@ -10,6 +10,7 @@ typedef NS_ENUM(NSUInteger, BackgroundTaskState) {
|
|||
BackgroundTaskState_Success,
|
||||
BackgroundTaskState_CouldNotStart,
|
||||
BackgroundTaskState_Expired,
|
||||
BackgroundTaskState_Cancelled,
|
||||
};
|
||||
|
||||
typedef void (^BackgroundTaskCompletionBlock)(BackgroundTaskState backgroundTaskState);
|
||||
|
@ -57,6 +58,8 @@ typedef void (^BackgroundTaskCompletionBlock)(BackgroundTaskState backgroundTask
|
|||
+ (OWSBackgroundTask *)backgroundTaskWithLabel:(NSString *)label
|
||||
completionBlock:(BackgroundTaskCompletionBlock)completionBlock;
|
||||
|
||||
- (void)cancel;
|
||||
|
||||
@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
|
||||
{
|
||||
// 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
|
||||
}()
|
||||
|
||||
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 = {
|
||||
let result: UIImageView = UIImageView()
|
||||
result.translatesAutoresizingMaskIntoConstraints = false
|
||||
result.contentMode = .scaleAspectFill
|
||||
result.tintColor = Colors.text
|
||||
result.isHidden = true
|
||||
|
||||
return result
|
||||
|
@ -107,11 +120,17 @@ public final class ProfilePictureView: UIView {
|
|||
imageContainerView.addSubview(animatedImageView)
|
||||
additionalImageContainerView.addSubview(additionalImageView)
|
||||
additionalImageContainerView.addSubview(additionalAnimatedImageView)
|
||||
additionalImageContainerView.addSubview(additionalProfilePlaceholderImageView)
|
||||
|
||||
imageView.pin(to: imageContainerView)
|
||||
animatedImageView.pin(to: imageContainerView)
|
||||
additionalImageView.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)
|
||||
|
@ -172,6 +191,7 @@ public final class ProfilePictureView: UIView {
|
|||
additionalAnimatedImageView.image = nil
|
||||
additionalImageView.isHidden = true
|
||||
additionalAnimatedImageView.isHidden = true
|
||||
additionalProfilePlaceholderImageView.isHidden = true
|
||||
return
|
||||
}
|
||||
guard !publicKey.isEmpty || openGroupProfilePictureData != nil else { return }
|
||||
|
@ -240,6 +260,12 @@ public final class ProfilePictureView: UIView {
|
|||
additionalAnimatedImageView.image = animatedImage
|
||||
additionalImageView.isHidden = (animatedImage != nil)
|
||||
additionalAnimatedImageView.isHidden = (animatedImage == nil)
|
||||
additionalProfilePlaceholderImageView.isHidden = true
|
||||
}
|
||||
else {
|
||||
additionalImageView.isHidden = true
|
||||
additionalAnimatedImageView.isHidden = true
|
||||
additionalProfilePlaceholderImageView.isHidden = false
|
||||
}
|
||||
|
||||
default:
|
||||
|
@ -251,6 +277,7 @@ public final class ProfilePictureView: UIView {
|
|||
additionalImageView.isHidden = true
|
||||
additionalAnimatedImageView.image = nil
|
||||
additionalAnimatedImageView.isHidden = true
|
||||
additionalProfilePlaceholderImageView.isHidden = true
|
||||
}
|
||||
|
||||
// Set the image
|
||||
|
|
|
@ -7,7 +7,7 @@ import SessionMessagingKit
|
|||
public class NoopNotificationsManager: NotificationsProtocol {
|
||||
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("")
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue