Cleared out some of the legacy serialisation logic, further UI binding

Refactored the SignalApp class to Swift
Fixed a horizontal alignment issue in the ConversationTitleView
Fixed an issue where expiration timer update messages weren't migrated or rendering correctly
Fixed an issue where expiring messages weren't migrated correctly
Fixed an issue where closed groups which had been left were causing migration failures (due to data incorrectly being assumed to be required)
Shifted the Legacy Attachment types into the 'SMKLegacy' namespace
Moved all of the NSCoding logic for the TSMessage
This commit is contained in:
Morgan Pretty 2022-05-03 17:14:56 +10:00
parent 3baeb981d9
commit 32304ae5dd
73 changed files with 2696 additions and 1617 deletions

View File

@ -21,7 +21,6 @@
3430FE181F7751D4000EC51B /* GiphyAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3430FE171F7751D4000EC51B /* GiphyAPI.swift */; };
34330AA31E79686200DF2FB9 /* OWSProgressView.m in Sources */ = {isa = PBXBuildFile; fileRef = 34330AA21E79686200DF2FB9 /* OWSProgressView.m */; };
34386A54207D271D009F5D9C /* NeverClearView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34386A53207D271C009F5D9C /* NeverClearView.swift */; };
346129991FD1E4DA00532771 /* SignalApp.m in Sources */ = {isa = PBXBuildFile; fileRef = 346129971FD1E4D900532771 /* SignalApp.m */; };
34661FB820C1C0D60056EDD6 /* message_sent.aiff in Resources */ = {isa = PBXBuildFile; fileRef = 34661FB720C1C0D60056EDD6 /* message_sent.aiff */; };
346B66311F4E29B200E5122F /* CropScaleImageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 346B66301F4E29B200E5122F /* CropScaleImageViewController.swift */; };
3478504C1FD7496D007B8332 /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = B66DBF4919D5BBC8006EA940 /* Images.xcassets */; };
@ -157,7 +156,6 @@
B821494F25D4E163009C0F2A /* BodyTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B821494E25D4E163009C0F2A /* BodyTextView.swift */; };
B82149B825D60393009C0F2A /* BlockedModal.swift in Sources */ = {isa = PBXBuildFile; fileRef = B82149B725D60393009C0F2A /* BlockedModal.swift */; };
B82149C125D605C6009C0F2A /* InfoBanner.swift in Sources */ = {isa = PBXBuildFile; fileRef = B82149C025D605C6009C0F2A /* InfoBanner.swift */; };
B8214A2B25D63EB9009C0F2A /* MessagesTableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8214A2A25D63EB9009C0F2A /* MessagesTableView.swift */; };
B8269D2925C7A4B400488AB4 /* InputView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8269D2825C7A4B400488AB4 /* InputView.swift */; };
B8269D3325C7A8C600488AB4 /* InputViewButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8269D3225C7A8C600488AB4 /* InputViewButton.swift */; };
B8269D3D25C7B34D00488AB4 /* InputTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8269D3C25C7B34D00488AB4 /* InputTextView.swift */; };
@ -785,11 +783,12 @@
FD6A7A6B2818C17C00035AC1 /* UpdateProfilePictureJob.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6A7A6A2818C17C00035AC1 /* UpdateProfilePictureJob.swift */; };
FD6A7A6D2818C61500035AC1 /* _002_SetupStandardJobs.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6A7A6C2818C61500035AC1 /* _002_SetupStandardJobs.swift */; };
FD705A8C278CDB5600F16121 /* SAEScreenLockViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD705A8B278CDB5600F16121 /* SAEScreenLockViewController.swift */; };
FD705A8E278CE29800F16121 /* String+Localization.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD705A8D278CE29800F16121 /* String+Localization.swift */; };
FD705A8E278CE29800F16121 /* String+Localized.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD705A8D278CE29800F16121 /* String+Localized.swift */; };
FD705A90278CEBBC00F16121 /* ShareAppExtensionContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD705A8F278CEBBC00F16121 /* ShareAppExtensionContext.swift */; };
FD705A92278D051200F16121 /* ReusableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD705A91278D051200F16121 /* ReusableView.swift */; };
FD705A94278D052B00F16121 /* UITableView+ReusableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD705A93278D052B00F16121 /* UITableView+ReusableView.swift */; };
FD705A98278E9F4D00F16121 /* UIColor+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD705A97278E9F4D00F16121 /* UIColor+Extensions.swift */; };
FD7162DB281B6C440060647B /* TypedTableAlias.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD7162DA281B6C440060647B /* TypedTableAlias.swift */; };
FD859F0027C4691300510D0C /* MockDataGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD859EFF27C4691300510D0C /* MockDataGenerator.swift */; };
FD88BAD927A7439C00BBC442 /* MessageRequestsCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD88BAD827A7439C00BBC442 /* MessageRequestsCell.swift */; };
FD88BADB27A750F200BBC442 /* MessageRequestsMigration.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD88BADA27A750F200BBC442 /* MessageRequestsMigration.swift */; };
@ -820,6 +819,11 @@
FDF0B75A2807F3A3004C14C5 /* MessageSenderError.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF0B7592807F3A3004C14C5 /* MessageSenderError.swift */; };
FDF0B75C2807F41D004C14C5 /* MessageSender+Convenience.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF0B75B2807F41D004C14C5 /* MessageSender+Convenience.swift */; };
FDF0B75E280AAF35004C14C5 /* Preferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF0B75D280AAF35004C14C5 /* Preferences.swift */; };
FDF222072818CECF000A4995 /* ConversationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF222062818CECF000A4995 /* ConversationViewModel.swift */; };
FDF222092818D2B0000A4995 /* NSAttributedString+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF222082818D2B0000A4995 /* NSAttributedString+Utilities.swift */; };
FDF2220B2818F38D000A4995 /* SessionApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF2220A2818F38D000A4995 /* SessionApp.swift */; };
FDF2220F281B55E6000A4995 /* QueryInterfaceRequest+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF2220E281B55E6000A4995 /* QueryInterfaceRequest+Utilities.swift */; };
FDF22211281B5E0B000A4995 /* TableRecord+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF22210281B5E0B000A4995 /* TableRecord+Utilities.swift */; };
FDFD645827EC1F4000808CA1 /* Atomic.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDFD645727EC1F4000808CA1 /* Atomic.swift */; };
/* End PBXBuildFile section */
@ -1010,8 +1014,6 @@
34386A53207D271C009F5D9C /* NeverClearView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NeverClearView.swift; sourceTree = "<group>"; };
34480B371FD092A900BC14EF /* SignalShareExtension-Bridging-Header.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "SignalShareExtension-Bridging-Header.h"; sourceTree = "<group>"; };
34480B381FD092E300BC14EF /* SessionShareExtension-Prefix.pch */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "SessionShareExtension-Prefix.pch"; sourceTree = "<group>"; };
346129971FD1E4D900532771 /* SignalApp.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SignalApp.m; sourceTree = "<group>"; };
346129981FD1E4DA00532771 /* SignalApp.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SignalApp.h; sourceTree = "<group>"; };
34661FB720C1C0D60056EDD6 /* message_sent.aiff */ = {isa = PBXFileReference; lastKnownFileType = audio.aiff; name = message_sent.aiff; path = Session/Meta/AudioFiles/message_sent.aiff; sourceTree = SOURCE_ROOT; };
346B66301F4E29B200E5122F /* CropScaleImageViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CropScaleImageViewController.swift; sourceTree = "<group>"; };
3488F9352191CC4000E524CC /* MediaView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MediaView.swift; sourceTree = "<group>"; };
@ -1206,7 +1208,6 @@
B821494E25D4E163009C0F2A /* BodyTextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BodyTextView.swift; sourceTree = "<group>"; };
B82149B725D60393009C0F2A /* BlockedModal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlockedModal.swift; sourceTree = "<group>"; };
B82149C025D605C6009C0F2A /* InfoBanner.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InfoBanner.swift; sourceTree = "<group>"; };
B8214A2A25D63EB9009C0F2A /* MessagesTableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessagesTableView.swift; sourceTree = "<group>"; };
B8269D2825C7A4B400488AB4 /* InputView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InputView.swift; sourceTree = "<group>"; };
B8269D3225C7A8C600488AB4 /* InputViewButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InputViewButton.swift; sourceTree = "<group>"; };
B8269D3C25C7B34D00488AB4 /* InputTextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InputTextView.swift; sourceTree = "<group>"; };
@ -1288,7 +1289,6 @@
B8D0A24F25E3678700C1835E /* LinkDeviceVC.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LinkDeviceVC.swift; sourceTree = "<group>"; };
B8D0A25825E367AC00C1835E /* Notification+MessageReceiver.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = "Notification+MessageReceiver.swift"; path = "SessionMessagingKit/Sending & Receiving/Notification+MessageReceiver.swift"; sourceTree = SOURCE_ROOT; };
B8D0A26825E4A2C200C1835E /* Onboarding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Onboarding.swift; sourceTree = "<group>"; };
B8D84E9325DF72AF005A043E /* ConversationViewAction.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ConversationViewAction.h; sourceTree = "<group>"; };
B8D84EA225DF745A005A043E /* LinkPreviewState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LinkPreviewState.swift; sourceTree = "<group>"; };
B8D84ECE25E3108A005A043E /* ExpandingAttachmentsButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExpandingAttachmentsButton.swift; sourceTree = "<group>"; };
B8D8F1372566120F0092EF10 /* Storage+ClosedGroups.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Storage+ClosedGroups.swift"; sourceTree = "<group>"; };
@ -1855,11 +1855,12 @@
FD6A7A6A2818C17C00035AC1 /* UpdateProfilePictureJob.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateProfilePictureJob.swift; sourceTree = "<group>"; };
FD6A7A6C2818C61500035AC1 /* _002_SetupStandardJobs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _002_SetupStandardJobs.swift; sourceTree = "<group>"; };
FD705A8B278CDB5600F16121 /* SAEScreenLockViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SAEScreenLockViewController.swift; sourceTree = "<group>"; };
FD705A8D278CE29800F16121 /* String+Localization.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+Localization.swift"; sourceTree = "<group>"; };
FD705A8D278CE29800F16121 /* String+Localized.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+Localized.swift"; sourceTree = "<group>"; };
FD705A8F278CEBBC00F16121 /* ShareAppExtensionContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareAppExtensionContext.swift; sourceTree = "<group>"; };
FD705A91278D051200F16121 /* ReusableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReusableView.swift; sourceTree = "<group>"; };
FD705A93278D052B00F16121 /* UITableView+ReusableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UITableView+ReusableView.swift"; sourceTree = "<group>"; };
FD705A97278E9F4D00F16121 /* UIColor+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIColor+Extensions.swift"; sourceTree = "<group>"; };
FD7162DA281B6C440060647B /* TypedTableAlias.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TypedTableAlias.swift; sourceTree = "<group>"; };
FD859EFF27C4691300510D0C /* MockDataGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockDataGenerator.swift; sourceTree = "<group>"; };
FD88BAD827A7439C00BBC442 /* MessageRequestsCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageRequestsCell.swift; sourceTree = "<group>"; };
FD88BADA27A750F200BBC442 /* MessageRequestsMigration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageRequestsMigration.swift; sourceTree = "<group>"; };
@ -1888,6 +1889,11 @@
FDF0B7592807F3A3004C14C5 /* MessageSenderError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageSenderError.swift; sourceTree = "<group>"; };
FDF0B75B2807F41D004C14C5 /* MessageSender+Convenience.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "MessageSender+Convenience.swift"; sourceTree = "<group>"; };
FDF0B75D280AAF35004C14C5 /* Preferences.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Preferences.swift; sourceTree = "<group>"; };
FDF222062818CECF000A4995 /* ConversationViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConversationViewModel.swift; sourceTree = "<group>"; };
FDF222082818D2B0000A4995 /* NSAttributedString+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSAttributedString+Utilities.swift"; sourceTree = "<group>"; };
FDF2220A2818F38D000A4995 /* SessionApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionApp.swift; sourceTree = "<group>"; };
FDF2220E281B55E6000A4995 /* QueryInterfaceRequest+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "QueryInterfaceRequest+Utilities.swift"; sourceTree = "<group>"; };
FDF22210281B5E0B000A4995 /* TableRecord+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TableRecord+Utilities.swift"; sourceTree = "<group>"; };
FDFD645727EC1F4000808CA1 /* Atomic.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Atomic.swift; sourceTree = "<group>"; };
FEDBAE1B98C49BBE8C87F575 /* Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionUtilitiesKit.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionUtilitiesKit.debug.xcconfig"; path = "Pods/Target Support Files/Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionUtilitiesKit/Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionUtilitiesKit.debug.xcconfig"; sourceTree = "<group>"; };
FF9BA33D021B115B1F5B4E46 /* Pods-SessionMessagingKit.app store release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SessionMessagingKit.app store release.xcconfig"; path = "Pods/Target Support Files/Pods-SessionMessagingKit/Pods-SessionMessagingKit.app store release.xcconfig"; sourceTree = "<group>"; };
@ -2267,7 +2273,6 @@
B82149B725D60393009C0F2A /* BlockedModal.swift */,
C374EEE125DA26740073A857 /* LinkPreviewModal.swift */,
B82149C025D605C6009C0F2A /* InfoBanner.swift */,
B8214A2A25D63EB9009C0F2A /* MessagesTableView.swift */,
C374EEEA25DA3CA70073A857 /* ConversationTitleView.swift */,
B848A4C4269EAAA200617031 /* UserDetailsSheet.swift */,
);
@ -2277,21 +2282,21 @@
B835246C25C38AA20089A44F /* Conversations */ = {
isa = PBXGroup;
children = (
B835246D25C38ABF0089A44F /* ConversationVC.swift */,
B8569AC225CB5D2900DBA3DB /* ConversationVC+Interaction.swift */,
3496744E2076ACCE00080B5F /* LongTextViewController.swift */,
4CC613352227A00400E21A3A /* ConversationSearch.swift */,
B8D84E9325DF72AF005A043E /* ConversationViewAction.h */,
34D1F06F1F8678AA0066283D /* ConversationViewItem.h */,
34D1F0701F8678AA0066283D /* ConversationViewItem.m */,
341341ED2187467900192D59 /* ConversationViewModel.h */,
341341EE2187467900192D59 /* ConversationViewModel.m */,
34ABC0E321DD20C500ED9469 /* ConversationMessageMapping.swift */,
B887C38125C7C79700E11DAE /* Input View */,
B835247725C38D190089A44F /* Message Cells */,
C328252E25CA54F70062D0A7 /* Context Menu */,
B821493625D4D6A7009C0F2A /* Views & Modals */,
C302094625DCDFD3001F572D /* Settings */,
FDF222062818CECF000A4995 /* ConversationViewModel.swift */,
B835246D25C38ABF0089A44F /* ConversationVC.swift */,
B8569AC225CB5D2900DBA3DB /* ConversationVC+Interaction.swift */,
3496744E2076ACCE00080B5F /* LongTextViewController.swift */,
4CC613352227A00400E21A3A /* ConversationSearch.swift */,
34D1F06F1F8678AA0066283D /* ConversationViewItem.h */,
34D1F0701F8678AA0066283D /* ConversationViewItem.m */,
341341ED2187467900192D59 /* ConversationViewModel.h */,
341341EE2187467900192D59 /* ConversationViewModel.m */,
34ABC0E321DD20C500ED9469 /* ConversationMessageMapping.swift */,
);
path = Conversations;
sourceTree = "<group>";
@ -2433,7 +2438,7 @@
C33FDB6B255A580F00E217F9 /* SNUserDefaults.swift */,
C33FDB3F255A580C00E217F9 /* String+SSK.swift */,
C3C2AC2D2553CBEB00C340D1 /* String+Trimming.swift */,
FD705A8D278CE29800F16121 /* String+Localization.swift */,
FD705A8D278CE29800F16121 /* String+Localized.swift */,
C38EF237255B6D65007E1867 /* UIDevice+featureSupport.swift */,
C35D0DB425AE5F1200B6BF49 /* UIEdgeInsets.swift */,
FD705A91278D051200F16121 /* ReusableView.swift */,
@ -3507,8 +3512,7 @@
C3CA3AA0255CDA7000F4C6D4 /* Mnemonic */,
B67EBF5C19194AC60084CCFD /* Settings.bundle */,
B657DDC91911A40500F45B0C /* Signal.entitlements */,
346129981FD1E4DA00532771 /* SignalApp.h */,
346129971FD1E4D900532771 /* SignalApp.m */,
FDF2220A2818F38D000A4995 /* SessionApp.swift */,
45B201741DAECBFD00C461E0 /* Signal-Bridging-Header.h */,
D221A095169C9E5E00537ABF /* Session-Info.plist */,
D221A09B169C9E5E00537ABF /* Session-Prefix.pch */,
@ -3630,11 +3634,12 @@
FD09796527F6B0A800936362 /* Utilities */ = {
isa = PBXGroup;
children = (
FDA8EB0F280F8238002B68E5 /* Codable+Utilities.swift */,
FD09796A27F6C67500936362 /* Failable.swift */,
FD09797127FAA2F500936362 /* Optional+Utilities.swift */,
FD09797C27FBDB2000936362 /* Notification+Utilities.swift */,
FDF222082818D2B0000A4995 /* NSAttributedString+Utilities.swift */,
C3E7134E251C867C009649BB /* Sodium+Conversion.swift */,
FDA8EB0F280F8238002B68E5 /* Codable+Utilities.swift */,
);
path = Utilities;
sourceTree = "<group>";
@ -3734,6 +3739,7 @@
FD17D7B727F51ECA00122BE0 /* Migration.swift */,
FD17D7B927F51F2100122BE0 /* TargetMigrations.swift */,
FD17D7C027F5200100122BE0 /* TypedTableDefinition.swift */,
FD7162DA281B6C440060647B /* TypedTableAlias.swift */,
);
path = Types;
sourceTree = "<group>";
@ -3746,6 +3752,8 @@
FD17D7C627F5207C00122BE0 /* DatabaseMigrator+Utilities.swift */,
FD17D7BC27F51F6900122BE0 /* GRDB+Notifications.swift */,
FDF0B74C280664E9004C14C5 /* PersistableRecord+Utilities.swift */,
FDF22210281B5E0B000A4995 /* TableRecord+Utilities.swift */,
FDF2220E281B55E6000A4995 /* QueryInterfaceRequest+Utilities.swift */,
);
path = Utilities;
sourceTree = "<group>";
@ -4889,6 +4897,7 @@
B8AE75A425A6C6A6001A84D2 /* Data+Trimming.swift in Sources */,
FD17D7C527F5206300122BE0 /* ColumnDefinition+Utilities.swift in Sources */,
B8856DE6256F15F2001CE70E /* String+SSK.swift in Sources */,
FDF2220F281B55E6000A4995 /* QueryInterfaceRequest+Utilities.swift in Sources */,
C3471ED42555386B00297E91 /* AESGCM.swift in Sources */,
FD17D7B827F51ECA00122BE0 /* Migration.swift in Sources */,
C32C5DDB256DD9FF003C73A2 /* ContentProxy.swift in Sources */,
@ -4903,9 +4912,11 @@
C3BBE0A72554D4DE0050F1E3 /* Promise+Retrying.swift in Sources */,
B8856D7B256F14F4001CE70E /* UIView+OWS.m in Sources */,
FD17D7B027F4225C00122BE0 /* Set+Utilities.swift in Sources */,
FDF22211281B5E0B000A4995 /* TableRecord+Utilities.swift in Sources */,
B88FA7FB26114EA70049422F /* Hex.swift in Sources */,
C35D0DB525AE5F1200B6BF49 /* UIEdgeInsets.swift in Sources */,
C3D9E4F4256778AF0040E4F3 /* NSData+Image.m in Sources */,
FDF222092818D2B0000A4995 /* NSAttributedString+Utilities.swift in Sources */,
C32C5E0C256DDAFA003C73A2 /* NSRegularExpression+SSK.swift in Sources */,
C3BBE0A92554D4DE0050F1E3 /* HTTP.swift in Sources */,
B8856D23256F116B001CE70E /* Weak.swift in Sources */,
@ -4919,9 +4930,10 @@
C300A60D2554B31900555489 /* Logging.swift in Sources */,
B8FF8EA625C11FEF004D1F22 /* IPv4.swift in Sources */,
C3D9E35525675EE10040E4F3 /* MIMETypeUtil.m in Sources */,
FD705A8E278CE29800F16121 /* String+Localization.swift in Sources */,
FD705A8E278CE29800F16121 /* String+Localized.swift in Sources */,
FD9004162818B46700ABAAF6 /* JobRunnerError.swift in Sources */,
FD09797227FAA2F500936362 /* Optional+Utilities.swift in Sources */,
FD7162DB281B6C440060647B /* TypedTableAlias.swift in Sources */,
C3A7219A2558C1660043A11F /* AnyPromise+Conversion.swift in Sources */,
C300A6322554B6D100555489 /* NSDate+Timestamp.mm in Sources */,
FD17D7E727F6A16700122BE0 /* _003_YDBToGRDBMigration.swift in Sources */,
@ -5103,6 +5115,7 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
FDF2220B2818F38D000A4995 /* SessionApp.swift in Sources */,
B8041AA725C90927003C2166 /* TypingIndicatorCell.swift in Sources */,
B8CCF63723961D6D0091D419 /* NewDMVC.swift in Sources */,
452EC6DF205E9E30000E787C /* MediaGalleryViewController.swift in Sources */,
@ -5156,7 +5169,6 @@
4CC613362227A00400E21A3A /* ConversationSearch.swift in Sources */,
B82149B825D60393009C0F2A /* BlockedModal.swift in Sources */,
B82B408C239A068800A248E7 /* RegisterVC.swift in Sources */,
346129991FD1E4DA00532771 /* SignalApp.m in Sources */,
34BECE301F7ABCF800D7438D /* GifPickerLayout.swift in Sources */,
C331FFFE2558FF3B00070591 /* ConversationCell.swift in Sources */,
FD659AC027A7649600F12C02 /* MessageRequestsViewController.swift in Sources */,
@ -5167,7 +5179,6 @@
FD88BAD927A7439C00BBC442 /* MessageRequestsCell.swift in Sources */,
45A6DAD61EBBF85500893231 /* ReminderView.swift in Sources */,
B82B408E239DC00D00A248E7 /* DisplayNameVC.swift in Sources */,
B8214A2B25D63EB9009C0F2A /* MessagesTableView.swift in Sources */,
B835246E25C38ABF0089A44F /* ConversationVC.swift in Sources */,
B8AF4BB426A5204600583500 /* SendSeedModal.swift in Sources */,
B821494625D4D6FF009C0F2A /* URLModal.swift in Sources */,
@ -5203,6 +5214,7 @@
B8CCF6432397711F0091D419 /* SettingsVC.swift in Sources */,
C354E75A23FE2A7600CE22E3 /* BaseVC.swift in Sources */,
45C0DC1B1E68FE9000E04C47 /* UIApplication+OWS.swift in Sources */,
FDF222072818CECF000A4995 /* ConversationViewModel.swift in Sources */,
4539B5861F79348F007141FF /* PushRegistrationManager.swift in Sources */,
B8041A9525C8FA1D003C2166 /* MediaLoaderView.swift in Sources */,
45F32C232057297A00A300D5 /* MediaPageViewController.swift in Sources */,

View File

@ -1,8 +0,0 @@
@import Foundation;
typedef NS_ENUM(NSUInteger, ConversationViewAction) {
ConversationViewActionNone,
ConversationViewActionCompose,
ConversationViewActionAudioCall,
ConversationViewActionVideoCall,
};

View File

@ -0,0 +1,3 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import Foundation

View File

@ -1,23 +0,0 @@
final class MessagesTableView : UITableView {
override init(frame: CGRect, style: UITableView.Style) {
super.init(frame: frame, style: style)
initialize()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
initialize()
}
private func initialize() {
register(VisibleMessageCell.self, forCellReuseIdentifier: VisibleMessageCell.identifier)
register(InfoMessageCell.self, forCellReuseIdentifier: InfoMessageCell.identifier)
register(TypingIndicatorCell.self, forCellReuseIdentifier: TypingIndicatorCell.identifier)
separatorStyle = .none
backgroundColor = .clear
showsVerticalScrollIndicator = false
contentInsetAdjustmentBehavior = .never
keyboardDismissMode = .interactive
}
}

View File

@ -100,7 +100,8 @@ final class HomeVC: BaseVC, UITableViewDataSource, UITableViewDelegate, NewConve
_ = CurrentAppContext().isRTL
// Preparation
SignalApp.shared().homeViewController = self
SessionApp.homeViewController.mutate { $0 = self }
// Gradient & nav bar
setUpGradientBackground()
if navigationController?.navigationBar != nil {

View File

@ -306,7 +306,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
AppReadiness.runNowOrWhenAppDidBecomeReady {
guard Identity.userExists() else { return }
SignalApp.shared().homeViewController?.createNewDM()
SessionApp.homeViewController.wrappedValue?.createNewDM()
completionHandler(true)
}
}
@ -373,7 +373,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
stopPollers()
let wasUnlinked: Bool = UserDefaults.standard[.wasUnlinked]
SignalApp.resetAppData {
SessionApp.resetAppData {
// Resetting the data clears the old user defaults. We need to restore the unlink default.
UserDefaults.standard[.wasUnlinked] = wasUnlinked
}

View File

@ -0,0 +1,85 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import Foundation
import SessionUtilitiesKit
import SessionMessagingKit
public struct SessionApp {
static let homeViewController: Atomic<HomeVC?> = Atomic(nil)
// MARK: - View Convenience Methods
public static func presentConversation(for recipientId: String, action: ConversationViewModel.Action = .none, animated: Bool) {
let maybeThread: SessionThread? = GRDBStorage.shared.write { db in
SessionThread.fetchOrCreate(db, id: recipientId, variant: .contact)
}
guard let thread: SessionThread = maybeThread else { return }
self.presentConversation(for: thread, action: action, animated: animated)
}
public static func presentConversation(for threadId: String, animated: Bool) {
guard let thread: SessionThread = GRDBStorage.shared.read({ db in try SessionThread.fetchOne(db, id: threadId) }) else {
SNLog("Unable to find thread with id:\(threadId)")
return
}
self.presentConversation(for: thread, animated: animated)
}
public static func presentConversation(
for thread: SessionThread,
action: ConversationViewModel.Action = .none,
focusInteractionId: Int64? = nil,
animated: Bool
) {
guard Thread.isMainThread else {
DispatchQueue.main.async {
self.presentConversation(
for: thread,
action: action,
focusInteractionId: focusInteractionId,
animated: animated
)
}
return
}
homeViewController.wrappedValue?.show(
thread,
with: action,
highlightedInteractionId: focusInteractionId, // TODO: Confirm this
animated: animated
)
}
// MARK: - Functions
public static func resetAppData(onReset: (() -> ())? = nil) {
// This _should_ be wiped out below.
Logger.error("")
DDLog.flushLog()
OWSStorage.resetAllStorage()
OWSUserProfile.resetProfileStorage()
Environment.shared.preferences.clear()
AppEnvironment.shared.notificationPresenter.clearAllNotifications()
onReset?()
exit(0)
}
public static func showHomeView() {
guard Thread.isMainThread else {
DispatchQueue.main.async {
self.showHomeView()
}
return
}
let homeViewController: HomeVC = HomeVC()
let navController: UINavigationController = UINavigationController(rootViewController: homeViewController)
(UIApplication.shared.delegate as? AppDelegate)?.window?.rootViewController = navController
}
}

View File

@ -1,60 +0,0 @@
//
// Copyright (c) 2019 Open Whisper Systems. All rights reserved.
//
#import "ConversationViewAction.h"
NS_ASSUME_NONNULL_BEGIN
@class AccountManager;
@class CallService;
@class CallUIAdapter;
@class HomeVC;
@class OWSMessageFetcherJob;
@class OWSNavigationController;
@class OutboundCallInitiator;
@class TSThread;
@interface SignalApp : NSObject
@property (nonatomic, nullable, weak) HomeVC *homeViewController;
@property (nonatomic, nullable, weak) OWSNavigationController *signUpFlowNavigationController;
- (instancetype)init NS_UNAVAILABLE;
+ (instancetype)sharedApp;
- (void)setup;
#pragma mark - Conversation Presentation
- (void)presentConversationForRecipientId:(NSString *)recipientId animated:(BOOL)isAnimated;
- (void)presentConversationForRecipientId:(NSString *)recipientId
action:(ConversationViewAction)action
animated:(BOOL)isAnimated;
- (void)presentConversationForThreadId:(NSString *)threadId animated:(BOOL)isAnimated;
- (void)presentConversationForThread:(TSThread *)thread animated:(BOOL)isAnimated;
- (void)presentConversationForThread:(TSThread *)thread action:(ConversationViewAction)action animated:(BOOL)isAnimated;
- (void)presentConversationForThread:(TSThread *)thread
action:(ConversationViewAction)action
focusMessageId:(nullable NSString *)focusMessageId
animated:(BOOL)isAnimated;
- (void)presentConversationAndScrollToFirstUnreadMessageForThreadId:(NSString *)threadId animated:(BOOL)isAnimated;
#pragma mark - Methods
+ (void)resetAppData;
+ (void)resetAppData:(void (^__nullable)(void))onReset;
- (void)showHomeView;
@end
NS_ASSUME_NONNULL_END

View File

@ -1,167 +0,0 @@
//
// Copyright (c) 2019 Open Whisper Systems. All rights reserved.
//
#import "SignalApp.h"
#import "Session-Swift.h"
#import <SignalCoreKit/Threading.h>
#import <SessionMessagingKit/Environment.h>
#import <SessionMessagingKit/OWSPrimaryStorage.h>
#import <SessionMessagingKit/TSContactThread.h>
#import <SessionMessagingKit/TSGroupThread.h>
NS_ASSUME_NONNULL_BEGIN
@implementation SignalApp
+ (instancetype)sharedApp
{
static SignalApp *sharedApp = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
sharedApp = [[self alloc] initDefault];
});
return sharedApp;
}
- (instancetype)initDefault
{
self = [super init];
if (!self) {
return self;
}
OWSSingletonAssert();
return self;
}
#pragma mark - Singletons
- (void)setup {
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(didChangeCallLoggingPreference:)
name:OWSPreferencesCallLoggingDidChangeNotification
object:nil];
}
#pragma mark - View Convenience Methods
- (void)presentConversationForRecipientId:(NSString *)recipientId animated:(BOOL)isAnimated
{
[self presentConversationForRecipientId:recipientId action:ConversationViewActionNone animated:(BOOL)isAnimated];
}
- (void)presentConversationForRecipientId:(NSString *)recipientId
action:(ConversationViewAction)action
animated:(BOOL)isAnimated
{
__block TSThread *thread = nil;
[LKStorage writeSyncWithBlock:^(YapDatabaseReadWriteTransaction *_Nonnull transaction) {
thread = [TSContactThread getOrCreateThreadWithContactSessionID:recipientId transaction:transaction];
}];
[self presentConversationForThread:thread action:action animated:(BOOL)isAnimated];
}
- (void)presentConversationForThreadId:(NSString *)threadId animated:(BOOL)isAnimated
{
OWSAssertDebug(threadId.length > 0);
TSThread *thread = [TSThread fetchObjectWithUniqueID:threadId];
if (thread == nil) {
OWSFailDebug(@"unable to find thread with id: %@", threadId);
return;
}
[self presentConversationForThread:thread animated:isAnimated];
}
- (void)presentConversationForThread:(TSThread *)thread animated:(BOOL)isAnimated
{
[self presentConversationForThread:thread action:ConversationViewActionNone animated:isAnimated];
}
- (void)presentConversationForThread:(TSThread *)thread action:(ConversationViewAction)action animated:(BOOL)isAnimated
{
[self presentConversationForThread:thread action:action focusMessageId:nil animated:isAnimated];
}
- (void)presentConversationForThread:(TSThread *)thread
action:(ConversationViewAction)action
focusMessageId:(nullable NSString *)focusMessageId
animated:(BOOL)isAnimated
{
OWSAssertIsOnMainThread();
OWSLogInfo(@"");
if (!thread) {
OWSFailDebug(@"Can't present nil thread.");
return;
}
DispatchMainThreadSafe(^{
[self.homeViewController show:thread with:action highlightedMessageID:focusMessageId animated:isAnimated];
});
}
- (void)presentConversationAndScrollToFirstUnreadMessageForThreadId:(NSString *)threadId animated:(BOOL)isAnimated
{
OWSAssertIsOnMainThread();
OWSAssertDebug(threadId.length > 0);
OWSLogInfo(@"");
TSThread *thread = [TSThread fetchObjectWithUniqueID:threadId];
if (thread == nil) {
OWSFailDebug(@"unable to find thread with id: %@", threadId);
return;
}
DispatchMainThreadSafe(^{
[self.homeViewController show:thread with:ConversationViewActionNone highlightedMessageID:nil animated:isAnimated];
});
}
- (void)didChangeCallLoggingPreference:(NSNotification *)notitication
{
// [AppEnvironment.shared.callService createCallUIAdapter];
}
#pragma mark - Methods
+ (void)resetAppData
{
[self resetAppData:nil];
}
+ (void)resetAppData:(void (^__nullable)(void))onReset {
// This _should_ be wiped out below.
OWSLogError(@"");
[DDLog flushLog];
[OWSStorage resetAllStorage];
[OWSUserProfile resetProfileStorage];
[Environment.shared.preferences clear];
[AppEnvironment.shared.notificationPresenter clearAllNotifications];
if (onReset != nil) { onReset(); }
exit(0);
}
- (void)showHomeView
{
HomeVC *homeView = [HomeVC new];
UINavigationController *navigationController = [[UINavigationController alloc] initWithRootViewController:homeView];
AppDelegate *appDelegate = (AppDelegate *)[UIApplication sharedApplication].delegate;
appDelegate.window.rootViewController = navigationController;
OWSAssertDebug([navigationController.topViewController isKindOfClass:[HomeVC class]]);
// Clear the signUpFlowNavigationController.
[self setSignUpFlowNavigationController:nil];
}
@end
NS_ASSUME_NONNULL_END

View File

@ -251,7 +251,7 @@ public class NotificationPresenter: NSObject, NotificationsProtocol {
DispatchQueue.main.async {
notificationBody = MentionUtilities.highlightMentions(
in: (notificationBody ?? ""),
threadId: thread.id
threadVariant: thread.variant
)
let sound: Preferences.Sound? = self.requestSound(thread: thread)
@ -354,10 +354,6 @@ class NotificationActionHandler {
// MARK: - Dependencies
var signalApp: SignalApp {
return SignalApp.shared()
}
var notificationPresenter: NotificationPresenter {
return AppEnvironment.shared.notificationPresenter
}
@ -421,12 +417,12 @@ class NotificationActionHandler {
// can be visible to the user immediately upon opening the app, rather than having to watch
// it animate in from the homescreen.
let shouldAnimate = UIApplication.shared.applicationState == .active
signalApp.presentConversationAndScrollToFirstUnreadMessage(forThreadId: threadId, animated: shouldAnimate)
SessionApp.presentConversation(for: threadId, animated: shouldAnimate)
return Promise.value(())
}
func showHomeVC() -> Promise<Void> {
signalApp.showHomeView()
SessionApp.showHomeView()
return Promise.value(())
}

View File

@ -4,7 +4,6 @@
#import "NotificationSettingsOptionsViewController.h"
#import "Session-Swift.h"
#import "SignalApp.h"
#import <SessionMessagingKit/Environment.h>
#import <SignalUtilitiesKit/SignalUtilitiesKit-Swift.h>

View File

@ -13,7 +13,6 @@
#import <SignalUtilitiesKit/SignalUtilitiesKit-Swift.h>
#import <SessionUtilitiesKit/NSString+SSK.h>
#import <SignalUtilitiesKit/ThreadUtil.h>
#import <SessionMessagingKit/OWSReadReceiptManager.h>
#import <SessionMessagingKit/SessionMessagingKit-Swift.h>
#import <SignalUtilitiesKit/SignalUtilitiesKit-Swift.h>
@ -73,7 +72,7 @@ static NSString *const kSealedSenderInfoURL = @"https://signal.org/blog/sealed-s
@"Label for the 'read receipts' setting.")
accessibilityIdentifier:[NSString stringWithFormat:@"settings.privacy.%@", @"read_receipts"]
isOnBlock:^{
return [OWSReadReceiptManager.sharedManager areReadReceiptsEnabled];
return [SSKPreferences areReadReceiptsEnabled];
}
isEnabledBlock:^{
return YES;

View File

@ -139,15 +139,23 @@ final class SettingsVC : BaseVC, AvatarViewHelperDelegate {
setUpGradientBackground()
setUpNavBarStyle()
setNavBarTitle(NSLocalizedString("vc_settings_title", comment: ""))
// Navigation bar buttons
updateNavigationBarButtons()
// Profile picture view
let profile: Profile = Profile.fetchOrCreateCurrentUser()
let profilePictureTapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(showEditProfilePictureUI))
profilePictureView.addGestureRecognizer(profilePictureTapGestureRecognizer)
profilePictureView.publicKey = getUserHexEncodedPublicKey()
profilePictureView.update()
profilePictureView
.update(
publicKey: profile.id,
profile: profile,
threadVariant: .contact
)
// Display name label
displayNameLabel.text = Profile.fetchOrCreateCurrentUser().name
displayNameLabel.text = profile.name
// Display name container
let displayNameContainer = UIView()
displayNameContainer.accessibilityLabel = "Edit display name text field"
@ -160,22 +168,27 @@ final class SettingsVC : BaseVC, AvatarViewHelperDelegate {
displayNameTextField.alpha = 0
let displayNameContainerTapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(showEditDisplayNameUI))
displayNameContainer.addGestureRecognizer(displayNameContainerTapGestureRecognizer)
// Header view
let headerStackView = UIStackView(arrangedSubviews: [ profilePictureView, displayNameContainer ])
headerStackView.axis = .vertical
headerStackView.spacing = Values.smallSpacing
headerStackView.alignment = .center
// Separator
let separator = Separator(title: NSLocalizedString("your_session_id", comment: ""))
// Share button
let shareButton = Button(style: .regular, size: .medium)
shareButton.setTitle(NSLocalizedString("share", comment: ""), for: UIControl.State.normal)
shareButton.addTarget(self, action: #selector(sharePublicKey), for: UIControl.Event.touchUpInside)
// Button container
let buttonContainer = UIStackView(arrangedSubviews: [ copyButton, shareButton ])
buttonContainer.axis = .horizontal
buttonContainer.spacing = Values.mediumSpacing
buttonContainer.distribution = .fillEqually
// Top stack view
let topStackView = UIStackView(arrangedSubviews: [ headerStackView, separator, publicKeyLabel, buttonContainer ])
topStackView.axis = .vertical
@ -183,10 +196,12 @@ final class SettingsVC : BaseVC, AvatarViewHelperDelegate {
topStackView.alignment = .fill
topStackView.layoutMargins = UIEdgeInsets(top: 0, left: Values.largeSpacing, bottom: 0, right: Values.largeSpacing)
topStackView.isLayoutMarginsRelativeArrangement = true
// Setting buttons stack view
getSettingButtons().forEach { settingButtonOrSeparator in
settingButtonsStackView.addArrangedSubview(settingButtonOrSeparator)
}
// Oxen logo
updateLogo()
let logoContainer = UIView()
@ -194,6 +209,7 @@ final class SettingsVC : BaseVC, AvatarViewHelperDelegate {
logoImageView.pin(.top, to: .top, of: logoContainer)
logoContainer.pin(.bottom, to: .bottom, of: logoImageView)
logoImageView.centerXAnchor.constraint(equalTo: logoContainer.centerXAnchor, constant: -2).isActive = true
// Main stack view
let stackView = UIStackView(arrangedSubviews: [ topStackView, settingButtonsStackView, inviteButton, faqButton, surveyButton, supportButton, helpTranslateButton, logoContainer, versionLabel ])
stackView.axis = .vertical
@ -202,6 +218,7 @@ final class SettingsVC : BaseVC, AvatarViewHelperDelegate {
stackView.layoutMargins = UIEdgeInsets(top: Values.mediumSpacing, left: 0, bottom: Values.mediumSpacing, right: 0)
stackView.isLayoutMarginsRelativeArrangement = true
stackView.set(.width, to: UIScreen.main.bounds.width)
// Scroll view
let scrollView = UIScrollView()
scrollView.showsVerticalScrollIndicator = false
@ -365,7 +382,7 @@ final class SettingsVC : BaseVC, AvatarViewHelperDelegate {
profileName: (name ?? ""),
avatarImage: profilePicture,
requiredSync: true,
success: {
success: { updatedProfile in
if displayNameToBeUploaded != nil {
userDefaults[.lastDisplayNameUpdate] = Date()
}
@ -378,11 +395,14 @@ final class SettingsVC : BaseVC, AvatarViewHelperDelegate {
DispatchQueue.main.async {
modalActivityIndicator.dismiss {
guard let self = self else { return }
self.profilePictureView.update()
self.displayNameLabel.text = name
self.profilePictureToBeUploaded = nil
self.displayNameToBeUploaded = nil
self?.profilePictureView.update(
publicKey: updatedProfile.id,
profile: updatedProfile,
threadVariant: .contact
)
self?.displayNameLabel.text = name
self?.profilePictureToBeUploaded = nil
self?.displayNameToBeUploaded = nil
}
}
},

View File

@ -78,10 +78,10 @@ final class UserCell : UITableViewCell {
separator.pin([ UIView.HorizontalEdge.leading, UIView.VerticalEdge.bottom, UIView.HorizontalEdge.trailing ], to: contentView)
}
// MARK: Updating
// MARK: - Updating
func update() {
profilePictureView.publicKey = publicKey
profilePictureView.update()
profilePictureView.update(for: publicKey)
displayNameLabel.text = Profile.displayName(id: publicKey)
switch accessory {

View File

@ -194,7 +194,6 @@ enum _001_InitialSetupMigration: Migration {
t.column(.recipientId, .text)
.notNull()
.indexed() // Quicker querying
.references(Profile.self)
t.column(.state, .integer)
.notNull()
.indexed() // Quicker querying
@ -225,6 +224,10 @@ enum _001_InitialSetupMigration: Migration {
t.column(.localRelativeFilePath, .text)
t.column(.width, .integer)
t.column(.height, .integer)
t.column(.duration, .double)
t.column(.isValid, .boolean)
.notNull()
.defaults(to: false)
t.column(.encryptionKey, .blob)
t.column(.digest, .blob)
t.column(.caption, .text)

View File

@ -1,6 +1,7 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import Foundation
import AVKit
import GRDB
import Curve25519Kit
import SessionUtilitiesKit
@ -13,16 +14,17 @@ enum _003_YDBToGRDBMigration: Migration {
static func migrate(_ db: Database) throws {
// MARK: - Process Contacts, Threads & Interactions
print("RAWR [\(Date().timeIntervalSince1970)] - SessionMessagingKit migration - Start")
var shouldFailMigration: Bool = false
var contacts: Set<Legacy.Contact> = []
var validProfileIds: Set<String> = []
var contactThreadIds: Set<String> = []
var legacyThreadIdToIdMap: [String: String] = [:]
var threads: Set<TSThread> = []
var disappearingMessagesConfiguration: [String: Legacy.DisappearingMessagesConfiguration] = [:]
var closedGroupKeys: [String: [TimeInterval: SessionUtilitiesKit.Legacy.KeyPair]] = [:]
var closedGroupKeys: [String: [TimeInterval: SUKLegacy.KeyPair]] = [:]
var closedGroupName: [String: String] = [:]
var closedGroupFormation: [String: UInt64] = [:]
var closedGroupModel: [String: TSGroupModel] = [:]
@ -36,18 +38,41 @@ enum _003_YDBToGRDBMigration: Migration {
// var openGroupServerToUniqueIdLookup: [String: [String]] = [:] // TODO: Not needed????
var interactions: [String: [TSInteraction]] = [:]
var attachments: [String: TSAttachment] = [:]
var attachments: [String: Legacy.Attachment] = [:]
var processedAttachmentIds: Set<String> = []
var outgoingReadReceiptsTimestampsMs: [String: Set<Int64>] = [:]
// Map the Legacy types for the NSKeyedUnarchiver
NSKeyedUnarchiver.setClass(
Legacy.Contact.self,
forClassName: "SNContact"
)
NSKeyedUnarchiver.setClass(
Legacy.Attachment.self,
forClassName: "TSAttachment"
)
NSKeyedUnarchiver.setClass(
Legacy.AttachmentStream.self,
forClassName: "TSAttachmentStream"
)
NSKeyedUnarchiver.setClass(
Legacy.AttachmentPointer.self,
forClassName: "TSAttachmentPointer"
)
NSKeyedUnarchiver.setClass(
Legacy.DisappearingConfigurationUpdateInfoMessage.self,
forClassName: "OWSDisappearingConfigurationUpdateInfoMessage"
)
Storage.read { transaction in
// Process the Contacts
transaction.enumerateRows(inCollection: Legacy.contactCollection) { _, object, _, _ in
guard let contact = object as? Legacy.Contact else { return }
contacts.insert(contact)
validProfileIds.insert(contact.sessionID)
}
print("RAWR [\(Date().timeIntervalSince1970)] - Process threads - Start")
let userClosedGroupPublicKeys: [String] = transaction.allKeys(inCollection: Legacy.closedGroupPublicKeyCollection)
// Process the threads
transaction.enumerateKeysAndObjects(inCollection: Legacy.threadCollection) { key, object, _ in
@ -66,7 +91,6 @@ enum _003_YDBToGRDBMigration: Migration {
disappearingMessagesConfiguration[threadId] = transaction
.object(forKey: threadId, inCollection: Legacy.disappearingMessagesCollection)
.asType(Legacy.DisappearingMessagesConfiguration.self)
.defaulting(to: Legacy.DisappearingMessagesConfiguration.defaultWith(threadId))
// Process group-specific info
guard let groupThread: TSGroupThread = thread as? TSGroupThread else {
@ -89,14 +113,6 @@ enum _003_YDBToGRDBMigration: Migration {
shouldFailMigration = true
return
}
guard userClosedGroupPublicKeys.contains(publicKey) else {
// TODO: Determine if we want to remove this
SNLog("[Migration Error] Found unexpected invalid closed group public key")
shouldFailMigration = true
return
}
let keyCollection: String = "\(Legacy.closedGroupKeyPairPrefix)\(publicKey)"
legacyThreadIdToIdMap[threadId] = publicKey
closedGroupName[threadId] = groupThread.name(with: transaction)
@ -107,10 +123,15 @@ enum _003_YDBToGRDBMigration: Migration {
inCollection: Legacy.closedGroupZombieMembersCollection
) as? Set<String>
// Note: If the user is no longer in a closed group then the group will still exist but the user
// won't have the closed group public key anymore
let keyCollection: String = "\(Legacy.closedGroupKeyPairPrefix)\(publicKey)"
transaction.enumerateKeysAndObjects(inCollection: keyCollection) { key, object, _ in
guard let timestamp: TimeInterval = TimeInterval(key), let keyPair: SessionUtilitiesKit.Legacy.KeyPair = object as? SessionUtilitiesKit.Legacy.KeyPair else {
return
}
guard
let timestamp: TimeInterval = TimeInterval(key),
let keyPair: SUKLegacy.KeyPair = object as? SUKLegacy.KeyPair
else { return }
closedGroupKeys[threadId] = (closedGroupKeys[threadId] ?? [:])
.setting(timestamp, keyPair)
@ -153,7 +174,7 @@ enum _003_YDBToGRDBMigration: Migration {
// Process attachments
print("RAWR [\(Date().timeIntervalSince1970)] - Process attachments - Start")
transaction.enumerateKeysAndObjects(inCollection: Legacy.attachmentsCollection) { key, object, _ in
guard let attachment: TSAttachment = object as? TSAttachment else {
guard let attachment: Legacy.Attachment = object as? Legacy.Attachment else {
SNLog("[Migration Error] Unable to process attachment")
shouldFailMigration = true
return
@ -227,7 +248,6 @@ enum _003_YDBToGRDBMigration: Migration {
var legacyInteractionToIdMap: [String: Int64] = [:]
var legacyInteractionIdentifierToIdMap: [String: Int64] = [:]
var legacyInteractionIdentifierToIdFallbackMap: [String: Int64] = [:]
var legacyAttachmentToIdMap: [String: String] = [:]
func identifier(
for threadId: String,
@ -296,7 +316,10 @@ enum _003_YDBToGRDBMigration: Migration {
creationDateTimestamp: thread.creationDate.timeIntervalSince1970,
shouldBeVisible: thread.shouldBeVisible,
isPinned: thread.isPinned,
messageDraft: thread.messageDraft,
messageDraft: ((thread.messageDraft ?? "").isEmpty ?
nil :
thread.messageDraft
),
notificationMode: notificationMode,
mutedUntilTimestamp: thread.mutedUntilDate?.timeIntervalSince1970
).insert(db)
@ -309,11 +332,15 @@ enum _003_YDBToGRDBMigration: Migration {
durationSeconds: TimeInterval(config.durationSeconds)
).insert(db)
}
else {
try DisappearingMessagesConfiguration
.defaultWith(threadId)
.insert(db)
}
// Closed Groups
if (thread as? TSGroupThread)?.isClosedGroup == true {
guard
let legacyKeys = closedGroupKeys[legacyThreadId],
let name: String = closedGroupName[legacyThreadId],
let groupModel: TSGroupModel = closedGroupModel[legacyThreadId],
let formationTimestamp: UInt64 = closedGroupFormation[legacyThreadId]
@ -328,7 +355,10 @@ enum _003_YDBToGRDBMigration: Migration {
formationTimestamp: TimeInterval(formationTimestamp)
).insert(db)
try legacyKeys.forEach { timestamp, legacyKeys in
// Note: If a user has left a closed group then they won't actually have any keys
// but they should still be able to browse the old messages so we do want to allow
// this case and migrate the rest of the info
try closedGroupKeys[legacyThreadId]?.forEach { timestamp, legacyKeys in
try ClosedGroupKeyPair(
threadId: threadId,
publicKey: legacyKeys.publicKey,
@ -469,7 +499,6 @@ enum _003_YDBToGRDBMigration: Migration {
recipientStateMap = [:]
mostRecentFailureText = nil
case let outgoingMessage as TSOutgoingMessage:
variant = .standardOutgoing
authorId = currentUserPublicKey
@ -481,11 +510,32 @@ enum _003_YDBToGRDBMigration: Migration {
mostRecentFailureText = outgoingMessage.mostRecentFailureText
case let infoMessage as TSInfoMessage:
// Note: The legacy 'TSInfoMessage' didn't store the author id so there is no
// way to determine who actually triggered the info message
authorId = currentUserPublicKey
body = ((infoMessage.body ?? "").isEmpty ?
infoMessage.customMessage :
infoMessage.body
)
body = {
// Note: The 'DisappearingConfigurationUpdateInfoMessage' stored additional info and constructed
// a string at display time so we want to continue that behaviour
guard
infoMessage.messageType == .disappearingMessagesUpdate,
let updateMessage: Legacy.DisappearingConfigurationUpdateInfoMessage = infoMessage as? Legacy.DisappearingConfigurationUpdateInfoMessage,
let infoMessageData: Data = try? JSONEncoder().encode(
DisappearingMessagesConfiguration.MessageInfo(
senderName: updateMessage.createdByRemoteName,
isEnabled: updateMessage.configurationIsEnabled,
durationSeconds: TimeInterval(updateMessage.configurationDurationSeconds)
)
),
let infoMessageString: String = String(data: infoMessageData, encoding: .utf8)
else {
return ((infoMessage.body ?? "").isEmpty ?
infoMessage.customMessage :
infoMessage.body
)
}
return infoMessageString
}()
wasRead = infoMessage.wasRead
expiresInSeconds = nil // Info messages don't expire
expiresStartedAtMs = nil // Info messages don't expire
@ -522,8 +572,15 @@ enum _003_YDBToGRDBMigration: Migration {
timestampMs: Int64(legacyInteraction.timestamp),
receivedAtTimestampMs: Int64(legacyInteraction.receivedAtTimestamp),
wasRead: wasRead,
expiresInSeconds: expiresInSeconds.map { TimeInterval($0) },
expiresStartedAtMs: expiresStartedAtMs.map { Double($0) },
// For both of these '0' used to be equivalent to null
expiresInSeconds: ((expiresInSeconds ?? 0) > 0 ?
expiresInSeconds.map { TimeInterval($0) } :
nil
),
expiresStartedAtMs: ((expiresStartedAtMs ?? 0) > 0 ?
expiresStartedAtMs.map { Double($0) } :
nil
),
linkPreviewUrl: linkPreview?.urlString, // Only a soft link so save to set
openGroupServerMessageId: openGroupServerMessageId.map { Int64($0) },
openGroupWhisperMods: false, // TODO: This in SOGSV4
@ -586,36 +643,75 @@ enum _003_YDBToGRDBMigration: Migration {
// Handle any quote
if let quotedMessage: TSQuotedMessage = quotedMessage {
let quoteAttachmentId: String? = quotedMessage.quotedAttachments
.compactMap { attachmentInfo in
if let attachmentId: String = attachmentInfo.attachmentId {
return attachmentId
}
else if let attachmentId: String = attachmentInfo.thumbnailAttachmentPointerId {
return attachmentId
}
// TODO: Looks like some of these might be busted???
return attachmentInfo.thumbnailAttachmentStreamId
var quoteAttachmentId: String? = quotedMessage.quotedAttachments
.flatMap { attachmentInfo in
return [
// Prioritise the thumbnail as it means we won't
// need to generate a new one
attachmentInfo.thumbnailAttachmentStreamId,
attachmentInfo.thumbnailAttachmentPointerId,
attachmentInfo.attachmentId
]
.compactMap { $0 }
}
.first { attachments[$0] != nil }
.first { attachmentId -> Bool in attachments[attachmentId] != nil }
guard quotedMessage.quotedAttachments.isEmpty || quoteAttachmentId != nil else {
// TODO: Is it possible to hit this case if a quoted attachment hasn't been downloaded?
SNLog("[Migration Error] Missing quote attachment")
throw GRDBStorageError.migrationFailed
// It looks like there can be cases where a quote can be quoting an
// interaction that isn't associated with a profile we know about (eg.
// if you join an open group and one of the first messages is a quote of
// an older message not cached to the device) - this will cause a foreign
// key constraint violation so in these cases just create an empty profile
if !validProfileIds.contains(quotedMessage.authorId) {
SNLog("[Migration Warning] Quote with unknown author found - Creating empty profile")
// Note: Need to upsert here because it's possible multiple quotes
// will use the same invalid 'authorId' value resulting in a unique
// constraint violation
try Profile(
id: quotedMessage.authorId,
name: quotedMessage.authorId
).save(db)
}
// Note: It looks like there is a way for a quote to not have it's
// associated attachmentId so let's try our best to track down the
// original interaction and re-create the attachment link before
// falling back to having no attachment in the quote
if quoteAttachmentId == nil && !quotedMessage.quotedAttachments.isEmpty {
quoteAttachmentId = interactions[legacyThreadId]?
.first(where: {
$0.timestamp == quotedMessage.timestamp &&
(
// Outgoing messages don't store the 'authorId' so we
// need to compare against the 'currentUserPublicKey'
// for those or cast to a TSIncomingMessage otherwise
quotedMessage.authorId == currentUserPublicKey ||
quotedMessage.authorId == ($0 as? TSIncomingMessage)?.authorId
)
})
.asType(TSMessage.self)?
.attachmentIds
.firstObject
.asType(String.self)
SNLog([
"[Migration Warning] Quote with invalid attachmentId found",
(quoteAttachmentId == nil ?
"Unable to reconcile, leaving attachment blank" :
"Original interaction found, using source attachment"
)
].joined(separator: " - "))
}
// Setup the attachment and add it to the lookup (if it exists)
let attachmentId: String? = try attachmentId(
db,
for: quoteAttachmentId,
attachments: attachments
isQuotedMessage: true,
attachments: attachments,
processedAttachmentIds: &processedAttachmentIds
)
if let quoteAttachmentId: String = quoteAttachmentId, let attachmentId: String = attachmentId {
legacyAttachmentToIdMap[quoteAttachmentId] = attachmentId
}
// Create the quote
try Quote(
interactionId: interactionId,
@ -642,13 +738,10 @@ enum _003_YDBToGRDBMigration: Migration {
let attachmentId: String? = try attachmentId(
db,
for: linkPreview.imageAttachmentId,
attachments: attachments
attachments: attachments,
processedAttachmentIds: &processedAttachmentIds
)
if let legacyAttachmentId: String = linkPreview.imageAttachmentId, let attachmentId: String = attachmentId {
legacyAttachmentToIdMap[legacyAttachmentId] = attachmentId
}
// Note: It's possible for there to be duplicate values here so we use 'save'
// instead of insert (ie. upsert)
try LinkPreview(
@ -663,8 +756,13 @@ enum _003_YDBToGRDBMigration: Migration {
// Handle any attachments
try attachmentIds.forEach { legacyAttachmentId in
guard let attachmentId: String = try attachmentId(db, for: legacyAttachmentId, interactionVariant: variant, attachments: attachments) else {
// TODO: Is it possible to hit this case if an interaction hasn't been viewed?
guard let attachmentId: String = try attachmentId(
db,
for: legacyAttachmentId,
interactionVariant: variant,
attachments: attachments,
processedAttachmentIds: &processedAttachmentIds
) else {
SNLog("[Migration Error] Missing interaction attachment")
throw GRDBStorageError.migrationFailed
}
@ -674,13 +772,13 @@ enum _003_YDBToGRDBMigration: Migration {
interactionId: interactionId,
attachmentId: attachmentId
).insert(db)
legacyAttachmentToIdMap[legacyAttachmentId] = attachmentId
}
}
}
}
print("RAWR [\(Date().timeIntervalSince1970)] - Process thread inserts - End")
// Clear out processed data (give the memory a change to be freed)
contacts = []
@ -706,6 +804,8 @@ enum _003_YDBToGRDBMigration: Migration {
// MARK: - Process Legacy Jobs
print("RAWR [\(Date().timeIntervalSince1970)] - Process jobs - Start")
var notifyPushServerJobs: Set<Legacy.NotifyPNServerJob> = []
var messageReceiveJobs: Set<Legacy.MessageReceiveJob> = []
var messageSendJobs: Set<Legacy.MessageSendJob> = []
@ -737,6 +837,83 @@ enum _003_YDBToGRDBMigration: Migration {
Legacy.AttachmentDownloadJob.self,
forClassName: "SessionMessagingKit.AttachmentDownloadJob"
)
NSKeyedUnarchiver.setClass(
Legacy.Message.self,
forClassName: "SNMessage"
)
NSKeyedUnarchiver.setClass(
Legacy.VisibleMessage.self,
forClassName: "SNVisibleMessage"
)
NSKeyedUnarchiver.setClass(
Legacy.Quote.self,
forClassName: "SNQuote"
)
NSKeyedUnarchiver.setClass(
Legacy.LinkPreview.self,
forClassName: "SessionServiceKit.OWSLinkPreview" // Very old legacy name
)
NSKeyedUnarchiver.setClass(
Legacy.LinkPreview.self,
forClassName: "SNLinkPreview"
)
NSKeyedUnarchiver.setClass(
Legacy.Profile.self,
forClassName: "SNProfile"
)
NSKeyedUnarchiver.setClass(
Legacy.OpenGroupInvitation.self,
forClassName: "SNOpenGroupInvitation"
)
NSKeyedUnarchiver.setClass(
Legacy.ControlMessage.self,
forClassName: "SNControlMessage"
)
NSKeyedUnarchiver.setClass(
Legacy.ReadReceipt.self,
forClassName: "SNReadReceipt"
)
NSKeyedUnarchiver.setClass(
Legacy.TypingIndicator.self,
forClassName: "SNTypingIndicator"
)
NSKeyedUnarchiver.setClass(
Legacy.ClosedGroupControlMessage.self,
forClassName: "SessionMessagingKit.ClosedGroupControlMessage"
)
NSKeyedUnarchiver.setClass(
Legacy.ClosedGroupControlMessage.KeyPairWrapper.self,
forClassName: "ClosedGroupControlMessage.SNKeyPairWrapper"
)
NSKeyedUnarchiver.setClass(
Legacy.DataExtractionNotification.self,
forClassName: "SessionMessagingKit.DataExtractionNotification"
)
NSKeyedUnarchiver.setClass(
Legacy.ExpirationTimerUpdate.self,
forClassName: "SNExpirationTimerUpdate"
)
NSKeyedUnarchiver.setClass(
Legacy.ConfigurationMessage.self,
forClassName: "SNConfigurationMessage"
)
NSKeyedUnarchiver.setClass(
Legacy.CMClosedGroup.self,
forClassName: "SNClosedGroup"
)
NSKeyedUnarchiver.setClass(
Legacy.CMContact.self,
forClassName: "SNConfigurationMessage.SNConfigurationMessageContact"
)
NSKeyedUnarchiver.setClass(
Legacy.UnsendRequest.self,
forClassName: "SNUnsendRequest"
)
NSKeyedUnarchiver.setClass(
Legacy.MessageRequestResponse.self,
forClassName: "SNMessageRequestResponse"
)
Storage.read { transaction in
transaction.enumerateRows(inCollection: Legacy.notifyPushServerJobCollection) { _, object, _, _ in
guard let job = object as? Legacy.NotifyPNServerJob else { return }
@ -764,8 +941,12 @@ enum _003_YDBToGRDBMigration: Migration {
}
}
print("RAWR [\(Date().timeIntervalSince1970)] - Process jobs - End")
// MARK: - Insert Jobs
print("RAWR [\(Date().timeIntervalSince1970)] - Process job inserts - Start")
// MARK: - --notifyPushServer
try autoreleasepool {
@ -805,7 +986,18 @@ enum _003_YDBToGRDBMigration: Migration {
return
}
let threadId: String? = MessageReceiver.extractSenderPublicKey(db, from: envelope)
let threadId: String?
switch envelope.type {
// For closed group messages the 'groupPublicKey' is stored in the
// 'envelope.source' value and that should be used for the 'threadId'
case .closedGroupMessage:
threadId = envelope.source
break
default:
threadId = MessageReceiver.extractSenderPublicKey(db, from: envelope)
}
_ = try Job(
failureCount: legacyJob.failureCount,
@ -873,7 +1065,8 @@ enum _003_YDBToGRDBMigration: Migration {
destination: legacyJob.destination,
variant: {
switch legacyJob.message {
case is ExpirationTimerUpdate: return .infoDisappearingMessagesUpdate
case is Legacy.ExpirationTimerUpdate:
return .infoDisappearingMessagesUpdate
default: return nil
}
}(),
@ -895,7 +1088,7 @@ enum _003_YDBToGRDBMigration: Migration {
// in these cases the 'interactionId' value will be nil
interactionId: interactionId,
destination: legacyJob.destination,
message: legacyJob.message
message: legacyJob.message.toNonLegacy()
)
)?.inserted(db)
@ -936,7 +1129,7 @@ enum _003_YDBToGRDBMigration: Migration {
SNLog("[Migration Error] attachmentDownload job unable to find interaction")
throw GRDBStorageError.migrationFailed
}
guard let attachmentId: String = legacyAttachmentToIdMap[legacyJob.attachmentID] else {
guard processedAttachmentIds.contains(legacyJob.attachmentID) else {
SNLog("[Migration Error] attachmentDownload job unable to find attachment")
throw GRDBStorageError.migrationFailed
}
@ -949,7 +1142,7 @@ enum _003_YDBToGRDBMigration: Migration {
threadId: legacyThreadIdToIdMap[legacyJob.threadID],
interactionId: interactionId,
details: AttachmentDownloadJob.Details(
attachmentId: attachmentId
attachmentId: legacyJob.attachmentID
)
)?.inserted(db)
}
@ -971,8 +1164,12 @@ enum _003_YDBToGRDBMigration: Migration {
}
}
print("RAWR [\(Date().timeIntervalSince1970)] - Process job inserts - End")
// MARK: - Process Preferences
print("RAWR [\(Date().timeIntervalSince1970)] - Process preferences inserts - Start")
var legacyPreferences: [String: Any] = [:]
Storage.read { transaction in
@ -1027,24 +1224,39 @@ enum _003_YDBToGRDBMigration: Migration {
db[.hasHiddenMessageRequests] = CurrentAppContext().appUserDefaults()
.bool(forKey: Legacy.userDefaultsHasHiddenMessageRequests)
print("RAWR [\(Date().timeIntervalSince1970)] - Process thread inserts - End")
print("RAWR [\(Date().timeIntervalSince1970)] - Process preferences inserts - End")
print("RAWR Done!!!")
}
// MARK: - Convenience
private static func attachmentId(_ db: Database, for legacyAttachmentId: String?, interactionVariant: Interaction.Variant? = nil, attachments: [String: TSAttachment]) throws -> String? {
private static func attachmentId(
_ db: Database,
for legacyAttachmentId: String?,
interactionVariant: Interaction.Variant? = nil,
isQuotedMessage: Bool = false,
attachments: [String: Legacy.Attachment],
processedAttachmentIds: inout Set<String>
) throws -> String? {
guard let legacyAttachmentId: String = legacyAttachmentId else { return nil }
guard let legacyAttachment: TSAttachment = attachments[legacyAttachmentId] else {
SNLog("[Migration Error] Missing attachment")
throw GRDBStorageError.migrationFailed
guard !processedAttachmentIds.contains(legacyAttachmentId) else {
guard isQuotedMessage else {
SNLog("[Migration Error] Attempted to process duplicate attachment")
throw GRDBStorageError.migrationFailed
}
return legacyAttachmentId
}
guard let legacyAttachment: Legacy.Attachment = attachments[legacyAttachmentId] else {
SNLog("[Migration Warning] Missing attachment - interaction will appear as blank")
return nil
}
let state: Attachment.State = {
switch legacyAttachment {
case let stream as TSAttachmentStream: // Outgoing or already downloaded
case let stream as Legacy.AttachmentStream: // Outgoing or already downloaded
switch interactionVariant {
case .standardOutgoing: return (stream.isUploaded ? .uploaded : .pending)
default: return .downloaded
@ -1056,28 +1268,90 @@ enum _003_YDBToGRDBMigration: Migration {
}()
let size: CGSize = {
switch legacyAttachment {
case let stream as TSAttachmentStream: return stream.calculateImageSize()
case let pointer as TSAttachmentPointer: return pointer.mediaSize
case let stream as Legacy.AttachmentStream:
guard let originalFilePath: String = Attachment.originalFilePath(id: legacyAttachmentId, mimeType: stream.contentType, sourceFilename: stream.localRelativeFilePath) else {
return .zero
}
return Attachment
.imageSize(
contentType: stream.contentType,
originalFilePath: originalFilePath
)
.defaulting(to: .zero)
case let pointer as Legacy.AttachmentPointer: return pointer.mediaSize
default: return CGSize.zero
}
}()
let (isValid, duration): (Bool, TimeInterval?) = {
guard
let stream: Legacy.AttachmentStream = legacyAttachment as? Legacy.AttachmentStream,
let originalFilePath: String = Attachment.originalFilePath(
id: legacyAttachmentId,
mimeType: stream.contentType,
sourceFilename: stream.localRelativeFilePath
)
else {
return (false, nil)
}
if stream.isAudio {
if let cachedDuration: TimeInterval = stream.cachedAudioDurationSeconds?.doubleValue, cachedDuration > 0 {
return (true, cachedDuration)
}
let (isValid, duration): (Bool, TimeInterval?) = Attachment.determineValidityAndDuration(
contentType: stream.contentType,
originalFilePath: originalFilePath
)
return (isValid, duration)
}
if stream.isVideo {
let videoPlayer: AVPlayer = AVPlayer(url: URL(fileURLWithPath: originalFilePath))
let duration: TimeInterval? = videoPlayer.currentItem
.map { item -> TimeInterval in
// Accorting to the CMTime docs "value/timescale = seconds"
(TimeInterval(item.duration.value) / TimeInterval(item.duration.timescale))
}
return ((duration ?? 0) > 0, duration)
}
if stream.isVisualMedia {
return (stream.isValidVisualMedia, nil)
}
return (true, nil)
}()
let attachment: Attachment = try Attachment(
_ = try Attachment(
// Note: The legacy attachment object used a UUID string for it's id as well
// and saved files using these id's so just used the existing id so we don't
// need to bother renaming files as part of the migration
id: legacyAttachmentId,
serverId: "\(legacyAttachment.serverId)",
variant: (legacyAttachment.isVoiceMessage ? .voiceMessage : .standard),
variant: (legacyAttachment.attachmentType == .voiceMessage ? .voiceMessage : .standard),
state: state,
contentType: legacyAttachment.contentType,
byteCount: UInt(legacyAttachment.byteCount),
creationTimestamp: (legacyAttachment as? TSAttachmentStream)?.creationTimestamp.timeIntervalSince1970,
creationTimestamp: (legacyAttachment as? Legacy.AttachmentStream)?.creationTimestamp.timeIntervalSince1970,
sourceFilename: legacyAttachment.sourceFilename,
downloadUrl: legacyAttachment.downloadURL,
width: (size == .zero ? nil : UInt(size.width)),
height: (size == .zero ? nil : UInt(size.height)),
duration: duration,
isValid: isValid,
encryptionKey: legacyAttachment.encryptionKey,
digest: (legacyAttachment as? TSAttachmentStream)?.digest,
digest: (legacyAttachment as? Legacy.AttachmentStream)?.digest,
caption: legacyAttachment.caption
).inserted(db)
return attachment.id
processedAttachmentIds.insert(legacyAttachmentId)
return legacyAttachmentId
}
}

View File

@ -2,9 +2,13 @@
import Foundation
import GRDB
import PromiseKit
import SignalCoreKit
import SessionUtilitiesKit
import AVFAudio
import AVFoundation
public struct Attachment: Codable, Identifiable, FetchableRecord, PersistableRecord, TableRecord, ColumnExpressible {
public struct Attachment: Codable, Identifiable, Equatable, FetchableRecord, PersistableRecord, TableRecord, ColumnExpressible {
public static var databaseTableName: String { "attachment" }
internal static let interactionAttachments = belongsTo(InteractionAttachment.self)
fileprivate static let quote = belongsTo(Quote.self)
@ -24,6 +28,8 @@ public struct Attachment: Codable, Identifiable, FetchableRecord, PersistableRec
case localRelativeFilePath
case width
case height
case duration
case isValid
case encryptionKey
case digest
case caption
@ -44,7 +50,7 @@ public struct Attachment: Codable, Identifiable, FetchableRecord, PersistableRec
}
/// A unique identifier for the attachment
public let id: String = UUID().uuidString
public let id: String
/// The id for the attachment returned by the server
///
@ -93,6 +99,12 @@ public struct Attachment: Codable, Identifiable, FetchableRecord, PersistableRec
/// The height of the attachment, this will be `null` for non-visual attachment types
public let height: UInt?
/// The number of seconds the attachment plays for (this will only be set for video and audio attachment types)
public let duration: TimeInterval?
/// A flag indicating whether the attachment data downloaded is valid for it's content type
public let isValid: Bool
/// The key used to decrypt the attachment
public let encryptionKey: Data?
@ -105,6 +117,7 @@ public struct Attachment: Codable, Identifiable, FetchableRecord, PersistableRec
// MARK: - Initialization
public init(
id: String = UUID().uuidString,
serverId: String? = nil,
variant: Variant,
state: State = .pending,
@ -116,10 +129,13 @@ public struct Attachment: Codable, Identifiable, FetchableRecord, PersistableRec
localRelativeFilePath: String? = nil,
width: UInt? = nil,
height: UInt? = nil,
duration: TimeInterval? = nil,
isValid: Bool = false,
encryptionKey: Data? = nil,
digest: Data? = nil,
caption: String? = nil
) {
self.id = id
self.serverId = serverId
self.variant = variant
self.state = state
@ -131,19 +147,21 @@ public struct Attachment: Codable, Identifiable, FetchableRecord, PersistableRec
self.localRelativeFilePath = localRelativeFilePath
self.width = width
self.height = height
self.duration = duration
self.isValid = isValid
self.encryptionKey = encryptionKey
self.digest = digest
self.caption = caption
}
/// This initializer should only be used when converting from either a LinkPreview or a SignalAttachment to an Attachment (prior to upload)
public init?(
id: String = UUID().uuidString,
variant: Variant = .standard,
contentType: String,
dataSource: DataSource
) {
guard
let originalFilePath: String = Attachment.originalFilePath(id: self.id, mimeType: contentType, sourceFilename: nil)
else {
guard let originalFilePath: String = Attachment.originalFilePath(id: id, mimeType: contentType, sourceFilename: nil) else {
return nil
}
guard dataSource.write(toPath: originalFilePath) else { return nil }
@ -152,7 +170,12 @@ public struct Attachment: Codable, Identifiable, FetchableRecord, PersistableRec
contentType: contentType,
originalFilePath: originalFilePath
)
let (isValid, duration): (Bool, TimeInterval?) = Attachment.determineValidityAndDuration(
contentType: contentType,
originalFilePath: originalFilePath
)
self.id = id
self.serverId = nil
self.variant = variant
self.state = .pending
@ -164,6 +187,8 @@ public struct Attachment: Codable, Identifiable, FetchableRecord, PersistableRec
self.localRelativeFilePath = nil
self.width = imageSize.map { UInt(floor($0.width)) }
self.height = imageSize.map { UInt(floor($0.height)) }
self.duration = duration
self.isValid = isValid
self.encryptionKey = nil
self.digest = nil
self.caption = nil
@ -223,7 +248,24 @@ public extension Attachment {
encryptionKey: Data? = nil,
digest: Data? = nil
) -> Attachment {
let (isValid, duration): (Bool, TimeInterval?) = {
switch (self.state, state) {
case (_, .downloaded):
return Attachment.determineValidityAndDuration(
contentType: contentType,
originalFilePath: originalFilePath
)
// Assume the data is already correct for "uploading" attachments (and don't override it)
case (.uploading, .failed), (.uploaded, .failed): return (self.isValid, self.duration)
case (_, .failed): return (false, nil)
default: return (self.isValid, self.duration)
}
}()
return Attachment(
id: self.id,
serverId: (serverId ?? self.serverId),
variant: variant,
state: (state ?? self.state),
@ -235,6 +277,8 @@ public extension Attachment {
localRelativeFilePath: (localRelativeFilePath ?? self.localRelativeFilePath),
width: width,
height: height,
duration: duration,
isValid: isValid,
encryptionKey: (encryptionKey ?? self.encryptionKey),
digest: (digest ?? self.digest),
caption: self.caption
@ -255,7 +299,8 @@ public extension Attachment {
return (MIMETypeUtil.mimeType(forFileExtension: fileExtension) ?? OWSMimeTypeApplicationOctetStream)
}
self.serverId = nil
self.id = UUID().uuidString
self.serverId = "\(proto.id)"
self.variant = {
let voiceMessageFlag: Int32 = SNProtoAttachmentPointer.SNProtoAttachmentPointerFlags
.voiceMessage
@ -276,6 +321,8 @@ public extension Attachment {
self.localRelativeFilePath = nil
self.width = (proto.hasWidth && proto.width > 0 ? UInt(proto.width) : nil)
self.height = (proto.hasHeight && proto.height > 0 ? UInt(proto.height) : nil)
self.duration = nil // Needs to be downloaded to be set
self.isValid = false // Needs to be downloaded to be set
self.encryptionKey = proto.key
self.digest = proto.digest
self.caption = (proto.hasCaption ? proto.caption : nil)
@ -335,31 +382,56 @@ public extension Attachment {
// MARK: - GRDB Interactions
public extension Attachment {
static func fetchAllPendingAttachments(_ db: Database, for threadId: String) throws -> [Attachment] {
return try Attachment
.select(Attachment.Columns.allCases + [Interaction.Columns.id])
.filter(Columns.variant == Variant.standard)
.filter(Columns.state == State.pending)
.joining(
optional: Attachment.interactionAttachments
.filter(Interaction.Columns.threadId == threadId)
)
.joining(
optional: Attachment.quote
.joining(
required: Quote.interaction
.filter(Interaction.Columns.threadId == threadId)
)
)//tmp.authorId
.joining(
optional: Attachment.linkPreview
.joining(
required: LinkPreview.interactions
.filter(Interaction.Columns.threadId == threadId)
)
)
.order(Interaction.Columns.id.desc) // Newest attachments first
.fetchAll(db)
struct DownloadInfo: FetchableRecord, Decodable {
public let attachmentId: String
public let interactionId: Int64
}
static func pendingAttachmentDownloadInfo(for authorId: String) -> SQLRequest<Attachment.DownloadInfo> {
let attachment: TypedTableAlias<Attachment> = TypedTableAlias()
let interaction: TypedTableAlias<Interaction> = TypedTableAlias()
let quote: TypedTableAlias<Quote> = TypedTableAlias()
let interactionAttachment: TypedTableAlias<InteractionAttachment> = TypedTableAlias()
let linkPreview: TypedTableAlias<LinkPreview> = TypedTableAlias()
// Note: In GRDB all joins need to run via their "association" system which doesn't support the type
// of query we have below (a required join based on one of 3 optional joins) so we have to construct
// the query manually
return """
SELECT DISTINCT
\(attachment[.id]) AS attachmentId,
\(interaction[.id]) AS interactionId
FROM \(Attachment.self)
JOIN \(Interaction.self) ON
\(interaction[.authorId]) = \(SQL(sql: ":authorId", arguments: StatementArguments(["authorId": authorId]))) AND (
\(interaction[.id]) = \(quote[.interactionId]) OR
\(interaction[.id]) = \(interactionAttachment[.interactionId]) OR
\(interaction[.linkPreviewUrl]) = \(linkPreview[.url])
)
LEFT JOIN \(Quote.self) ON \(quote[.attachmentId]) = \(attachment[.id])
LEFT JOIN \(InteractionAttachment.self) ON \(interactionAttachment[.attachmentId]) = \(attachment[.id])
LEFT JOIN \(LinkPreview.self) ON
\(linkPreview[.attachmentId]) = \(attachment[.id]) AND
\(linkPreview[.variant]) = \(SQL(
sql: ":variant",
arguments: StatementArguments(["variant": LinkPreview.Variant.standard])
))
WHERE
\(attachment[.variant]) = \(SQL(
sql: ":attachmentVariant",
arguments: StatementArguments(["attachmentVariant": Attachment.Variant.standard])
)) AND
\(attachment[.state]) = \(SQL(
sql: ":state",
arguments: StatementArguments(["state": Attachment.State.pending])
))
ORDER BY interactionId DESC
"""
}
}
@ -370,11 +442,11 @@ public extension Attachment {
private static let thumbnailDimensionMedium: UInt = 450
/// This size is large enough to render full screen
private static var thumbnailDimensionsLarge: CGFloat = {
private static var thumbnailDimensionLarge: UInt = {
let screenSizePoints: CGSize = UIScreen.main.bounds.size
let minZoomFactor: CGFloat = 2 // TODO: Should this be screen scale?
let minZoomFactor: CGFloat = UIScreen.main.scale
return (max(screenSizePoints.width, screenSizePoints.height) * minZoomFactor)
return UInt(floor(max(screenSizePoints.width, screenSizePoints.height) * minZoomFactor))
}()
private static var sharedDataAttachmentsDirPath: String = {
@ -404,7 +476,7 @@ public extension Attachment {
)
}
static func imageSize(contentType: String, originalFilePath: String) -> CGSize? {
internal static func imageSize(contentType: String, originalFilePath: String) -> CGSize? {
let isVideo: Bool = MIMETypeUtil.isVideo(contentType)
let isImage: Bool = MIMETypeUtil.isImage(contentType)
let isAnimated: Bool = MIMETypeUtil.isAnimated(contentType)
@ -423,15 +495,77 @@ public extension Attachment {
static func videoStillImage(filePath: String) -> UIImage? {
return try? OWSMediaUtils.thumbnail(
forVideoAtPath: filePath,
maxDimension: Attachment.thumbnailDimensionsLarge
maxDimension: CGFloat(Attachment.thumbnailDimensionLarge)
)
}
internal static func determineValidityAndDuration(contentType: String, originalFilePath: String?) -> (isValid: Bool, duration: TimeInterval?) {
guard let originalFilePath: String = originalFilePath else { return (false, nil) }
// Process audio attachments
if MIMETypeUtil.isAudio(contentType) {
do {
let audioPlayer: AVAudioPlayer = try AVAudioPlayer(contentsOf: URL(fileURLWithPath: originalFilePath))
return ((audioPlayer.duration > 0), audioPlayer.duration)
}
catch {
switch (error as NSError).code {
case Int(kAudioFileInvalidFileError), Int(kAudioFileStreamError_InvalidFile):
// Ignore "invalid audio file" errors
return (false, nil) // TODO: Confirm this behaviour (previously returned 0)
default: return (false, nil)
}
}
}
// Process image attachments
if MIMETypeUtil.isImage(contentType) {
return (
NSData.ows_isValidImage(atPath: originalFilePath, mimeType: contentType),
nil
)
}
// Process video attachments
if MIMETypeUtil.isVideo(contentType) {
let videoPlayer: AVPlayer = AVPlayer(url: URL(fileURLWithPath: originalFilePath))
let durationSeconds: TimeInterval? = videoPlayer.currentItem
.map { item -> TimeInterval in
// Accorting to the CMTime docs "value/timescale = seconds"
(TimeInterval(item.duration.value) / TimeInterval(item.duration.timescale))
}
return (
OWSMediaUtils.isValidVideo(path: originalFilePath),
durationSeconds
)
}
// Any other attachment types are valid and have no duration
return (true, nil)
}
}
// MARK: - Convenience
extension Attachment {
var originalFilePath: String? {
public enum ThumbnailSize {
case small
case medium
case large
var dimension: UInt {
switch self {
case .small: return Attachment.thumbnailDimensionSmall
case .medium: return Attachment.thumbnailDimensionMedium
case .large: return Attachment.thumbnailDimensionLarge
}
}
}
public var originalFilePath: String? {
return Attachment.originalFilePath(
id: self.id,
mimeType: self.contentType,
@ -453,18 +587,19 @@ extension Attachment {
}
guard isImage || isAnimated else { return nil }
guard NSData.ows_isValidImage(atPath: originalFilePath, mimeType: contentType) else {
return nil
}
guard isValid else { return nil }
return UIImage(contentsOfFile: originalFilePath)
}
var isImage: Bool { MIMETypeUtil.isImage(contentType) }
var isVideo: Bool { MIMETypeUtil.isVideo(contentType) }
var isAnimated: Bool { MIMETypeUtil.isAnimated(contentType) }
public var isImage: Bool { MIMETypeUtil.isImage(contentType) }
public var isVideo: Bool { MIMETypeUtil.isVideo(contentType) }
public var isAnimated: Bool { MIMETypeUtil.isAnimated(contentType) }
public var isAudio: Bool { MIMETypeUtil.isAudio(contentType) }
func readDataFromFile() throws -> Data? {
public var isVisualMedia: Bool { isImage || isVideo || isAnimated }
public func readDataFromFile() throws -> Data? {
guard let filePath: String = Attachment.originalFilePath(id: self.id, mimeType: self.contentType, sourceFilename: self.sourceFilename) else {
return nil
}
@ -514,14 +649,18 @@ extension Attachment {
)
}
func thumbnailImageSmallSync() -> UIImage? {
public func thumbnail(size: ThumbnailSize, success: @escaping (UIImage) -> (), failure: @escaping () -> ()) {
loadThumbnail(with: size.dimension, success: success, failure: failure)
}
func thumbnailSync(size: ThumbnailSize) -> UIImage? {
guard isVideo || isImage || isAnimated else { return nil }
let semaphore: DispatchSemaphore = DispatchSemaphore(value: 0)
var image: UIImage?
loadThumbnail(
with: Attachment.thumbnailDimensionSmall,
thumbnail(
size: size,
success: { loadedImage in
image = loadedImage
semaphore.signal()

View File

@ -12,7 +12,7 @@ public struct ClosedGroup: Codable, Identifiable, FetchableRecord, PersistableRe
ClosedGroupKeyPair.self,
using: ClosedGroupKeyPair.closedGroupForeignKey
)
private static let members = hasMany(GroupMember.self, using: GroupMember.closedGroupForeignKey)
public static let members = hasMany(GroupMember.self, using: GroupMember.closedGroupForeignKey)
public typealias Columns = CodingKeys
public enum CodingKeys: String, CodingKey, ColumnExpression {
@ -65,6 +65,18 @@ public struct ClosedGroup: Codable, Identifiable, FetchableRecord, PersistableRe
.filter(GroupMember.Columns.role == GroupMember.Role.admin)
}
// MARK: - Initialization
public init(
threadId: String,
name: String,
formationTimestamp: TimeInterval
) {
self.threadId = threadId
self.name = name
self.formationTimestamp = formationTimestamp
}
// MARK: - Custom Database Interaction
public func delete(_ db: Database) throws -> Bool {

View File

@ -7,6 +7,7 @@ import SessionUtilitiesKit
public struct Contact: Codable, Identifiable, FetchableRecord, PersistableRecord, TableRecord, ColumnExpressible {
public static var databaseTableName: String { "contact" }
internal static let threadForeignKey = ForeignKey([Columns.id], to: [SessionThread.Columns.id])
public static let profile = hasOne(Profile.self, using: Profile.contactForeignKey)
public typealias Columns = CodingKeys
public enum CodingKeys: String, CodingKey, ColumnExpression {
@ -37,6 +38,12 @@ public struct Contact: Codable, Identifiable, FetchableRecord, PersistableRecord
/// This flag is used to determine whether this contact has ever been blocked (will be included in the config message if so)
public let hasBeenBlocked: Bool
// MARK: - Relationships
public var profile: QueryInterfaceRequest<Profile> {
request(for: Contact.profile)
}
// MARK: - Initialization
public init(

View File

@ -57,6 +57,34 @@ public extension DisappearingMessagesConfiguration {
// MARK: - Convenience
public extension DisappearingMessagesConfiguration {
struct MessageInfo: Codable {
public let senderName: String?
public let isEnabled: Bool
public let durationSeconds: TimeInterval
var previewText: String {
guard let senderName: String = senderName else {
// Changed by localNumber on this device or via synced transcript
guard isEnabled, durationSeconds > 0 else { return "YOU_DISABLED_DISAPPEARING_MESSAGES_CONFIGURATION".localized() }
return String(
format: "YOU_UPDATED_DISAPPEARING_MESSAGES_CONFIGURATION".localized(),
NSString.formatDurationSeconds(UInt32(floor(durationSeconds)), useShortFormat: false)
)
}
guard isEnabled, durationSeconds > 0 else {
return String(format: "OTHER_DISABLED_DISAPPEARING_MESSAGES_CONFIGURATION".localized(), senderName)
}
return String(
format: "OTHER_UPDATED_DISAPPEARING_MESSAGES_CONFIGURATION".localized(),
NSString.formatDurationSeconds(UInt32(floor(durationSeconds)), useShortFormat: false),
senderName
)
}
}
var durationIndex: Int {
return DisappearingMessagesConfiguration.validDurationsSeconds
.firstIndex(of: durationSeconds)
@ -67,26 +95,16 @@ public extension DisappearingMessagesConfiguration {
NSString.formatDurationSeconds(UInt32(durationSeconds), useShortFormat: false)
}
func infoUpdateMessage(with senderName: String?) -> String {
guard let senderName: String = senderName else {
// Changed by localNumber on this device or via synced transcript
guard isEnabled, durationSeconds > 0 else { return "YOU_DISABLED_DISAPPEARING_MESSAGES_CONFIGURATION".localized() }
return String(
format: "YOU_UPDATED_DISAPPEARING_MESSAGES_CONFIGURATION".localized(),
NSString.formatDurationSeconds(UInt32(floor(durationSeconds)), useShortFormat: false)
)
}
guard isEnabled, durationSeconds > 0 else {
return String(format: "OTHER_DISABLED_DISAPPEARING_MESSAGES_CONFIGURATION".localized(), senderName)
}
return String(
format: "OTHER_UPDATED_DISAPPEARING_MESSAGES_CONFIGURATION".localized(),
NSString.formatDurationSeconds(UInt32(floor(durationSeconds)), useShortFormat: false),
senderName
func messageInfoString(with senderName: String?) -> String? {
let messageInfo: MessageInfo = DisappearingMessagesConfiguration.MessageInfo(
senderName: senderName,
isEnabled: isEnabled,
durationSeconds: durationSeconds
)
guard let messageInfoData: Data = try? JSONEncoder().encode(messageInfo) else { return nil }
return String(data: messageInfoData, encoding: .utf8)
}
}

View File

@ -9,9 +9,9 @@ public struct GroupMember: Codable, FetchableRecord, PersistableRecord, TableRec
internal static let openGroupForeignKey = ForeignKey([Columns.groupId], to: [OpenGroup.Columns.threadId])
internal static let closedGroupForeignKey = ForeignKey([Columns.groupId], to: [ClosedGroup.Columns.threadId])
internal static let profileForeignKey = ForeignKey([Columns.profileId], to: [Profile.Columns.id])
private static let openGroup = belongsTo(OpenGroup.self, using: openGroupForeignKey)
private static let closedGroup = belongsTo(ClosedGroup.self, using: closedGroupForeignKey)
private static let profile = hasOne(Profile.self, using: profileForeignKey)
public static let openGroup = belongsTo(OpenGroup.self, using: openGroupForeignKey)
public static let closedGroup = belongsTo(ClosedGroup.self, using: closedGroupForeignKey)
public static let profile = hasOne(Profile.self, using: profileForeignKey)
public typealias Columns = CodingKeys
public enum CodingKeys: String, CodingKey, ColumnExpression {

View File

@ -7,25 +7,31 @@ import SessionUtilitiesKit
public struct Interaction: Codable, Identifiable, Equatable, FetchableRecord, MutablePersistableRecord, TableRecord, ColumnExpressible {
public static var databaseTableName: String { "interaction" }
internal static let threadForeignKey = ForeignKey([Columns.threadId], to: [SessionThread.Columns.id])
internal static let profileForeignKey = ForeignKey([Columns.authorId], to: [Profile.Columns.id])
internal static let linkPreviewForeignKey = ForeignKey(
[Columns.linkPreviewUrl],
to: [LinkPreview.Columns.url]
)
internal static let thread = belongsTo(SessionThread.self, using: threadForeignKey)
private static let profile = hasOne(Profile.self, using: profileForeignKey)
public static let profile = hasOne(Profile.self, using: Profile.interactionForeignKey)
internal static let interactionAttachments = hasMany(
InteractionAttachment.self,
using: InteractionAttachment.interactionForeignKey
)
internal static let attachments = hasMany(
public static let attachments = hasMany(
Attachment.self,
through: interactionAttachments,
using: InteractionAttachment.attachment
)
public static let quote = hasOne(Quote.self, using: Quote.interactionForeignKey)
internal static let linkPreview = hasOne(LinkPreview.self, using: LinkPreview.interactionForeignKey)
private static let recipientStates = hasMany(RecipientState.self, using: RecipientState.interactionForeignKey)
/// Whenever using this `linkPreview` association make sure to filter the result using `.filter(literal: Interaction.linkPreviewFilterLiteral)` to ensure the correct LinkPreview is returned
public static let linkPreview = hasOne(LinkPreview.self, using: LinkPreview.interactionForeignKey)
public static let linkPreviewFilterLiteral: SQL = {
let interaction: TypedTableAlias<Interaction> = TypedTableAlias()
let linkPreview: TypedTableAlias<LinkPreview> = TypedTableAlias()
return "(ROUND((\(interaction[.timestampMs]) / 1000 / 100000) - 0.5) * 100000) = \(linkPreview[.timestamp])"
}()
public static let recipientStates = hasMany(RecipientState.self, using: RecipientState.interactionForeignKey)
public typealias Columns = CodingKeys
public enum CodingKeys: String, CodingKey, ColumnExpression {
@ -67,6 +73,20 @@ public struct Interaction: Codable, Identifiable, Equatable, FetchableRecord, Mu
case infoMediaSavedNotification
case infoMessageRequestAccepted = 4000
// MARK: - Convenience
public var isInfoMessage: Bool {
switch self {
case .infoClosedGroupCreated, .infoClosedGroupUpdated, .infoClosedGroupCurrentUserLeft,
.infoDisappearingMessagesUpdate, .infoScreenshotNotification, .infoMediaSavedNotification,
.infoMessageRequestAccepted:
return true
case .standardIncoming, .standardOutgoing, .standardIncomingDeleted:
return false
}
}
}
/// The `id` value is auto incremented by the database, if the `Interaction` hasn't been inserted into
@ -83,6 +103,10 @@ public struct Interaction: Codable, Identifiable, Equatable, FetchableRecord, Mu
public let threadId: String
/// The id of the user who sent the interaction, also used to expose the `profile` variable)
///
/// **Note:** For any "info" messages this value will always be the current user public key (this is because these
/// messages are created locally based on control messages and the initiator of a control message doesn't always
/// get transmitted)
public let authorId: String
/// The type of interaction
@ -156,20 +180,10 @@ public struct Interaction: Codable, Identifiable, Equatable, FetchableRecord, Mu
}
public var linkPreview: QueryInterfaceRequest<LinkPreview> {
let linkPreviewAlias: TableAlias = TableAlias()
return LinkPreview
.aliased(linkPreviewAlias)
.joining(
required: LinkPreview.interactions
.filter(literal: [
"(ROUND((\(Interaction.Columns.timestampMs) / 1000 / 100000) - 0.5) * 100000)",
"=",
"\(linkPreviewAlias[LinkPreview.Columns.timestamp])"
].joined(separator: " "))
.limit(1) // Avoid joining to multiple interactions
)
.limit(1) // Avoid joining to multiple interactions
/// **Note:** This equation **MUST** match the `linkPreviewFilterLiteral` logic
let roundedTimestamp: Double = (round(((Double(timestampMs) / 1000) / 100000) - 0.5) * 100000)
return request(for: Interaction.linkPreview)
.filter(LinkPreview.Columns.timestamp == roundedTimestamp)
}
public var recipientStates: QueryInterfaceRequest<RecipientState> {
@ -320,11 +334,7 @@ public struct Interaction: Codable, Identifiable, Equatable, FetchableRecord, Mu
.aliased(interactionAlias)
.joining(
required: Interaction.linkPreview
.filter(literal: [
"(ROUND((\(interactionAlias[Columns.timestampMs]) / 1000 / 100000) - 0.5) * 100000)",
"=",
"\(LinkPreview.Columns.timestamp)"
].joined(separator: " "))
.filter(literal: Interaction.linkPreviewFilterLiteral)
)
.fetchCount(db)
let tmp = try linkPreview.fetchAll(db)
@ -449,7 +459,7 @@ public extension Interaction {
scheduleJobs(
interactionIds: try Int64.fetchAll(
db,
interactionQuery.select(Interaction.Columns.id)
interactionQuery.select(.id)
)
)
}
@ -538,10 +548,10 @@ public extension Interaction {
)
}
/// Use the `Interaction.previewText` method directly where possible rather than this method as it
/// makes it's own database queries
func previewText(_ db: Database) -> String {
switch variant {
case .standardIncomingDeleted: return ""
case .standardIncoming, .standardOutgoing:
struct AttachmentDescriptionInfo: Decodable, FetchableRecord {
let id: String
@ -549,36 +559,19 @@ public extension Interaction {
let contentType: String
let sourceFilename: String?
}
var bodyDescription: String?
if let body: String = self.body, !body.isEmpty {
bodyDescription = body
}
if bodyDescription == nil {
struct AttachmentBodyInfo: Decodable, FetchableRecord {
let id: String
let variant: Attachment.Variant
let contentType: String
let sourceFilename: String?
}
var targetBody: String? = self.body
if self.body == nil || self.body?.isEmpty == true {
let maybeTextInfo: AttachmentDescriptionInfo? = try? AttachmentDescriptionInfo
.fetchOne(
db,
attachments
.select(
Attachment.Columns.id,
Attachment.Columns.state,
Attachment.Columns.variant,
Attachment.Columns.contentType,
Attachment.Columns.sourceFilename
)
.select(.id, .state, .variant, .contentType, .sourceFilename)
.filter(Attachment.Columns.contentType == OWSMimeTypeOversizeTextMessage)
.filter(Attachment.Columns.state == Attachment.State.downloaded)
)
if
let textInfo: AttachmentDescriptionInfo = maybeTextInfo,
let filePath: String = Attachment.originalFilePath(
@ -589,20 +582,15 @@ public extension Interaction {
let data: Data = try? Data(contentsOf: URL(fileURLWithPath: filePath)),
let dataString: String = String(data: data, encoding: .utf8)
{
bodyDescription = dataString.filterForDisplay
targetBody = dataString.filterForDisplay
}
}
let attachmentDescription: String? = try? AttachmentDescriptionInfo
.fetchOne(
db,
attachments
.select(
Attachment.Columns.id,
Attachment.Columns.variant,
Attachment.Columns.contentType,
Attachment.Columns.sourceFilename
)
.select(.id, .variant, .contentType, .sourceFilename)
.filter(Attachment.Columns.contentType != OWSMimeTypeOversizeTextMessage)
)
.map { info -> String in
@ -612,6 +600,70 @@ public extension Interaction {
sourceFilename: info.sourceFilename
)
}
let isOpenGroupInvitation: Bool = (try? linkPreview
.filter(LinkPreview.Columns.variant == LinkPreview.Variant.openGroupInvitation)
.isNotEmpty(db))
.defaulting(to: false)
return Interaction.previewText(
variant: self.variant,
body: targetBody,
attachments: [],
customAttachmentDescription: attachmentDescription,
isOpenGroupInvitation: isOpenGroupInvitation
)
case .infoMediaSavedNotification, .infoScreenshotNotification:
// Note: This should only occur in 'contact' threads so the `threadId`
// is the contact id
return Interaction.previewText(
variant: self.variant,
body: self.body,
authorDisplayName: Profile.displayName(db, id: threadId),
attachments: []
)
default: return Interaction.previewText(
variant: self.variant,
body: self.body,
attachments: []
)
}
}
/// This menthod generates the preview text for a given transaction
static func previewText(
variant: Variant,
body: String?,
authorDisplayName: String = "",
attachments: [Attachment],
customAttachmentDescription: String? = nil,
isOpenGroupInvitation: Bool = false
) -> String {
switch variant {
case .standardIncomingDeleted: return ""
case .standardIncoming, .standardOutgoing:
var bodyDescription: String?
let attachmentDescription: String? = (customAttachmentDescription ?? attachments
.first(where: { $0.contentType != OWSMimeTypeOversizeTextMessage })?
.description
)
if let body: String = body, !body.isEmpty {
bodyDescription = body
}
else if
let textAttachment: Attachment = attachments.first(where: { attachment in
attachment.state == .downloaded &&
attachment.contentType == OWSMimeTypeOversizeTextMessage
}),
let filePath: String = textAttachment.originalFilePath,
let data: Data = try? Data(contentsOf: URL(fileURLWithPath: filePath)),
let dataString: String = String(data: data, encoding: .utf8)
{
bodyDescription = dataString.filterForDisplay
}
if
let attachmentDescription: String = attachmentDescription,
@ -634,7 +686,7 @@ public extension Interaction {
return attachmentDescription
}
if let linkPreview: LinkPreview = try? linkPreview.fetchOne(db), linkPreview.variant == .openGroupInvitation {
if isOpenGroupInvitation {
return "😎 Open group invitation"
}
@ -642,19 +694,11 @@ public extension Interaction {
return ""
case .infoMediaSavedNotification:
// Note: This should only occur in 'contact' threads so the `threadId`
// is the contact id
let displayName: String = Profile.displayName(id: threadId)
// TODO: Use referencedAttachmentTimestamp to tell the user * which * media was saved
return String(format: "media_saved".localized(), displayName)
return String(format: "media_saved".localized(), authorDisplayName)
case .infoScreenshotNotification:
// Note: This should only occur in 'contact' threads so the `threadId`
// is the contact id
let displayName: String = Profile.displayName(id: threadId)
return String(format: "screenshot_taken".localized(), displayName)
return String(format: "screenshot_taken".localized(), authorDisplayName)
case .infoClosedGroupCreated: return "GROUP_CREATED".localized()
case .infoClosedGroupCurrentUserLeft: return "GROUP_YOU_LEFT".localized()
@ -662,8 +706,15 @@ public extension Interaction {
case .infoMessageRequestAccepted: return (body ?? "MESSAGE_REQUESTS_ACCEPTED".localized())
case .infoDisappearingMessagesUpdate:
// TODO: We should do better here
return (body ?? "")
guard
let infoMessageData: Data = (body ?? "").data(using: .utf8),
let messageInfo: DisappearingMessagesConfiguration.MessageInfo = try? JSONDecoder().decode(
DisappearingMessagesConfiguration.MessageInfo.self,
from: infoMessageData
)
else { return (body ?? "") }
return messageInfo.previewText
}
}
@ -671,9 +722,17 @@ public extension Interaction {
let states: [RecipientState.State] = try RecipientState.State
.fetchAll(
db,
recipientStates
.select(RecipientState.Columns.state)
recipientStates.select(.state)
)
return Interaction.state(for: states)
}
static func state(for states: [RecipientState.State]) -> RecipientState.State {
// If there are no states then assume this is a new interaction which hasn't been
// saved yet so has no states
guard !states.isEmpty else { return .sending }
var hasFailed: Bool = false
for state in states {

View File

@ -7,7 +7,7 @@ import SessionUtilitiesKit
public struct InteractionAttachment: Codable, FetchableRecord, PersistableRecord, TableRecord, ColumnExpressible {
public static var databaseTableName: String { "interactionAttachment" }
internal static let interactionForeignKey = ForeignKey([Columns.interactionId], to: [Interaction.Columns.id])
private static let attachmentForeignKey = ForeignKey([Columns.attachmentId], to: [Attachment.Columns.id])
internal static let attachmentForeignKey = ForeignKey([Columns.attachmentId], to: [Attachment.Columns.id])
internal static let interaction = belongsTo(Interaction.self, using: interactionForeignKey)
internal static let attachment = belongsTo(Attachment.self, using: attachmentForeignKey)
@ -36,11 +36,11 @@ public struct InteractionAttachment: Codable, FetchableRecord, PersistableRecord
// If we have an Attachment then check if this is the only type that is referencing it
// and delete the Attachment if so
let quoteUses: Int? = try? Quote
.select(Quote.Columns.attachmentId)
.select(.attachmentId)
.filter(Quote.Columns.attachmentId == attachmentId)
.fetchCount(db)
let linkPreviewUses: Int? = try? LinkPreview
.select(LinkPreview.Columns.attachmentId)
.select(.attachmentId)
.filter(LinkPreview.Columns.attachmentId == attachmentId)
.fetchCount(db)

View File

@ -4,15 +4,14 @@ import Foundation
import GRDB
import SessionUtilitiesKit
public struct LinkPreview: Codable, FetchableRecord, PersistableRecord, TableRecord, ColumnExpressible {
public struct LinkPreview: Codable, Equatable, FetchableRecord, PersistableRecord, TableRecord, ColumnExpressible {
public static var databaseTableName: String { "linkPreview" }
internal static let interactionForeignKey = ForeignKey(
[Columns.url],
to: [Interaction.Columns.linkPreviewUrl]
)
private static let attachmentForeignKey = ForeignKey([Columns.attachmentId], to: [Attachment.Columns.id])
internal static let interactions = hasMany(Interaction.self, using: Interaction.linkPreviewForeignKey)
internal static let attachment = hasOne(Attachment.self, using: attachmentForeignKey)
public static let attachment = hasOne(Attachment.self, using: Attachment.linkPreviewForeignKey)
/// We want to cache url previews to the nearest 100,000 seconds (~28 hours - simpler than 86,400) to ensure the user isn't shown a preview that is too stale
internal static let timstampResolution: Double = 100000

View File

@ -82,14 +82,14 @@ public struct OpenGroup: Codable, Identifiable, FetchableRecord, PersistableReco
// MARK: - Initialization
init(
public init(
server: String,
room: String,
publicKey: String,
name: String,
groupDescription: String?,
imageId: Int?,
imageData: Data?,
groupDescription: String? = nil,
imageId: Int? = nil,
imageData: Data? = nil,
userCount: Int,
infoUpdates: Int
) {

View File

@ -5,8 +5,11 @@ import GRDB
import SignalCoreKit
import SessionUtilitiesKit
public struct Profile: Codable, Identifiable, FetchableRecord, PersistableRecord, TableRecord, ColumnExpressible, CustomStringConvertible {
public struct Profile: Codable, Identifiable, Equatable, FetchableRecord, PersistableRecord, TableRecord, ColumnExpressible, CustomStringConvertible {
public static var databaseTableName: String { "profile" }
internal static let interactionForeignKey = ForeignKey([Columns.id], to: [Interaction.Columns.authorId])
internal static let contactForeignKey = ForeignKey([Columns.id], to: [Contact.Columns.id])
public static let groupMembers = hasMany(GroupMember.self, using: GroupMember.profileForeignKey)
public typealias Columns = CodingKeys
public enum CodingKeys: String, CodingKey, ColumnExpression {
@ -38,6 +41,24 @@ public struct Profile: Codable, Identifiable, FetchableRecord, PersistableRecord
/// The key with which the profile is encrypted.
public let profileEncryptionKey: OWSAES256Key?
// MARK: - Initialization
public init(
id: String,
name: String,
nickname: String? = nil,
profilePictureUrl: String? = nil,
profilePictureFileName: String? = nil,
profileEncryptionKey: OWSAES256Key? = nil
) {
self.id = id
self.name = name
self.nickname = nickname
self.profilePictureUrl = profilePictureUrl
self.profilePictureFileName = profilePictureFileName
self.profileEncryptionKey = profileEncryptionKey
}
// MARK: - Description
public var description: String {
@ -177,7 +198,7 @@ public extension Profile {
}
}
// MARK: - Convenience
// MARK: - Mutation
public extension Profile {
func with(
@ -196,29 +217,6 @@ public extension Profile {
profileEncryptionKey: (profileEncryptionKey ?? self.profileEncryptionKey)
)
}
// MARK: - Context
@objc enum Context: Int {
case regular
case openGroup
}
/// The name to display in the UI. For local use only.
func displayName(for context: Context = .regular) -> String {
if let nickname: String = nickname { return nickname }
switch context {
case .regular: return name
case .openGroup:
// In open groups, where it's more likely that multiple users have the same name, we display a bit of the Session ID after
// a user's display name for added context.
let endIndex = id.endIndex
let cutoffIndex = id.index(endIndex, offsetBy: -8)
return "\(name) (...\(id[cutoffIndex..<endIndex]))"
}
}
}
// MARK: - GRDB Interactions
@ -313,6 +311,65 @@ public extension Profile {
}
}
// MARK: - Convenience
public extension Profile {
// MARK: - Context
@objc enum Context: Int {
case regular
case openGroup
}
// MARK: - Truncation
enum Truncation {
case start
case middle
case end
}
/// A standardised mechanism for truncating a user id for a given thread
static func truncated(id: String, thread: SessionThread) -> String {
switch thread.variant {
case .openGroup: return truncated(id: id, truncating: .start)
default: return truncated(id: id, truncating: .middle)
}
}
/// A standardised mechanism for truncating a user id
static func truncated(id: String, truncating: Truncation = .start) -> String {
guard id.count > 8 else { return id }
switch truncating {
case .start: return "...\(id.suffix(8))"
case .middle: return "\(id.prefix(4))...\(id.suffix(4))"
case .end: return "\(id.prefix(8))..."
}
}
/// The name to display in the UI for a given thread variant
func displayName(for threadVariant: SessionThread.Variant) -> String {
return displayName(
for: (threadVariant == .openGroup ? .openGroup : .regular)
)
}
/// The name to display in the UI
func displayName(for context: Context = .regular) -> String {
if let nickname: String = nickname { return nickname }
switch context {
case .regular: return name
case .openGroup:
// In open groups, where it's more likely that multiple users have the same name,
// we display a bit of the Session ID after a user's display name for added context
return "\(name) (\(Profile.truncated(id: id, truncating: .start)))"
}
}
}
// MARK: - Objective-C Support
@objc(SMKProfile)
public class SMKProfile: NSObject {

View File

@ -4,19 +4,18 @@ import Foundation
import GRDB
import SessionUtilitiesKit
public struct Quote: Codable, FetchableRecord, PersistableRecord, TableRecord, ColumnExpressible {
public struct Quote: Codable, Equatable, FetchableRecord, PersistableRecord, TableRecord, ColumnExpressible {
public static var databaseTableName: String { "quote" }
internal static let interactionForeignKey = ForeignKey([Columns.interactionId], to: [Interaction.Columns.id])
public static let interactionForeignKey = ForeignKey([Columns.interactionId], to: [Interaction.Columns.id])
internal static let originalInteractionForeignKey = ForeignKey(
[Columns.timestampMs, Columns.authorId],
to: [Interaction.Columns.timestampMs, Interaction.Columns.authorId]
)
internal static let profileForeignKey = ForeignKey([Columns.authorId], to: [Profile.Columns.id])
private static let attachmentForeignKey = ForeignKey([Columns.attachmentId], to: [Attachment.Columns.id])
internal static let interaction = belongsTo(Interaction.self, using: interactionForeignKey)
private static let profile = hasOne(Profile.self, using: profileForeignKey)
private static let quotedInteraction = hasOne(Interaction.self, using: originalInteractionForeignKey)
internal static let attachment = hasOne(Attachment.self, using: attachmentForeignKey)
public static let attachment = hasOne(Attachment.self, using: Attachment.quoteForeignKey)
public typealias Columns = CodingKeys
public enum CodingKeys: String, CodingKey, ColumnExpression {

View File

@ -2,6 +2,7 @@
import Foundation
import GRDB
import SignalCoreKit
import SessionUtilitiesKit
public struct RecipientState: Codable, Equatable, FetchableRecord, PersistableRecord, TableRecord, ColumnExpressible {
@ -25,12 +26,38 @@ public struct RecipientState: Codable, Equatable, FetchableRecord, PersistableRe
case sending
case skipped
case sent
func message(hasAttachments: Bool, hasAtLeastOneReadReceipt: Bool) -> String {
switch self {
case .failed: return "MESSAGE_STATUS_FAILED".localized()
case .sending:
guard hasAttachments else {
return "MESSAGE_STATUS_SENDING".localized()
}
return "MESSAGE_STATUS_UPLOADING".localized()
case .sent:
guard hasAtLeastOneReadReceipt else {
return "MESSAGE_STATUS_SENT".localized()
}
return "MESSAGE_STATUS_READ".localized()
default:
owsFailDebug("Message has unexpected status: \(self).")
return "MESSAGE_STATUS_SENT".localized()
}
}
}
/// The id for the interaction this state belongs to
public let interactionId: Int64
/// The id for the recipient this state belongs to
/// The id for the recipient that has this state
///
/// **Note:** For contact and closedGroup threads this can be used as a lookup for a contact/profile but in an
/// openGroup thread this will be the threadId so wont resolve to a contact/profile
public let recipientId: String
/// The current state for the recipient

View File

@ -96,7 +96,6 @@ public struct SessionThread: Codable, Identifiable, Equatable, FetchableRecord,
request(for: SessionThread.interactions)
}
// MARK: - Initialization
public init(
@ -133,6 +132,26 @@ public struct SessionThread: Codable, Identifiable, Equatable, FetchableRecord,
}
}
// MARK: - Mutation
public extension SessionThread {
func with(
shouldBeVisible: Bool? = nil
) -> SessionThread {
return SessionThread(
id: id,
variant: variant,
creationDateTimestamp: creationDateTimestamp,
shouldBeVisible: (shouldBeVisible ?? self.shouldBeVisible),
isPinned: isPinned,
messageDraft: messageDraft,
notificationMode: notificationMode,
notificationSound: notificationSound,
mutedUntilTimestamp: mutedUntilTimestamp
)
}
}
// MARK: - GRDB Interactions
public extension SessionThread {
@ -174,21 +193,36 @@ public extension SessionThread {
func name(_ db: Database) -> String {
switch variant {
case .contact: return Profile.displayName(db, id: id)
case .contact:
guard !isNoteToSelf(db) else { return name(isNoteToSelf: true) }
return name(
displayName: Profile.displayName(
db,
id: id,
customFallback: Profile.truncated(id: id, truncating: .middle)
)
)
case .closedGroup:
guard let name: String = try? String.fetchOne(db, closedGroup.select(ClosedGroup.Columns.name)), !name.isEmpty else {
return "Group"
}
return name
return name(displayName: try? String.fetchOne(db, closedGroup.select(.name)))
case .openGroup:
guard let name: String = try? String.fetchOne(db, openGroup.select(OpenGroup.Columns.name)), !name.isEmpty else {
return "Group"
}
return name(displayName: try? String.fetchOne(db, openGroup.select(.name)))
}
}
func name(isNoteToSelf: Bool = false, displayName: String? = nil) -> String {
switch variant {
case .contact:
guard !isNoteToSelf else { return "Note to Self" }
return name
return displayName
.defaulting(to: "Anonymous", useDefaultIfEmpty: true)
case .closedGroup, .openGroup:
return displayName
.defaulting(to: "Group", useDefaultIfEmpty: true)
}
}
}

View File

@ -1,8 +1,7 @@
//
// Copyright (c) 2019 Open Whisper Systems. All rights reserved.
//
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import Foundation
import SessionUtilitiesKit
@objc
public class SSKPreferences: NSObject {
@ -65,6 +64,11 @@ public extension SSKPreferences {
GRDBStorage.shared.write { db in db[.preferencesAppSwitcherPreviewEnabled] = enabled }
}
@objc(areReadReceiptsEnabled)
static func objc_areReadReceiptsEnabled() -> Bool {
return GRDBStorage.shared[.areReadReceiptsEnabled]
}
@objc(setAreReadReceiptsEnabled:)
static func objc_setAreReadReceiptsEnabled(_ enabled: Bool) {
GRDBStorage.shared.write { db in db[.areReadReceiptsEnabled] = enabled }

View File

@ -28,7 +28,8 @@ public enum AttachmentDownloadJob: JobExecutor {
return
}
guard attachment.state != .downloaded else {
// FIXME: It's not clear * how * this happens, but apparently we can get to this point from time to time with an already downloaded attachment.
// If there is a bug elsewhere in the code it's possible for an AttachmentDownloadJob to be created
// for an attachment that is already downloaded - if it is just succeed immediately
success(job, false)
return
}
@ -133,6 +134,10 @@ public enum AttachmentDownloadJob: JobExecutor {
extension AttachmentDownloadJob {
public struct Details: Codable {
public let attachmentId: String
public init(attachmentId: String) {
self.attachmentId = attachmentId
}
}
public enum AttachmentDownloadError: LocalizedError {

View File

@ -22,7 +22,7 @@ public enum DisappearingMessagesJob: JobExecutor {
let updatedJob: Job? = GRDBStorage.shared.write { db in
_ = try Interaction
.filter(Interaction.Columns.expiresStartedAtMs != nil)
.filter(sql: "(\(Interaction.Columns.expiresStartedAtMs) + (\(Interaction.Columns.expiresInSeconds) * 1000) <= \(timestampNowMs)")
.filter((Interaction.Columns.expiresStartedAtMs + (Interaction.Columns.expiresInSeconds * 1000)) <= timestampNowMs)
.deleteAll(db)
// Update the next run timestamp for the DisappearingMessagesJob (if the call
@ -55,8 +55,8 @@ public extension DisappearingMessagesJob {
.fetchOne(
db,
Interaction
.select(sql: "(\(Interaction.Columns.expiresStartedAtMs) + (\(Interaction.Columns.expiresInSeconds) * 1000)")
.order(sql: "(\(Interaction.Columns.expiresStartedAtMs) + (\(Interaction.Columns.expiresInSeconds) * 1000) asc")
.select(Interaction.Columns.expiresStartedAtMs + (Interaction.Columns.expiresInSeconds * 1000))
.order((Interaction.Columns.expiresStartedAtMs + (Interaction.Columns.expiresInSeconds * 1000)).asc)
)
guard let nextExpirationTimestampMs: Double = nextExpirationTimestampMs else { return nil }

View File

@ -1,6 +1,7 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import Foundation
import GRDB
import PromiseKit
import SignalCoreKit
import SessionUtilitiesKit
@ -36,46 +37,21 @@ public enum MessageSendJob: JobExecutor {
return
}
var shouldDeferJob: Bool = false
var shouldFailJob: Bool = false
GRDBStorage.shared.read { db in
// Fetch all associated attachments
let attachments: [Attachment] = try interaction.attachments.fetchAll(db)
let attachmentCount: Int = try interaction.attachments
.filter(Attachment.Columns.state == Attachment.State.pending)
.fetchCount(db)
// Create jobs for any pending attachment jobs and insert them into the
// queue before the current job (this will mean the current job will re-run
// after these inserted jobs complete)
let pendingAttachments: [Attachment] = attachments.filter { $0.state == .pending }
pendingAttachments
.forEach { attachment in
JobRunner.insert(
db,
job: Job(
variant: .attachmentUpload,
behaviour: .runOnce,
threadId: job.threadId,
details: AttachmentUploadJob.Details(
threadId: threadId,
attachmentId: attachment.id,
messageSendJobId: jobId
)
),
before: job
)
}
// If there were pending or uploading attachments then stop here (we want to
// upload them first and then re-run this send job - the 'JobRunner.insert'
// method will take care of this)
shouldDeferJob = (
!pendingAttachments.isEmpty ||
attachments.contains(where: { $0.state == .uploading })
)
shouldFailJob = (attachmentCount > 0)
}
// Only continue if we don't want to defer the job
guard !shouldDeferJob else {
deferred(job)
// Cannot send messages with pending attachments (the app doesn't currently
// support deferred attachment uploads)
guard !shouldFailJob else {
failure(job, Attachment.UploadError.notUploaded, true)
return
}
}

View File

@ -49,13 +49,35 @@ public enum SendReadReceiptsJob: JobExecutor {
// When we complete the 'SendReadReceiptsJob' we want to immediately schedule
// another one for the same thread but with a 'nextRunTimestamp' set to the
// 'minRunFrequency' value to throttle the read receipt requests
GRDBStorage.shared.write { db in
_ = try createOrUpdateIfNeeded(db, threadId: threadId, interactionIds: [])?
.with(nextRunTimestamp: (Date().timeIntervalSince1970 + minRunFrequency))
var shouldFinishCurrentJob: Bool = false
let nextRunTimestamp: TimeInterval = (Date().timeIntervalSince1970 + minRunFrequency)
let updatedJob: Job? = GRDBStorage.shared.write { db in
// If another 'sendReadReceipts' job was scheduled then update that one
// to run at 'nextRunTimestamp' and make the current job stop
if
let existingJob: Job = try? Job
.filter(Job.Columns.id != job.id)
.filter(Job.Columns.variant == Job.Variant.sendReadReceipts)
.filter(Job.Columns.threadId == threadId)
.fetchOne(db),
!JobRunner.isCurrentlyRunning(existingJob)
{
_ = try existingJob
.with(nextRunTimestamp: nextRunTimestamp)
.saved(db)
shouldFinishCurrentJob = true
return job
}
return try job
.with(details: Details(destination: details.destination, timestampMsValues: []))
.defaulting(to: job)
.with(nextRunTimestamp: nextRunTimestamp)
.saved(db)
}
success(job, false)
success(updatedJob ?? job, shouldFinishCurrentJob)
}
.catch { error in failure(job, error, false) }
.retainUntilComplete()
@ -82,7 +104,7 @@ public extension SendReadReceiptsJob {
let maybeTimestampMsValues: [Int64]? = try? Int64.fetchAll(
db,
Interaction
.select(Interaction.Columns.timestampMs)
.select(.timestampMs)
.filter(interactionIds.contains(Interaction.Columns.id))
// Only `standardIncoming` incoming interactions should have read receipts sent
.filter(Interaction.Columns.variant == Interaction.Variant.standardIncoming)

View File

@ -39,7 +39,7 @@ public enum UpdateProfilePictureJob: JobExecutor {
profileName: profile.name,
avatarImage: profilePicture,
requiredSync: true,
success: { success(job, false) },
success: { _ in success(job, false) },
failure: { error in failure(job, error, false) }
)
}

View File

@ -6,7 +6,7 @@ import Sodium
import Curve25519Kit
import SessionUtilitiesKit
public final class ClosedGroupControlMessage : ControlMessage {
public final class ClosedGroupControlMessage: ControlMessage {
private enum CodingKeys: String, CodingKey {
case kind
}
@ -22,7 +22,8 @@ public final class ClosedGroupControlMessage : ControlMessage {
public override var isSelfSendValid: Bool { true }
// MARK: Kind
// MARK: - Kind
public enum Kind: CustomStringConvertible, Codable {
private enum CodingKeys: String, CodingKey {
case description
@ -49,13 +50,13 @@ public final class ClosedGroupControlMessage : ControlMessage {
public var description: String {
switch self {
case .new: return "new"
case .encryptionKeyPair: return "encryptionKeyPair"
case .nameChange: return "nameChange"
case .membersAdded: return "membersAdded"
case .membersRemoved: return "membersRemoved"
case .memberLeft: return "memberLeft"
case .encryptionKeyPairRequest: return "encryptionKeyPairRequest"
case .new: return "new"
case .encryptionKeyPair: return "encryptionKeyPair"
case .nameChange: return "nameChange"
case .membersAdded: return "membersAdded"
case .membersRemoved: return "membersRemoved"
case .memberLeft: return "memberLeft"
case .encryptionKeyPairRequest: return "encryptionKeyPairRequest"
}
}
@ -150,9 +151,9 @@ public final class ClosedGroupControlMessage : ControlMessage {
}
}
// MARK: Key Pair Wrapper
@objc(SNKeyPairWrapper)
public final class KeyPairWrapper: NSObject, Codable, NSCoding { // NSObject/NSCoding conformance is needed for YapDatabase compatibility
// MARK: - Key Pair Wrapper
public struct KeyPairWrapper: Codable {
public var publicKey: String?
public var encryptedKeyPair: Data?
@ -162,16 +163,8 @@ public final class ClosedGroupControlMessage : ControlMessage {
self.publicKey = publicKey
self.encryptedKeyPair = encryptedKeyPair
}
public required init?(coder: NSCoder) {
if let publicKey = coder.decodeObject(forKey: "publicKey") as! String? { self.publicKey = publicKey }
if let encryptedKeyPair = coder.decodeObject(forKey: "encryptedKeyPair") as! Data? { self.encryptedKeyPair = encryptedKeyPair }
}
public func encode(with coder: NSCoder) {
coder.encode(publicKey, forKey: "publicKey")
coder.encode(encryptedKeyPair, forKey: "encryptedKeyPair")
}
// MARK: - Proto Conversion
public static func fromProto(_ proto: SNProtoDataMessageClosedGroupControlMessageKeyPairWrapper) -> KeyPairWrapper? {
return KeyPairWrapper(publicKey: proto.publicKey.toHexString(), encryptedKeyPair: proto.encryptedKeyPair)
@ -189,97 +182,36 @@ public final class ClosedGroupControlMessage : ControlMessage {
}
}
// MARK: Initialization
public override init() { super.init() }
// MARK: - Initialization
internal init(kind: Kind) {
super.init()
self.kind = kind
}
// MARK: Validation
// MARK: - Validation
public override var isValid: Bool {
guard super.isValid, let kind = kind else { return false }
switch kind {
case .new(let publicKey, let name, let encryptionKeyPair, let members, let admins, let expirationTimer):
return !publicKey.isEmpty && !name.isEmpty && !encryptionKeyPair.publicKey.isEmpty
&& !encryptionKeyPair.secretKey.isEmpty && !members.isEmpty && !admins.isEmpty
case .encryptionKeyPair: return true
case .nameChange(let name): return !name.isEmpty
case .membersAdded(let members): return !members.isEmpty
case .membersRemoved(let members): return !members.isEmpty
case .memberLeft: return true
case .encryptionKeyPairRequest: return true
}
}
// MARK: Coding
public required init?(coder: NSCoder) {
super.init(coder: coder)
guard let rawKind = coder.decodeObject(forKey: "kind") as? String else { return nil }
switch rawKind {
case "new":
guard let publicKey = coder.decodeObject(forKey: "publicKey") as? Data,
let name = coder.decodeObject(forKey: "name") as? String,
let encryptionKeyPair = coder.decodeObject(forKey: "encryptionKeyPair") as? SessionUtilitiesKit.Legacy.KeyPair,
let members = coder.decodeObject(forKey: "members") as? [Data],
let admins = coder.decodeObject(forKey: "admins") as? [Data] else { return nil }
let expirationTimer = coder.decodeObject(forKey: "expirationTimer") as? UInt32 ?? 0
let keyPair: Box.KeyPair = Box.KeyPair(
publicKey: encryptionKeyPair.publicKey.bytes,
secretKey: encryptionKeyPair.privateKey.bytes
)
self.kind = .new(publicKey: publicKey, name: name, encryptionKeyPair: keyPair, members: members, admins: admins, expirationTimer: expirationTimer)
case "encryptionKeyPair":
let publicKey = coder.decodeObject(forKey: "publicKey") as? Data
guard let wrappers = coder.decodeObject(forKey: "wrappers") as? [KeyPairWrapper] else { return nil }
self.kind = .encryptionKeyPair(publicKey: publicKey, wrappers: wrappers)
case "nameChange":
guard let name = coder.decodeObject(forKey: "name") as? String else { return nil }
self.kind = .nameChange(name: name)
case "membersAdded":
guard let members = coder.decodeObject(forKey: "members") as? [Data] else { return nil }
self.kind = .membersAdded(members: members)
case "membersRemoved":
guard let members = coder.decodeObject(forKey: "members") as? [Data] else { return nil }
self.kind = .membersRemoved(members: members)
case "memberLeft":
self.kind = .memberLeft
case "encryptionKeyPairRequest":
self.kind = .encryptionKeyPairRequest
default: return nil
}
}
public override func encode(with coder: NSCoder) {
super.encode(with: coder)
guard let kind = kind else { return }
switch kind {
case .new(let publicKey, let name, let encryptionKeyPair, let members, let admins, let expirationTimer):
coder.encode("new", forKey: "kind")
coder.encode(publicKey, forKey: "publicKey")
coder.encode(name, forKey: "name")
coder.encode(encryptionKeyPair, forKey: "encryptionKeyPair")
coder.encode(members, forKey: "members")
coder.encode(admins, forKey: "admins")
coder.encode(expirationTimer, forKey: "expirationTimer")
case .encryptionKeyPair(let publicKey, let wrappers):
coder.encode("encryptionKeyPair", forKey: "kind")
coder.encode(publicKey, forKey: "publicKey")
coder.encode(wrappers, forKey: "wrappers")
case .nameChange(let name):
coder.encode("nameChange", forKey: "kind")
coder.encode(name, forKey: "name")
case .membersAdded(let members):
coder.encode("membersAdded", forKey: "kind")
coder.encode(members, forKey: "members")
case .membersRemoved(let members):
coder.encode("membersRemoved", forKey: "kind")
coder.encode(members, forKey: "members")
case .memberLeft:
coder.encode("memberLeft", forKey: "kind")
case .encryptionKeyPairRequest:
coder.encode("encryptionKeyPairRequest", forKey: "kind")
case .new(let publicKey, let name, let encryptionKeyPair, let members, let admins, _):
return (
!publicKey.isEmpty &&
!name.isEmpty &&
!encryptionKeyPair.publicKey.isEmpty &&
!encryptionKeyPair.secretKey.isEmpty &&
!members.isEmpty &&
!admins.isEmpty
)
case .encryptionKeyPair: return true
case .nameChange(let name): return !name.isEmpty
case .membersAdded(let members): return !members.isEmpty
case .membersRemoved(let members): return !members.isEmpty
case .memberLeft: return true
case .encryptionKeyPairRequest: return true
}
}
@ -301,35 +233,64 @@ public final class ClosedGroupControlMessage : ControlMessage {
try container.encode(kind, forKey: .kind)
}
// MARK: Proto Conversion
// MARK: - Proto Conversion
public override class func fromProto(_ proto: SNProtoContent, sender: String) -> ClosedGroupControlMessage? {
guard let closedGroupControlMessageProto = proto.dataMessage?.closedGroupControlMessage else { return nil }
let kind: Kind
switch closedGroupControlMessageProto.type {
case .new:
guard let publicKey = closedGroupControlMessageProto.publicKey, let name = closedGroupControlMessageProto.name,
let encryptionKeyPairAsProto = closedGroupControlMessageProto.encryptionKeyPair else { return nil }
let expirationTimer = closedGroupControlMessageProto.expirationTimer
let encryptionKeyPair = Box.KeyPair(publicKey: encryptionKeyPairAsProto.publicKey.removing05PrefixIfNeeded().bytes, secretKey: encryptionKeyPairAsProto.privateKey.bytes)
kind = .new(publicKey: publicKey, name: name, encryptionKeyPair: encryptionKeyPair,
members: closedGroupControlMessageProto.members, admins: closedGroupControlMessageProto.admins, expirationTimer: expirationTimer)
case .encryptionKeyPair:
let publicKey = closedGroupControlMessageProto.publicKey
let wrappers = closedGroupControlMessageProto.wrappers.compactMap { KeyPairWrapper.fromProto($0) }
kind = .encryptionKeyPair(publicKey: publicKey, wrappers: wrappers)
case .nameChange:
guard let name = closedGroupControlMessageProto.name else { return nil }
kind = .nameChange(name: name)
case .membersAdded:
kind = .membersAdded(members: closedGroupControlMessageProto.members)
case .membersRemoved:
kind = .membersRemoved(members: closedGroupControlMessageProto.members)
case .memberLeft:
kind = .memberLeft
case .encryptionKeyPairRequest:
kind = .encryptionKeyPairRequest
guard let closedGroupControlMessageProto = proto.dataMessage?.closedGroupControlMessage else {
return nil
}
switch closedGroupControlMessageProto.type {
case .new:
guard
let publicKey = closedGroupControlMessageProto.publicKey,
let name = closedGroupControlMessageProto.name,
let encryptionKeyPairAsProto = closedGroupControlMessageProto.encryptionKeyPair
else { return nil }
return ClosedGroupControlMessage(
kind: .new(
publicKey: publicKey,
name: name,
encryptionKeyPair: Box.KeyPair(
publicKey: encryptionKeyPairAsProto.publicKey.removing05PrefixIfNeeded().bytes,
secretKey: encryptionKeyPairAsProto.privateKey.bytes
),
members: closedGroupControlMessageProto.members,
admins: closedGroupControlMessageProto.admins,
expirationTimer: closedGroupControlMessageProto.expirationTimer
)
)
case .encryptionKeyPair:
return ClosedGroupControlMessage(
kind: .encryptionKeyPair(
publicKey: closedGroupControlMessageProto.publicKey,
wrappers: closedGroupControlMessageProto.wrappers
.compactMap { KeyPairWrapper.fromProto($0) }
)
)
case .nameChange:
guard let name = closedGroupControlMessageProto.name else { return nil }
return ClosedGroupControlMessage(kind: .nameChange(name: name))
case .membersAdded:
return ClosedGroupControlMessage(
kind: .membersAdded(members: closedGroupControlMessageProto.members)
)
case .membersRemoved:
return ClosedGroupControlMessage(
kind: .membersRemoved(members: closedGroupControlMessageProto.members)
)
case .memberLeft: return ClosedGroupControlMessage(kind: .memberLeft)
case .encryptionKeyPairRequest:
return ClosedGroupControlMessage(kind: .encryptionKeyPairRequest)
}
return ClosedGroupControlMessage(kind: kind)
}
public override func toProto(_ db: Database) -> SNProtoContent? {
@ -387,8 +348,9 @@ public final class ClosedGroupControlMessage : ControlMessage {
}
}
// MARK: Description
public override var description: String {
// MARK: - Description
public var description: String {
"""
ClosedGroupControlMessage(
kind: \(kind?.description ?? "null")

View File

@ -12,7 +12,7 @@ extension ConfigurationMessage {
let displayName: String = profile.name
let profilePictureUrl: String? = profile.profilePictureUrl
let profileKey: Data? = profile.profileEncryptionKey?.keyData
var closedGroups: Set<ClosedGroup> = []
var closedGroups: Set<CMClosedGroup> = []
var openGroups: Set<String> = []
Storage.read { transaction in
@ -66,7 +66,7 @@ extension ConfigurationMessage {
return CMContact(
publicKey: contact.id,
displayName: (profile?.name ?? contact.id),
profilePictureURL: profile?.profilePictureUrl,
profilePictureUrl: profile?.profilePictureUrl,
profileKey: profile?.profileEncryptionKey?.keyData,
hasIsApproved: true,
isApproved: contact.isApproved,
@ -80,7 +80,7 @@ extension ConfigurationMessage {
return ConfigurationMessage(
displayName: displayName,
profilePictureURL: profilePictureUrl,
profilePictureUrl: profilePictureUrl,
profileKey: profileKey,
closedGroups: closedGroups,
openGroups: openGroups,

View File

@ -1,63 +1,49 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import Foundation
import Sodium
import GRDB
import Curve25519Kit
import SessionUtilitiesKit
@objc(SNConfigurationMessage)
public final class ConfigurationMessage : ControlMessage {
public final class ConfigurationMessage: ControlMessage {
private enum CodingKeys: String, CodingKey {
case closedGroups
case openGroups
case displayName
case profilePictureURL
case profilePictureUrl
case profileKey
case contacts
}
public var closedGroups: Set<ClosedGroup> = []
public var closedGroups: Set<CMClosedGroup> = []
public var openGroups: Set<String> = []
public var displayName: String?
public var profilePictureURL: String?
public var profilePictureUrl: String?
public var profileKey: Data?
public var contacts: Set<CMContact> = []
public override var isSelfSendValid: Bool { true }
// MARK: Initialization
public override init() { super.init() }
// MARK: - Initialization
public init(displayName: String?, profilePictureURL: String?, profileKey: Data?, closedGroups: Set<ClosedGroup>, openGroups: Set<String>, contacts: Set<CMContact>) {
public init(
displayName: String?,
profilePictureUrl: String?,
profileKey: Data?,
closedGroups: Set<CMClosedGroup>,
openGroups: Set<String>,
contacts: Set<CMContact>
) {
super.init()
self.displayName = displayName
self.profilePictureURL = profilePictureURL
self.profilePictureUrl = profilePictureUrl
self.profileKey = profileKey
self.closedGroups = closedGroups
self.openGroups = openGroups
self.contacts = contacts
}
// MARK: Coding
public required init?(coder: NSCoder) {
super.init(coder: coder)
if let closedGroups = coder.decodeObject(forKey: "closedGroups") as! Set<ClosedGroup>? { self.closedGroups = closedGroups }
if let openGroups = coder.decodeObject(forKey: "openGroups") as! Set<String>? { self.openGroups = openGroups }
if let displayName = coder.decodeObject(forKey: "displayName") as! String? { self.displayName = displayName }
if let profilePictureURL = coder.decodeObject(forKey: "profilePictureURL") as! String? { self.profilePictureURL = profilePictureURL }
if let profileKey = coder.decodeObject(forKey: "profileKey") as! Data? { self.profileKey = profileKey }
if let contacts = coder.decodeObject(forKey: "contacts") as! Set<CMContact>? { self.contacts = contacts }
}
public override func encode(with coder: NSCoder) {
super.encode(with: coder)
coder.encode(closedGroups, forKey: "closedGroups")
coder.encode(openGroups, forKey: "openGroups")
coder.encode(displayName, forKey: "displayName")
coder.encode(profilePictureURL, forKey: "profilePictureURL")
coder.encode(profileKey, forKey: "profileKey")
coder.encode(contacts, forKey: "contacts")
}
// MARK: - Codable
@ -66,10 +52,10 @@ public final class ConfigurationMessage : ControlMessage {
let container: KeyedDecodingContainer<CodingKeys> = try decoder.container(keyedBy: CodingKeys.self)
closedGroups = ((try? container.decode(Set<ClosedGroup>.self, forKey: .closedGroups)) ?? [])
closedGroups = ((try? container.decode(Set<CMClosedGroup>.self, forKey: .closedGroups)) ?? [])
openGroups = ((try? container.decode(Set<String>.self, forKey: .openGroups)) ?? [])
displayName = try? container.decode(String.self, forKey: .displayName)
profilePictureURL = try? container.decode(String.self, forKey: .profilePictureURL)
profilePictureUrl = try? container.decode(String.self, forKey: .profilePictureUrl)
profileKey = try? container.decode(Data.self, forKey: .profileKey)
contacts = ((try? container.decode(Set<CMContact>.self, forKey: .contacts)) ?? [])
}
@ -82,28 +68,36 @@ public final class ConfigurationMessage : ControlMessage {
try container.encodeIfPresent(closedGroups, forKey: .closedGroups)
try container.encodeIfPresent(openGroups, forKey: .openGroups)
try container.encodeIfPresent(displayName, forKey: .displayName)
try container.encodeIfPresent(profilePictureURL, forKey: .profilePictureURL)
try container.encodeIfPresent(profilePictureUrl, forKey: .profilePictureUrl)
try container.encodeIfPresent(profileKey, forKey: .profileKey)
try container.encodeIfPresent(contacts, forKey: .contacts)
}
// MARK: Proto Conversion
// MARK: - Proto Conversion
public override class func fromProto(_ proto: SNProtoContent, sender: String) -> ConfigurationMessage? {
guard let configurationProto = proto.configurationMessage else { return nil }
let displayName = configurationProto.displayName
let profilePictureURL = configurationProto.profilePicture
let profilePictureUrl = configurationProto.profilePicture
let profileKey = configurationProto.profileKey
let closedGroups = Set(configurationProto.closedGroups.compactMap { ClosedGroup.fromProto($0) })
let closedGroups = Set(configurationProto.closedGroups.compactMap { CMClosedGroup.fromProto($0) })
let openGroups = Set(configurationProto.openGroups)
let contacts = Set(configurationProto.contacts.compactMap { CMContact.fromProto($0) })
return ConfigurationMessage(displayName: displayName, profilePictureURL: profilePictureURL, profileKey: profileKey,
closedGroups: closedGroups, openGroups: openGroups, contacts: contacts)
return ConfigurationMessage(
displayName: displayName,
profilePictureUrl: profilePictureUrl,
profileKey: profileKey,
closedGroups: closedGroups,
openGroups: openGroups,
contacts: contacts
)
}
public override func toProto(_ db: Database) -> SNProtoContent? {
let configurationProto = SNProtoConfigurationMessage.builder()
if let displayName = displayName { configurationProto.setDisplayName(displayName) }
if let profilePictureURL = profilePictureURL { configurationProto.setProfilePicture(profilePictureURL) }
if let profilePictureUrl = profilePictureUrl { configurationProto.setProfilePicture(profilePictureUrl) }
if let profileKey = profileKey { configurationProto.setProfileKey(profileKey) }
configurationProto.setClosedGroups(closedGroups.compactMap { $0.toProto() })
configurationProto.setOpenGroups([String](openGroups))
@ -118,14 +112,15 @@ public final class ConfigurationMessage : ControlMessage {
}
}
// MARK: Description
public override var description: String {
// MARK: - Description
public var description: String {
"""
ConfigurationMessage(
closedGroups: \([ClosedGroup](closedGroups).prettifiedDescription),
closedGroups: \([CMClosedGroup](closedGroups).prettifiedDescription),
openGroups: \([String](openGroups).prettifiedDescription),
displayName: \(displayName ?? "null"),
profilePictureURL: \(profilePictureURL ?? "null"),
profilePictureUrl: \(profilePictureUrl ?? "null"),
profileKey: \(profileKey?.toHexString() ?? "null"),
contacts: \([CMContact](contacts).prettifiedDescription)
)
@ -133,11 +128,10 @@ public final class ConfigurationMessage : ControlMessage {
}
}
// MARK: Closed Group
extension ConfigurationMessage {
// MARK: - Closed Group
@objc(SNClosedGroup)
public final class ClosedGroup: NSObject, Codable, NSCoding { // NSObject/NSCoding conformance is needed for YapDatabase compatibility
extension ConfigurationMessage {
public struct CMClosedGroup: Codable, Hashable, CustomStringConvertible {
private enum CodingKeys: String, CodingKey {
case publicKey
case name
@ -150,57 +144,43 @@ extension ConfigurationMessage {
public let publicKey: String
public let name: String
public let encryptionKeyPair: ECKeyPair
public let encryptionKeyPublicKey: Data
public let encryptionKeySecretKey: Data
public let members: Set<String>
public let admins: Set<String>
public let expirationTimer: UInt32
public var isValid: Bool { !members.isEmpty && !admins.isEmpty }
public init(publicKey: String, name: String, encryptionKeyPair: ECKeyPair, members: Set<String>, admins: Set<String>, expirationTimer: UInt32) {
self.publicKey = publicKey
self.name = name
self.encryptionKeyPair = encryptionKeyPair
self.members = members
self.admins = admins
self.expirationTimer = expirationTimer
}
public required init?(coder: NSCoder) {
guard let publicKey = coder.decodeObject(forKey: "publicKey") as! String?,
let name = coder.decodeObject(forKey: "name") as! String?,
let encryptionKeyPair = coder.decodeObject(forKey: "encryptionKeyPair") as! ECKeyPair?,
let members = coder.decodeObject(forKey: "members") as! Set<String>?,
let admins = coder.decodeObject(forKey: "admins") as! Set<String>? else { return nil }
let expirationTimer = coder.decodeObject(forKey: "expirationTimer") as? UInt32 ?? 0
self.publicKey = publicKey
self.name = name
self.encryptionKeyPair = encryptionKeyPair
self.members = members
self.admins = admins
self.expirationTimer = expirationTimer
}
public func encode(with coder: NSCoder) {
coder.encode(publicKey, forKey: "publicKey")
coder.encode(name, forKey: "name")
coder.encode(encryptionKeyPair, forKey: "encryptionKeyPair")
coder.encode(members, forKey: "members")
coder.encode(admins, forKey: "admins")
coder.encode(expirationTimer, forKey: "expirationTimer")
}
// MARK: - Initialization
public init(
publicKey: String,
name: String,
encryptionKeyPublicKey: Data,
encryptionKeySecretKey: Data,
members: Set<String>,
admins: Set<String>,
expirationTimer: UInt32
) {
self.publicKey = publicKey
self.name = name
self.encryptionKeyPublicKey = encryptionKeyPublicKey
self.encryptionKeySecretKey = encryptionKeySecretKey
self.members = members
self.admins = admins
self.expirationTimer = expirationTimer
}
// MARK: - Codable
public required init(from decoder: Decoder) throws {
public init(from decoder: Decoder) throws {
let container: KeyedDecodingContainer<CodingKeys> = try decoder.container(keyedBy: CodingKeys.self)
publicKey = try container.decode(String.self, forKey: .publicKey)
name = try container.decode(String.self, forKey: .name)
encryptionKeyPair = try ECKeyPair(
publicKeyData: try container.decode(Data.self, forKey: .encryptionKeyPublicKey),
privateKeyData: try container.decode(Data.self, forKey: .encryptionKeySecretKey)
)
encryptionKeyPublicKey = try container.decode(Data.self, forKey: .encryptionKeyPublicKey)
encryptionKeySecretKey = try container.decode(Data.self, forKey: .encryptionKeySecretKey)
members = try container.decode(Set<String>.self, forKey: .members)
admins = try container.decode(Set<String>.self, forKey: .admins)
expirationTimer = try container.decode(UInt32.self, forKey: .expirationTimer)
@ -211,28 +191,33 @@ extension ConfigurationMessage {
try container.encode(publicKey, forKey: .publicKey)
try container.encode(name, forKey: .name)
try container.encode(encryptionKeyPair.publicKey, forKey: .encryptionKeyPublicKey)
try container.encode(encryptionKeyPair.privateKey, forKey: .encryptionKeySecretKey)
try container.encode(encryptionKeyPublicKey, forKey: .encryptionKeyPublicKey)
try container.encode(encryptionKeySecretKey, forKey: .encryptionKeySecretKey)
try container.encode(members, forKey: .members)
try container.encode(admins, forKey: .admins)
try container.encode(expirationTimer, forKey: .expirationTimer)
}
public static func fromProto(_ proto: SNProtoConfigurationMessageClosedGroup) -> ClosedGroup? {
guard let publicKey = proto.publicKey?.toHexString(),
public static func fromProto(_ proto: SNProtoConfigurationMessageClosedGroup) -> CMClosedGroup? {
guard
let publicKey = proto.publicKey?.toHexString(),
let name = proto.name,
let encryptionKeyPairAsProto = proto.encryptionKeyPair else { return nil }
let encryptionKeyPair: ECKeyPair
do {
encryptionKeyPair = try ECKeyPair(publicKeyData: encryptionKeyPairAsProto.publicKey, privateKeyData: encryptionKeyPairAsProto.privateKey)
} catch {
SNLog("Couldn't construct closed group from proto: \(self).")
return nil
}
let encryptionKeyPairAsProto = proto.encryptionKeyPair
else { return nil }
let members = Set(proto.members.map { $0.toHexString() })
let admins = Set(proto.admins.map { $0.toHexString() })
let expirationTimer = proto.expirationTimer
let result = ClosedGroup(publicKey: publicKey, name: name, encryptionKeyPair: encryptionKeyPair, members: members, admins: admins, expirationTimer: expirationTimer)
let result = CMClosedGroup(
publicKey: publicKey,
name: name,
encryptionKeyPublicKey: encryptionKeyPairAsProto.publicKey,
encryptionKeySecretKey: encryptionKeyPairAsProto.privateKey,
members: members,
admins: admins,
expirationTimer: expirationTimer
)
guard result.isValid else { return nil }
return result
}
@ -243,7 +228,10 @@ extension ConfigurationMessage {
result.setPublicKey(Data(hex: publicKey))
result.setName(name)
do {
let encryptionKeyPairAsProto = try SNProtoKeyPair.builder(publicKey: encryptionKeyPair.publicKey, privateKey: encryptionKeyPair.privateKey).build()
let encryptionKeyPairAsProto = try SNProtoKeyPair.builder(
publicKey: encryptionKeyPublicKey,
privateKey: encryptionKeySecretKey
).build()
result.setEncryptionKeyPair(encryptionKeyPairAsProto)
} catch {
SNLog("Couldn't construct closed group proto from: \(self).")
@ -260,19 +248,18 @@ extension ConfigurationMessage {
}
}
public override var description: String { name }
public var description: String { name }
}
}
// MARK: Contact
extension ConfigurationMessage {
// MARK: - Contact
@objc(SNConfigurationMessageContact)
public final class CMContact: NSObject, Codable, NSCoding { // NSObject/NSCoding conformance is needed for YapDatabase compatibility
extension ConfigurationMessage {
public struct CMContact: Codable, Hashable, CustomStringConvertible {
private enum CodingKeys: String, CodingKey {
case publicKey
case displayName
case profilePictureURL
case profilePictureUrl
case profileKey
case hasIsApproved
@ -285,7 +272,7 @@ extension ConfigurationMessage {
public var publicKey: String?
public var displayName: String?
public var profilePictureURL: String?
public var profilePictureUrl: String?
public var profileKey: Data?
public var hasIsApproved: Bool
@ -298,9 +285,9 @@ extension ConfigurationMessage {
public var isValid: Bool { publicKey != nil && displayName != nil }
public init(
publicKey: String,
displayName: String,
profilePictureURL: String?,
publicKey: String?,
displayName: String?,
profilePictureUrl: String?,
profileKey: Data?,
hasIsApproved: Bool,
isApproved: Bool,
@ -311,7 +298,7 @@ extension ConfigurationMessage {
) {
self.publicKey = publicKey
self.displayName = displayName
self.profilePictureURL = profilePictureURL
self.profilePictureUrl = profilePictureUrl
self.profileKey = profileKey
self.hasIsApproved = hasIsApproved
self.isApproved = isApproved
@ -320,43 +307,15 @@ extension ConfigurationMessage {
self.hasDidApproveMe = hasDidApproveMe
self.didApproveMe = didApproveMe
}
public required init?(coder: NSCoder) {
guard let publicKey = coder.decodeObject(forKey: "publicKey") as! String?,
let displayName = coder.decodeObject(forKey: "displayName") as! String? else { return nil }
self.publicKey = publicKey
self.displayName = displayName
self.profilePictureURL = coder.decodeObject(forKey: "profilePictureURL") as! String?
self.profileKey = coder.decodeObject(forKey: "profileKey") as! Data?
self.hasIsApproved = (coder.decodeObject(forKey: "hasIsApproved") as? Bool ?? false)
self.isApproved = (coder.decodeObject(forKey: "isApproved") as? Bool ?? false)
self.hasIsBlocked = (coder.decodeObject(forKey: "hasIsBlocked") as? Bool ?? false)
self.isBlocked = (coder.decodeObject(forKey: "isBlocked") as? Bool ?? false)
self.hasDidApproveMe = (coder.decodeObject(forKey: "hasDidApproveMe") as? Bool ?? false)
self.didApproveMe = (coder.decodeObject(forKey: "didApproveMe") as? Bool ?? false)
}
public func encode(with coder: NSCoder) {
coder.encode(publicKey, forKey: "publicKey")
coder.encode(displayName, forKey: "displayName")
coder.encode(profilePictureURL, forKey: "profilePictureURL")
coder.encode(profileKey, forKey: "profileKey")
coder.encode(hasIsApproved, forKey: "hasIsApproved")
coder.encode(isApproved, forKey: "isApproved")
coder.encode(hasIsBlocked, forKey: "hasIsBlocked")
coder.encode(isBlocked, forKey: "isBlocked")
coder.encode(hasDidApproveMe, forKey: "hasDidApproveMe")
coder.encode(didApproveMe, forKey: "didApproveMe")
}
// MARK: - Codable
public required init(from decoder: Decoder) throws {
public init(from decoder: Decoder) throws {
let container: KeyedDecodingContainer<CodingKeys> = try decoder.container(keyedBy: CodingKeys.self)
publicKey = try? container.decode(String.self, forKey: .publicKey)
displayName = try? container.decode(String.self, forKey: .displayName)
profilePictureURL = try? container.decode(String.self, forKey: .profilePictureURL)
profilePictureUrl = try? container.decode(String.self, forKey: .profilePictureUrl)
profileKey = try? container.decode(Data.self, forKey: .profileKey)
hasIsApproved = try container.decode(Bool.self, forKey: .hasIsApproved)
@ -371,7 +330,7 @@ extension ConfigurationMessage {
let result: CMContact = CMContact(
publicKey: proto.publicKey.toHexString(),
displayName: proto.name,
profilePictureURL: proto.profilePicture,
profilePictureUrl: proto.profilePicture,
profileKey: proto.profileKey,
hasIsApproved: proto.hasIsApproved,
isApproved: proto.isApproved,
@ -389,7 +348,7 @@ extension ConfigurationMessage {
guard isValid else { return nil }
guard let publicKey = publicKey, let displayName = displayName else { return nil }
let result = SNProtoConfigurationMessageContact.builder(publicKey: Data(hex: publicKey), name: displayName)
if let profilePictureURL = profilePictureURL { result.setProfilePicture(profilePictureURL) }
if let profilePictureUrl = profilePictureUrl { result.setProfilePicture(profilePictureUrl) }
if let profileKey = profileKey { result.setProfileKey(profileKey) }
if hasIsApproved { result.setIsApproved(isApproved) }
@ -404,6 +363,6 @@ extension ConfigurationMessage {
}
}
public override var description: String { displayName ?? "" }
public var description: String { displayName ?? "" }
}
}

View File

@ -2,5 +2,4 @@
import Foundation
@objc(SNControlMessage)
public class ControlMessage: Message { }

View File

@ -11,59 +11,36 @@ public final class DataExtractionNotification : ControlMessage {
public var kind: Kind?
// MARK: Kind
// MARK: - Kind
public enum Kind: CustomStringConvertible, Codable {
case screenshot
case mediaSaved(timestamp: UInt64)
public var description: String {
switch self {
case .screenshot: return "screenshot"
case .mediaSaved: return "mediaSaved"
case .screenshot: return "screenshot"
case .mediaSaved: return "mediaSaved"
}
}
}
// MARK: Initialization
public override init() { super.init() }
// MARK: - Initialization
internal init(kind: Kind) {
super.init()
self.kind = kind
}
// MARK: Validation
// MARK: - Validation
public override var isValid: Bool {
guard super.isValid, let kind = kind else { return false }
switch kind {
case .screenshot: return true
case .mediaSaved(let timestamp): return timestamp > 0
}
}
// MARK: Coding
public required init?(coder: NSCoder) {
super.init(coder: coder)
guard let rawKind = coder.decodeObject(forKey: "kind") as? String else { return nil }
switch rawKind {
case "screenshot":
self.kind = .screenshot
case "mediaSaved":
guard let timestamp = coder.decodeObject(forKey: "timestamp") as? UInt64 else { return nil }
self.kind = .mediaSaved(timestamp: timestamp)
default: return nil
}
}
public override func encode(with coder: NSCoder) {
super.encode(with: coder)
guard let kind = kind else { return }
switch kind {
case .screenshot:
coder.encode("screenshot", forKey: "kind")
case .mediaSaved(let timestamp):
coder.encode("mediaSaved", forKey: "kind")
coder.encode(timestamp, forKey: "timestamp")
case .screenshot: return true
case .mediaSaved(let timestamp): return timestamp > 0
}
}
@ -85,7 +62,8 @@ public final class DataExtractionNotification : ControlMessage {
try container.encodeIfPresent(kind, forKey: .kind)
}
// MARK: Proto Conversion
// MARK: - Proto Conversion
public override class func fromProto(_ proto: SNProtoContent, sender: String) -> DataExtractionNotification? {
guard let dataExtractionNotification = proto.dataExtractionNotification else { return nil }
let kind: Kind
@ -121,8 +99,9 @@ public final class DataExtractionNotification : ControlMessage {
}
}
// MARK: Description
public override var description: String {
// MARK: - Description
public var description: String {
"""
DataExtractionNotification(
kind: \(kind?.description ?? "null")

View File

@ -4,8 +4,7 @@ import Foundation
import GRDB
import SessionUtilitiesKit
@objc(SNExpirationTimerUpdate)
public final class ExpirationTimerUpdate : ControlMessage {
public final class ExpirationTimerUpdate: ControlMessage {
private enum CodingKeys: String, CodingKey {
case syncTarget
case duration
@ -19,33 +18,21 @@ public final class ExpirationTimerUpdate : ControlMessage {
public override var isSelfSendValid: Bool { true }
// MARK: Initialization
public override init() { super.init() }
// MARK: - Initialization
internal init(syncTarget: String?, duration: UInt32) {
super.init()
self.syncTarget = syncTarget
self.duration = duration
}
// MARK: Validation
// MARK: - Validation
public override var isValid: Bool {
guard super.isValid else { return false }
return duration != nil
}
// MARK: Coding
public required init?(coder: NSCoder) {
super.init(coder: coder)
if let syncTarget = coder.decodeObject(forKey: "syncTarget") as! String? { self.syncTarget = syncTarget }
if let duration = coder.decodeObject(forKey: "durationSeconds") as! UInt32? { self.duration = duration }
}
public override func encode(with coder: NSCoder) {
super.encode(with: coder)
coder.encode(syncTarget, forKey: "syncTarget")
coder.encode(duration, forKey: "durationSeconds")
}
// MARK: - Codable
@ -67,7 +54,8 @@ public final class ExpirationTimerUpdate : ControlMessage {
try container.encodeIfPresent(duration, forKey: .duration)
}
// MARK: Proto Conversion
// MARK: - Proto Conversion
public override class func fromProto(_ proto: SNProtoContent, sender: String) -> ExpirationTimerUpdate? {
guard let dataMessageProto = proto.dataMessage else { return nil }
@ -106,8 +94,9 @@ public final class ExpirationTimerUpdate : ControlMessage {
}
}
// MARK: Description
public override var description: String {
// MARK: - Description
public var description: String {
"""
ExpirationTimerUpdate(
syncTarget: \(syncTarget ?? "null"),
@ -115,9 +104,4 @@ public final class ExpirationTimerUpdate : ControlMessage {
)
"""
}
// MARK: Convenience
@objc public func setDuration(_ duration: UInt32) {
self.duration = duration
}
}

View File

@ -4,7 +4,6 @@ import Foundation
import GRDB
import SessionUtilitiesKit
@objc(SNMessageRequestResponse)
public final class MessageRequestResponse: ControlMessage {
private enum CodingKeys: String, CodingKey {
case isApproved
@ -20,22 +19,6 @@ public final class MessageRequestResponse: ControlMessage {
super.init()
}
// MARK: - Coding
public required init?(coder: NSCoder) {
guard let isApproved: Bool = coder.decodeObject(forKey: "isApproved") as? Bool else { return nil }
self.isApproved = isApproved
super.init(coder: coder)
}
public override func encode(with coder: NSCoder) {
super.encode(with: coder)
coder.encode(isApproved, forKey: "isApproved")
}
// MARK: - Codable
required init(from decoder: Decoder) throws {
@ -79,7 +62,7 @@ public final class MessageRequestResponse: ControlMessage {
// MARK: - Description
public override var description: String {
public var description: String {
"""
MessageRequestResponse(
isApproved: \(isApproved)

View File

@ -4,39 +4,28 @@ import Foundation
import GRDB
import SessionUtilitiesKit
@objc(SNReadReceipt)
public final class ReadReceipt : ControlMessage {
public final class ReadReceipt: ControlMessage {
private enum CodingKeys: String, CodingKey {
case timestamps
}
@objc public var timestamps: [UInt64]?
public var timestamps: [UInt64]?
// MARK: Initialization
public override init() { super.init() }
// MARK: - Initialization
internal init(timestamps: [UInt64]) {
super.init()
self.timestamps = timestamps
}
// MARK: Validation
// MARK: - Validation
public override var isValid: Bool {
guard super.isValid else { return false }
if let timestamps = timestamps, !timestamps.isEmpty { return true }
return false
}
// MARK: Coding
public required init?(coder: NSCoder) {
super.init(coder: coder)
if let timestamps = coder.decodeObject(forKey: "messageTimestamps") as! [UInt64]? { self.timestamps = timestamps }
}
public override func encode(with coder: NSCoder) {
super.encode(with: coder)
coder.encode(timestamps, forKey: "messageTimestamps")
}
// MARK: - Codable
@ -56,7 +45,8 @@ public final class ReadReceipt : ControlMessage {
try container.encodeIfPresent(timestamps, forKey: .timestamps)
}
// MARK: Proto Conversion
// MARK: - Proto Conversion
public override class func fromProto(_ proto: SNProtoContent, sender: String) -> ReadReceipt? {
guard let receiptProto = proto.receiptMessage, receiptProto.type == .read else { return nil }
let timestamps = receiptProto.timestamp
@ -81,8 +71,9 @@ public final class ReadReceipt : ControlMessage {
}
}
// MARK: Description
public override var description: String {
// MARK: - Description
public var description: String {
"""
ReadReceipt(
timestamps: \(timestamps?.description ?? "null")

View File

@ -4,8 +4,7 @@ import Foundation
import GRDB
import SessionUtilitiesKit
@objc(SNTypingIndicator)
public final class TypingIndicator : ControlMessage {
public final class TypingIndicator: ControlMessage {
private enum CodingKeys: String, CodingKey {
case kind
}
@ -14,56 +13,47 @@ public final class TypingIndicator : ControlMessage {
public override var ttl: UInt64 { 20 * 1000 }
// MARK: Kind
// MARK: - Kind
public enum Kind: Int, Codable, CustomStringConvertible {
case started, stopped
static func fromProto(_ proto: SNProtoTypingMessage.SNProtoTypingMessageAction) -> Kind {
switch proto {
case .started: return .started
case .stopped: return .stopped
case .started: return .started
case .stopped: return .stopped
}
}
func toProto() -> SNProtoTypingMessage.SNProtoTypingMessageAction {
switch self {
case .started: return .started
case .stopped: return .stopped
case .started: return .started
case .stopped: return .stopped
}
}
public var description: String {
switch self {
case .started: return "started"
case .stopped: return "stopped"
case .started: return "started"
case .stopped: return "stopped"
}
}
}
// MARK: Validation
// MARK: - Validation
public override var isValid: Bool {
guard super.isValid else { return false }
return kind != nil
}
// MARK: Initialization
public override init() { super.init() }
// MARK: - Initialization
internal init(kind: Kind) {
super.init()
self.kind = kind
}
// MARK: Coding
public required init?(coder: NSCoder) {
super.init(coder: coder)
if let rawKind = coder.decodeObject(forKey: "action") as! Int? { kind = Kind(rawValue: rawKind) }
}
public override func encode(with coder: NSCoder) {
super.encode(with: coder)
coder.encode(kind?.rawValue, forKey: "action")
}
// MARK: - Codable
@ -83,7 +73,8 @@ public final class TypingIndicator : ControlMessage {
try container.encodeIfPresent(kind, forKey: .kind)
}
// MARK: Proto Conversion
// MARK: - Proto Conversion
public override class func fromProto(_ proto: SNProtoContent, sender: String) -> TypingIndicator? {
guard let typingIndicatorProto = proto.typingMessage else { return nil }
let kind = Kind.fromProto(typingIndicatorProto.action)
@ -106,8 +97,9 @@ public final class TypingIndicator : ControlMessage {
}
}
// MARK: Description
public override var description: String {
// MARK: - Description
public var description: String {
"""
TypingIndicator(
kind: \(kind?.description ?? "null")

View File

@ -4,7 +4,6 @@ import Foundation
import GRDB
import SessionUtilitiesKit
@objc(SNUnsendRequest)
public final class UnsendRequest: ControlMessage {
private enum CodingKeys: String, CodingKey {
case timestamp
@ -16,33 +15,22 @@ public final class UnsendRequest: ControlMessage {
public override var isSelfSendValid: Bool { true }
// MARK: Validation
// MARK: - Validation
public override var isValid: Bool {
guard super.isValid else { return false }
return timestamp != nil && author != nil
}
// MARK: Initialization
public override init() { super.init() }
// MARK: - Initialization
internal init(timestamp: UInt64, author: String) {
super.init()
self.timestamp = timestamp
self.author = author
}
// MARK: Coding
public required init?(coder: NSCoder) {
super.init(coder: coder)
if let timestamp = coder.decodeObject(forKey: "timestamp") as! UInt64? { self.timestamp = timestamp }
if let author = coder.decodeObject(forKey: "author") as! String? { self.author = author }
}
public override func encode(with coder: NSCoder) {
super.encode(with: coder)
coder.encode(timestamp, forKey: "timestamp")
coder.encode(author, forKey: "author")
}
// MARK: - Codable
@ -64,7 +52,8 @@ public final class UnsendRequest: ControlMessage {
try container.encodeIfPresent(author, forKey: .author)
}
// MARK: Proto Conversion
// MARK: - Proto Conversion
public override class func fromProto(_ proto: SNProtoContent, sender: String) -> UnsendRequest? {
guard let unsendRequestProto = proto.unsendRequest else { return nil }
let timestamp = unsendRequestProto.timestamp
@ -88,8 +77,9 @@ public final class UnsendRequest: ControlMessage {
}
}
// MARK: Description
public override var description: String {
// MARK: - Description
public var description: String {
"""
UnsendRequest(
timestamp: \(timestamp?.description ?? "null")

View File

@ -4,59 +4,57 @@ import Foundation
import GRDB
/// Abstract base class for `VisibleMessage` and `ControlMessage`.
@objc(SNMessage)
public class Message: NSObject, Codable, NSCoding { // NSObject/NSCoding conformance is needed for YapDatabase compatibility
public class Message: Codable {
public var id: String?
@objc public var threadID: String?
public var threadId: String?
public var sentTimestamp: UInt64?
public var receivedTimestamp: UInt64?
public var recipient: String?
public var sender: String?
public var groupPublicKey: String?
public var openGroupServerMessageID: UInt64?
public var openGroupServerMessageId: UInt64?
public var openGroupServerTimestamp: UInt64?
public var serverHash: String?
public var ttl: UInt64 { 14 * 24 * 60 * 60 * 1000 }
public var isSelfSendValid: Bool { false }
public override init() { }
// MARK: Validation
// MARK: - Validation
public var isValid: Bool {
if let sentTimestamp = sentTimestamp { guard sentTimestamp > 0 else { return false } }
if let receivedTimestamp = receivedTimestamp { guard receivedTimestamp > 0 else { return false } }
return sender != nil && recipient != nil
}
// MARK: Coding
public required init?(coder: NSCoder) {
if let id = coder.decodeObject(forKey: "id") as! String? { self.id = id }
if let threadID = coder.decodeObject(forKey: "threadID") as! String? { self.threadID = threadID }
if let sentTimestamp = coder.decodeObject(forKey: "sentTimestamp") as! UInt64? { self.sentTimestamp = sentTimestamp }
if let receivedTimestamp = coder.decodeObject(forKey: "receivedTimestamp") as! UInt64? { self.receivedTimestamp = receivedTimestamp }
if let recipient = coder.decodeObject(forKey: "recipient") as! String? { self.recipient = recipient }
if let sender = coder.decodeObject(forKey: "sender") as! String? { self.sender = sender }
if let groupPublicKey = coder.decodeObject(forKey: "groupPublicKey") as! String? { self.groupPublicKey = groupPublicKey }
if let openGroupServerMessageID = coder.decodeObject(forKey: "openGroupServerMessageID") as! UInt64? { self.openGroupServerMessageID = openGroupServerMessageID }
if let openGroupServerTimestamp = coder.decodeObject(forKey: "openGroupServerTimestamp") as! UInt64? { self.openGroupServerTimestamp = openGroupServerTimestamp }
if let serverHash = coder.decodeObject(forKey: "serverHash") as! String? { self.serverHash = serverHash }
// MARK: - Initialization
public init(
id: String? = nil,
threadId: String? = nil,
sentTimestamp: UInt64? = nil,
receivedTimestamp: UInt64? = nil,
recipient: String? = nil,
sender: String? = nil,
groupPublicKey: String? = nil,
openGroupServerMessageId: UInt64? = nil,
openGroupServerTimestamp: UInt64? = nil,
serverHash: String? = nil
) {
self.id = id
self.threadId = threadId
self.sentTimestamp = sentTimestamp
self.receivedTimestamp = receivedTimestamp
self.recipient = recipient
self.sender = sender
self.groupPublicKey = groupPublicKey
self.openGroupServerMessageId = openGroupServerMessageId
self.openGroupServerTimestamp = openGroupServerTimestamp
self.serverHash = serverHash
}
public func encode(with coder: NSCoder) {
coder.encode(id, forKey: "id")
coder.encode(threadID, forKey: "threadID")
coder.encode(sentTimestamp, forKey: "sentTimestamp")
coder.encode(receivedTimestamp, forKey: "receivedTimestamp")
coder.encode(recipient, forKey: "recipient")
coder.encode(sender, forKey: "sender")
coder.encode(groupPublicKey, forKey: "groupPublicKey")
coder.encode(openGroupServerMessageID, forKey: "openGroupServerMessageID")
coder.encode(openGroupServerTimestamp, forKey: "openGroupServerTimestamp")
coder.encode(serverHash, forKey: "serverHash")
}
// MARK: Proto Conversion
// MARK: - Proto Conversion
public class func fromProto(_ proto: SNProtoContent, sender: String) -> Self? {
preconditionFailure("fromProto(_:sender:) is abstract and must be overridden.")
}
@ -67,7 +65,7 @@ public class Message: NSObject, Codable, NSCoding { // NSObject/NSCoding conform
public func setGroupContextIfNeeded(_ db: Database, on dataMessage: SNProtoDataMessage.SNProtoDataMessageBuilder) throws {
guard
let threadId: String = threadID,
let threadId: String = threadId,
let thread: SessionThread = try? SessionThread.fetchOne(db, id: threadId),
thread.variant == .closedGroup,
let legacyGroupId: Data = "\(Legacy.closedGroupIdPrefix)\(threadId)".data(using: .utf8)
@ -77,9 +75,4 @@ public class Message: NSObject, Codable, NSCoding { // NSObject/NSCoding conform
let groupProto = SNProtoGroupContext.builder(id: legacyGroupId, type: .deliver)
dataMessage.setGroup(try groupProto.build())
}
// MARK: General
@objc public func setSentTimestamp(_ sentTimestamp: UInt64) {
self.sentTimestamp = sentTimestamp
}
}

View File

@ -7,15 +7,15 @@ public extension TSIncomingMessage {
Storage.read { transaction in
expiration = thread.disappearingMessagesDuration(with: transaction)
}
let openGroupServerMessageID = visibleMessage.openGroupServerMessageID ?? 0
let isOpenGroupMessage = (openGroupServerMessageID != 0)
let openGroupServerMessageId = visibleMessage.openGroupServerMessageId ?? 0
let isOpenGroupMessage = (openGroupServerMessageId != 0)
let result = TSIncomingMessage(
timestamp: visibleMessage.sentTimestamp!,
in: thread,
authorId: sender,
sourceDeviceId: 1,
messageBody: visibleMessage.text,
attachmentIds: visibleMessage.attachmentIDs,
attachmentIds: visibleMessage.attachmentIds,
expiresInSeconds: !isOpenGroupMessage ? expiration : 0, // Ensure we don't ever expire open group messages
quotedMessage: quotedMessage,
linkPreview: linkPreview,
@ -24,7 +24,7 @@ public extension TSIncomingMessage {
openGroupInvitationURL: visibleMessage.openGroupInvitation?.url,
serverHash: visibleMessage.serverHash
)
result.openGroupServerMessageID = openGroupServerMessageID
result.openGroupServerMessageID = openGroupServerMessageId
return result
}
}

View File

@ -5,37 +5,25 @@ import GRDB
import SessionUtilitiesKit
public extension VisibleMessage {
struct LinkPreview: Codable {
public let title: String?
public let url: String?
public let attachmentId: String?
@objc(SNLinkPreview)
class LinkPreview: NSObject, Codable, NSCoding {
public var title: String?
public var url: String?
public var attachmentID: String?
public var isValid: Bool { title != nil && url != nil && attachmentId != nil }
public var isValid: Bool { title != nil && url != nil && attachmentID != nil }
internal init(title: String?, url: String, attachmentID: String?) {
internal init(title: String?, url: String, attachmentId: String?) {
self.title = title
self.url = url
self.attachmentID = attachmentID
}
public required init?(coder: NSCoder) {
if let title = coder.decodeObject(forKey: "title") as! String? { self.title = title }
if let url = coder.decodeObject(forKey: "urlString") as! String? { self.url = url }
if let attachmentID = coder.decodeObject(forKey: "attachmentID") as! String? { self.attachmentID = attachmentID }
}
public func encode(with coder: NSCoder) {
coder.encode(title, forKey: "title")
coder.encode(url, forKey: "urlString")
coder.encode(attachmentID, forKey: "attachmentID")
self.attachmentId = attachmentId
}
// MARK: - Proto Conversion
public static func fromProto(_ proto: SNProtoDataMessagePreview) -> LinkPreview? {
let title = proto.title
let url = proto.url
return LinkPreview(title: title, url: url, attachmentID: nil)
return LinkPreview(title: title, url: url, attachmentId: nil)
}
public func toProto() -> SNProtoDataMessagePreview? {
@ -51,8 +39,9 @@ public extension VisibleMessage {
if let title = title { linkPreviewProto.setTitle(title) }
if
let attachmentID = attachmentID,
let attachment: SessionMessagingKit.Attachment = try? SessionMessagingKit.Attachment.fetchOne(db, id: attachmentID),
let attachmentId = attachmentId,
// TODO: try to ditch `SessionMessagingKit.`
let attachment: SessionMessagingKit.Attachment = try? SessionMessagingKit.Attachment.fetchOne(db, id: attachmentId),
let attachmentProto = attachment.buildProto()
{
linkPreviewProto.setImage(attachmentProto)
@ -66,13 +55,14 @@ public extension VisibleMessage {
}
}
// MARK: Description
public override var description: String {
// MARK: - Description
public var description: String {
"""
LinkPreview(
title: \(title ?? "null"),
url: \(url ?? "null"),
attachmentID: \(attachmentID ?? "null")
attachmentId: \(attachmentId ?? "null")
)
"""
}
@ -86,7 +76,7 @@ public extension VisibleMessage.LinkPreview {
return VisibleMessage.LinkPreview(
title: linkPreview.title,
url: linkPreview.url,
attachmentID: linkPreview.attachmentId
attachmentId: linkPreview.attachmentId
)
}
}

View File

@ -5,27 +5,16 @@ import GRDB
import SessionUtilitiesKit
public extension VisibleMessage {
struct OpenGroupInvitation: Codable {
public let name: String?
public let url: String?
@objc(SNOpenGroupInvitation)
class OpenGroupInvitation: NSObject, Codable, NSCoding {
public var name: String?
public var url: String?
@objc
public init(name: String, url: String) {
self.name = name
self.url = url
}
public required init?(coder: NSCoder) {
if let name = coder.decodeObject(forKey: "name") as! String? { self.name = name }
if let url = coder.decodeObject(forKey: "url") as! String? { self.url = url }
}
public func encode(with coder: NSCoder) {
coder.encode(name, forKey: "name")
coder.encode(url, forKey: "url")
}
// MARK: - Proto Conversion
public static func fromProto(_ proto: SNProtoDataMessageOpenGroupInvitation) -> OpenGroupInvitation? {
let url = proto.url
@ -47,8 +36,9 @@ public extension VisibleMessage {
}
}
// MARK: Description
public override var description: String {
// MARK: - Description
public var description: String {
"""
OpenGroupInvitation(
name: \(name ?? "null"),

View File

@ -4,40 +4,30 @@ import Foundation
import SessionUtilitiesKit
public extension VisibleMessage {
struct Profile: Codable {
public let displayName: String?
public let profileKey: Data?
public let profilePictureUrl: String?
@objc(SNProfile)
class Profile: NSObject, Codable, NSCoding {
public var displayName: String?
public var profileKey: Data?
public var profilePictureURL: String?
internal init(displayName: String, profileKey: Data? = nil, profilePictureURL: String? = nil) {
internal init(displayName: String, profileKey: Data? = nil, profilePictureUrl: String? = nil) {
self.displayName = displayName
self.profileKey = profileKey
self.profilePictureURL = profilePictureURL
self.profilePictureUrl = profilePictureUrl
}
public required init?(coder: NSCoder) {
if let displayName = coder.decodeObject(forKey: "displayName") as! String? { self.displayName = displayName }
if let profileKey = coder.decodeObject(forKey: "profileKey") as! Data? { self.profileKey = profileKey }
if let profilePictureURL = coder.decodeObject(forKey: "profilePictureURL") as! String? { self.profilePictureURL = profilePictureURL }
}
public func encode(with coder: NSCoder) {
coder.encode(displayName, forKey: "displayName")
coder.encode(profileKey, forKey: "profileKey")
coder.encode(profilePictureURL, forKey: "profilePictureURL")
}
// MARK: - Proto Conversion
public static func fromProto(_ proto: SNProtoDataMessage) -> Profile? {
guard let profileProto = proto.profile, let displayName = profileProto.displayName else { return nil }
let profileKey = proto.profileKey
let profilePictureURL = profileProto.profilePicture
if let profileKey = profileKey, let profilePictureURL = profilePictureURL {
return Profile(displayName: displayName, profileKey: profileKey, profilePictureURL: profilePictureURL)
} else {
return Profile(displayName: displayName)
}
guard
let profileProto = proto.profile,
let displayName = profileProto.displayName
else { return nil }
return Profile(
displayName: displayName,
profileKey: proto.profileKey,
profilePictureUrl: profileProto.profilePicture
)
}
public func toProto() -> SNProtoDataMessage? {
@ -48,9 +38,10 @@ public extension VisibleMessage {
let dataMessageProto = SNProtoDataMessage.builder()
let profileProto = SNProtoDataMessageLokiProfile.builder()
profileProto.setDisplayName(displayName)
if let profileKey = profileKey, let profilePictureURL = profilePictureURL {
if let profileKey = profileKey, let profilePictureUrl = profilePictureUrl {
dataMessageProto.setProfileKey(profileKey)
profileProto.setProfilePicture(profilePictureURL)
profileProto.setProfilePicture(profilePictureUrl)
}
do {
dataMessageProto.setProfile(try profileProto.build())
@ -62,12 +53,13 @@ public extension VisibleMessage {
}
// MARK: Description
public override var description: String {
public var description: String {
"""
Profile(
displayName: \(displayName ?? "null"),
profileKey: \(profileKey?.description ?? "null"),
profilePictureURL: \(profilePictureURL ?? "null")
profilePictureUrl: \(profilePictureUrl ?? "null")
)
"""
}

View File

@ -6,43 +6,30 @@ import SessionUtilitiesKit
public extension VisibleMessage {
@objc(SNQuote)
class Quote: NSObject, Codable, NSCoding {
public var timestamp: UInt64?
public var publicKey: String?
public var text: String?
public var attachmentID: String?
struct Quote: Codable {
public let timestamp: UInt64?
public let publicKey: String?
public let text: String?
public let attachmentId: String?
public var isValid: Bool { timestamp != nil && publicKey != nil }
public override init() { super.init() }
internal init(timestamp: UInt64, publicKey: String, text: String?, attachmentID: String?) {
// MARK: - Initialization
internal init(timestamp: UInt64, publicKey: String, text: String?, attachmentId: String?) {
self.timestamp = timestamp
self.publicKey = publicKey
self.text = text
self.attachmentID = attachmentID
}
public required init?(coder: NSCoder) {
if let timestamp = coder.decodeObject(forKey: "timestamp") as! UInt64? { self.timestamp = timestamp }
if let publicKey = coder.decodeObject(forKey: "authorId") as! String? { self.publicKey = publicKey }
if let text = coder.decodeObject(forKey: "body") as! String? { self.text = text }
if let attachmentID = coder.decodeObject(forKey: "attachmentID") as! String? { self.attachmentID = attachmentID }
}
public func encode(with coder: NSCoder) {
coder.encode(timestamp, forKey: "timestamp")
coder.encode(publicKey, forKey: "authorId")
coder.encode(text, forKey: "body")
coder.encode(attachmentID, forKey: "attachmentID")
self.attachmentId = attachmentId
}
// MARK: - Proto Conversion
public static func fromProto(_ proto: SNProtoDataMessageQuote) -> Quote? {
let timestamp = proto.id
let publicKey = proto.author
let text = proto.text
return Quote(timestamp: timestamp, publicKey: publicKey, text: text, attachmentID: nil)
return Quote(timestamp: timestamp, publicKey: publicKey, text: text, attachmentId: nil)
}
public func toProto() -> SNProtoDataMessageQuote? {
@ -66,9 +53,9 @@ public extension VisibleMessage {
}
private func addAttachmentsIfNeeded(_ db: Database, to quoteProto: SNProtoDataMessageQuote.SNProtoDataMessageQuoteBuilder) {
guard let attachmentID = attachmentID else { return }
guard let attachmentId = attachmentId else { return }
guard
let attachment: SessionMessagingKit.Attachment = try? SessionMessagingKit.Attachment.fetchOne(db, id: attachmentID),
let attachment: SessionMessagingKit.Attachment = try? SessionMessagingKit.Attachment.fetchOne(db, id: attachmentId),
attachment.state != .uploaded
else {
#if DEBUG
@ -91,14 +78,15 @@ public extension VisibleMessage {
}
}
// MARK: Description
public override var description: String {
// MARK: - Description
public var description: String {
"""
Quote(
timestamp: \(timestamp?.description ?? "null"),
publicKey: \(publicKey ?? "null"),
text: \(text ?? "null"),
attachmentID: \(attachmentID ?? "null")
attachmentId: \(attachmentId ?? "null")
)
"""
}
@ -109,12 +97,11 @@ public extension VisibleMessage {
public extension VisibleMessage.Quote {
static func from(_ db: Database, quote: Quote) -> VisibleMessage.Quote {
let result = VisibleMessage.Quote()
result.timestamp = UInt64(quote.timestampMs)
result.publicKey = quote.authorId
result.text = quote.body
result.attachmentID = quote.attachmentId
return result
return VisibleMessage.Quote(
timestamp: UInt64(quote.timestampMs),
publicKey: quote.authorId,
text: quote.body,
attachmentId: quote.attachmentId
)
}
}

View File

@ -4,12 +4,11 @@ import Foundation
import GRDB
import SessionUtilitiesKit
@objc(SNVisibleMessage)
public final class VisibleMessage: Message {
private enum CodingKeys: String, CodingKey {
case syncTarget
case text = "body"
case attachmentIDs = "attachments"
case attachmentIds = "attachments"
case quote
case linkPreview
case profile
@ -20,65 +19,68 @@ public final class VisibleMessage: Message {
///
/// - Note: `nil` if this isn't a sync message.
public var syncTarget: String?
@objc public var text: String?
@objc public var attachmentIDs: [String] = []
@objc public var quote: Quote?
@objc public var linkPreview: LinkPreview?
@objc public var contact: Legacy.Contact?
@objc public var profile: Profile?
@objc public var openGroupInvitation: OpenGroupInvitation?
public let text: String?
public var attachmentIds: [String]
public let quote: Quote?
public let linkPreview: LinkPreview?
public var profile: Profile?
public let openGroupInvitation: OpenGroupInvitation?
public override var isSelfSendValid: Bool { true }
// MARK: Initialization
public override init() { super.init() }
// MARK: Validation
// MARK: - Validation
public override var isValid: Bool {
guard super.isValid else { return false }
if !attachmentIDs.isEmpty { return true }
if !attachmentIds.isEmpty { return true }
if openGroupInvitation != nil { return true }
if let text = text?.trimmingCharacters(in: .whitespacesAndNewlines), !text.isEmpty { return true }
return false
}
// MARK: Coding
public required init?(coder: NSCoder) {
super.init(coder: coder)
if let syncTarget = coder.decodeObject(forKey: "syncTarget") as! String? { self.syncTarget = syncTarget }
if let text = coder.decodeObject(forKey: "body") as! String? { self.text = text }
if let attachmentIDs = coder.decodeObject(forKey: "attachments") as! [String]? { self.attachmentIDs = attachmentIDs }
if let quote = coder.decodeObject(forKey: "quote") as! Quote? { self.quote = quote }
if let linkPreview = coder.decodeObject(forKey: "linkPreview") as! LinkPreview? { self.linkPreview = linkPreview }
if let profile = coder.decodeObject(forKey: "profile") as! Profile? { self.profile = profile }
if let openGroupInvitation = coder.decodeObject(forKey: "openGroupInvitation") as! OpenGroupInvitation? { self.openGroupInvitation = openGroupInvitation }
}
public override func encode(with coder: NSCoder) {
super.encode(with: coder)
coder.encode(syncTarget, forKey: "syncTarget")
coder.encode(text, forKey: "body")
coder.encode(attachmentIDs, forKey: "attachments")
coder.encode(quote, forKey: "quote")
coder.encode(linkPreview, forKey: "linkPreview")
coder.encode(profile, forKey: "profile")
coder.encode(openGroupInvitation, forKey: "openGroupInvitation")
// MARK: - Initialization
public init(
sentTimestamp: UInt64? = nil,
recipient: String? = nil,
groupPublicKey: String? = nil,
syncTarget: String? = nil,
text: String?,
attachmentIds: [String] = [],
quote: Quote? = nil,
linkPreview: LinkPreview? = nil,
profile: Profile? = nil,
openGroupInvitation: OpenGroupInvitation? = nil
) {
self.syncTarget = syncTarget
self.text = text
self.attachmentIds = attachmentIds
self.quote = quote
self.linkPreview = linkPreview
self.profile = profile
self.openGroupInvitation = openGroupInvitation
super.init(
sentTimestamp: sentTimestamp,
recipient: recipient,
groupPublicKey: groupPublicKey
)
}
// MARK: - Codable
required init(from decoder: Decoder) throws {
try super.init(from: decoder)
let container: KeyedDecodingContainer<CodingKeys> = try decoder.container(keyedBy: CodingKeys.self)
syncTarget = try? container.decode(String.self, forKey: .syncTarget)
text = try? container.decode(String.self, forKey: .text)
attachmentIDs = ((try? container.decode([String].self, forKey: .attachmentIDs)) ?? [])
attachmentIds = ((try? container.decode([String].self, forKey: .attachmentIds)) ?? [])
quote = try? container.decode(Quote.self, forKey: .quote)
linkPreview = try? container.decode(LinkPreview.self, forKey: .linkPreview)
profile = try? container.decode(Profile.self, forKey: .profile)
openGroupInvitation = try? container.decode(OpenGroupInvitation.self, forKey: .openGroupInvitation)
try super.init(from: decoder)
}
public override func encode(to encoder: Encoder) throws {
@ -88,32 +90,32 @@ public final class VisibleMessage: Message {
try container.encodeIfPresent(syncTarget, forKey: .syncTarget)
try container.encodeIfPresent(text, forKey: .text)
try container.encodeIfPresent(attachmentIDs, forKey: .attachmentIDs)
try container.encodeIfPresent(attachmentIds, forKey: .attachmentIds)
try container.encodeIfPresent(quote, forKey: .quote)
try container.encodeIfPresent(linkPreview, forKey: .linkPreview)
try container.encodeIfPresent(profile, forKey: .profile)
try container.encodeIfPresent(openGroupInvitation, forKey: .openGroupInvitation)
}
// MARK: Proto Conversion
// MARK: - Proto Conversion
public override class func fromProto(_ proto: SNProtoContent, sender: String) -> VisibleMessage? {
guard let dataMessage = proto.dataMessage else { return nil }
let result = VisibleMessage()
result.text = dataMessage.body
// Attachments are handled in MessageReceiver
if let quoteProto = dataMessage.quote, let quote = Quote.fromProto(quoteProto) { result.quote = quote }
if let linkPreviewProto = dataMessage.preview.first, let linkPreview = LinkPreview.fromProto(linkPreviewProto) { result.linkPreview = linkPreview }
// TODO: Contact
if let profile = Profile.fromProto(dataMessage) { result.profile = profile }
if let openGroupInvitationProto = dataMessage.openGroupInvitation,
let openGroupInvitation = OpenGroupInvitation.fromProto(openGroupInvitationProto) { result.openGroupInvitation = openGroupInvitation }
result.syncTarget = dataMessage.syncTarget
return result
return VisibleMessage(
syncTarget: dataMessage.syncTarget,
text: dataMessage.body,
attachmentIds: [], // Attachments are handled in MessageReceiver
quote: dataMessage.quote.map { Quote.fromProto($0) },
linkPreview: dataMessage.preview.first.map { LinkPreview.fromProto($0) },
profile: Profile.fromProto(dataMessage),
openGroupInvitation: dataMessage.openGroupInvitation.map { OpenGroupInvitation.fromProto($0) }
)
}
public override func toProto(_ db: Database) -> SNProtoContent? {
let proto = SNProtoContent.builder()
var attachmentIDs = self.attachmentIDs
var attachmentIds = self.attachmentIds
let dataMessage: SNProtoDataMessage.SNProtoDataMessageBuilder
// Profile
@ -138,8 +140,8 @@ public final class VisibleMessage: Message {
}
// Link preview
if let linkPreviewAttachmentID = linkPreview?.attachmentID, let index = attachmentIDs.firstIndex(of: linkPreviewAttachmentID) {
attachmentIDs.remove(at: index)
if let linkPreviewAttachmentId = linkPreview?.attachmentId, let index = attachmentIds.firstIndex(of: linkPreviewAttachmentId) {
attachmentIds.remove(at: index)
}
if let linkPreview = linkPreview, let linkPreviewProto = linkPreview.toProto(db) {
@ -148,7 +150,7 @@ public final class VisibleMessage: Message {
// Attachments
let attachments: [SessionMessagingKit.Attachment]? = try? SessionMessagingKit.Attachment.fetchAll(db, ids: self.attachmentIDs)
let attachments: [SessionMessagingKit.Attachment]? = try? SessionMessagingKit.Attachment.fetchAll(db, ids: self.attachmentIds)
if !(attachments ?? []).allSatisfy({ $0.state == .uploaded }) {
#if DEBUG
@ -158,8 +160,6 @@ public final class VisibleMessage: Message {
let attachmentProtos = (attachments ?? []).compactMap { $0.buildProto() }
dataMessage.setAttachments(attachmentProtos)
// TODO: Contact
// Open group invitation
if let openGroupInvitation = openGroupInvitation, let openGroupInvitationProto = openGroupInvitation.toProto() { dataMessage.setOpenGroupInvitation(openGroupInvitationProto) }
@ -184,15 +184,15 @@ public final class VisibleMessage: Message {
}
}
// MARK: Description
public override var description: String {
// MARK: - Description
public var description: String {
"""
VisibleMessage(
text: \(text ?? "null"),
attachmentIDs: \(attachmentIDs),
attachmentIds: \(attachmentIds),
quote: \(quote?.description ?? "null"),
linkPreview: \(linkPreview?.description ?? "null"),
contact: \(contact?.description ?? "null"),
profile: \(profile?.description ?? "null")
"openGroupInvitation": \(openGroupInvitation?.description ?? "null")
)

View File

@ -194,7 +194,7 @@ extension MessageReceiver {
threadId: thread.id,
authorId: sender,
variant: .infoDisappearingMessagesUpdate,
body: config.infoUpdateMessage(
body: config.messageInfoString(
with: (sender != getUserHexEncodedPublicKey(db) ?
Profile.displayName(db, id: sender) :
nil
@ -225,7 +225,7 @@ extension MessageReceiver {
db,
publicKey: userPublicKey,
name: message.displayName,
profilePictureUrl: message.profilePictureURL,
profilePictureUrl: message.profilePictureUrl,
profileKey: OWSAES256Key(data: message.profileKey),
sentTimestamp: messageSentTimestamp
)
@ -248,7 +248,7 @@ extension MessageReceiver {
try profile
.with(
name: contactInfo.displayName,
profilePictureUrl: .updateIf(contactInfo.profilePictureURL),
profilePictureUrl: .updateIf(contactInfo.profilePictureUrl),
profileEncryptionKey: .updateIf(
contactInfo.profileKey.map { OWSAES256Key(data: $0) }
)
@ -312,8 +312,8 @@ extension MessageReceiver {
guard !existingClosedGroupsIds.contains(closedGroup.publicKey) else { return }
let keyPair: Box.KeyPair = Box.KeyPair(
publicKey: closedGroup.encryptionKeyPair.publicKey.bytes,
secretKey: closedGroup.encryptionKeyPair.privateKey.bytes
publicKey: closedGroup.encryptionKeyPublicKey.bytes,
secretKey: closedGroup.encryptionKeySecretKey.bytes
)
try handleNewClosedGroup(
@ -409,7 +409,7 @@ extension MessageReceiver {
}
try attachments.saveAll(db)
message.attachmentIDs = attachments.map { $0.id }
message.attachmentIds = attachments.map { $0.id }
// Update profile if needed
if let profile = message.profile {
@ -420,7 +420,7 @@ extension MessageReceiver {
db,
publicKey: sender,
name: profile.displayName,
profilePictureUrl: profile.profilePictureURL,
profilePictureUrl: profile.profilePictureUrl,
profileKey: contactProfileKey,
sentTimestamp: messageSentTimestamp
)
@ -474,7 +474,7 @@ extension MessageReceiver {
interaction = try existingInteraction
.with(
serverHash: message.serverHash, // Keep track of server hash
openGroupServerMessageId: message.openGroupServerMessageID.map { Int64($0) }
openGroupServerMessageId: message.openGroupServerMessageId.map { Int64($0) }
)
.saved(db)
@ -494,7 +494,7 @@ extension MessageReceiver {
body: message.text,
timestampMs: Int64(messageSentTimestamp * 1000),
// Note: Ensure we don't ever expire open group messages
expiresInSeconds: (disappearingMessagesConfiguration.isEnabled && message.openGroupServerMessageID == nil ?
expiresInSeconds: (disappearingMessagesConfiguration.isEnabled && message.openGroupServerMessageId == nil ?
disappearingMessagesConfiguration.durationSeconds :
nil
),
@ -502,9 +502,9 @@ extension MessageReceiver {
// OpenGroupInvitations are stored as LinkPreview's in the database
linkPreviewUrl: (message.linkPreview?.url ?? message.openGroupInvitation?.url),
// Keep track of the open group server message ID message ID relationship
openGroupServerMessageId: message.openGroupServerMessageID.map { Int64($0) },
openGroupWhisperMods: false, // TODO: SOGSV4
openGroupWhisperTo: nil // TODO: SOGSV4
openGroupServerMessageId: message.openGroupServerMessageId.map { Int64($0) },
).inserted(db)
guard let newInteractionId: Int64 = interaction.id else { throw GRDBStorageError.failedToSave }

View File

@ -147,7 +147,7 @@ public enum MessageReceiver {
message.sentTimestamp = envelope.timestamp
message.receivedTimestamp = UInt64((Date().timeIntervalSince1970) * 1000)
message.groupPublicKey = groupPublicKey
message.openGroupServerMessageID = openGroupMessageServerId
message.openGroupServerMessageId = openGroupMessageServerId
// Validate
var isValid: Bool = message.isValid

View File

@ -147,7 +147,11 @@ public final class MessageSender : NSObject {
let profile: Profile = Profile.fetchOrCreateCurrentUser(db)
if let profileKey: Data = profile.profileEncryptionKey?.keyData, let profilePictureUrl: String = profile.profilePictureUrl {
message.profile = VisibleMessage.Profile(displayName: profile.name, profileKey: profileKey, profilePictureURL: profilePictureUrl)
message.profile = VisibleMessage.Profile(
displayName: profile.name,
profileKey: profileKey,
profilePictureUrl: profilePictureUrl
)
}
else {
message.profile = VisibleMessage.Profile(displayName: profile.name)
@ -400,7 +404,7 @@ public final class MessageSender : NSObject {
on: server
)
.done(on: DispatchQueue.global(qos: .userInitiated)) { openGroupMessage in
message.openGroupServerMessageID = given(openGroupMessage.serverID) { UInt64($0) }
message.openGroupServerMessageId = given(openGroupMessage.serverID) { UInt64($0) }
GRDBStorage.shared.write { db in
try MessageSender.handleSuccessfulMessageSend(
@ -445,11 +449,11 @@ public final class MessageSender : NSObject {
// Track the open group server message ID and update server timestamp (use server
// timestamp for open group messages otherwise the quote messages may not be able
// to be found by the timestamp on other devices
timestampMs: (message.openGroupServerMessageID == nil ?
timestampMs: (message.openGroupServerMessageId == nil ?
nil :
serverTimestampMs.map { Int64($0) }
),
openGroupServerMessageId: message.openGroupServerMessageID.map { Int64($0) }
openGroupServerMessageId: message.openGroupServerMessageId.map { Int64($0) }
).update(db)
// Mark the message as sent
@ -489,14 +493,14 @@ public final class MessageSender : NSObject {
}
}(),
sentTimestampMs: {
if message.openGroupServerMessageID != nil {
if message.openGroupServerMessageId != nil {
return (serverTimestampMs.map { Int64($0) } ?? 0)
}
return (message.sentTimestamp.map { Int64($0) } ?? 0)
}(),
serverHash: (message.serverHash ?? ""),
openGroupMessageServerId: (message.openGroupServerMessageID.map { Int64($0) } ?? 0)
openGroupMessageServerId: (message.openGroupServerMessageId.map { Int64($0) } ?? 0)
).insert(db)
}
// Sync the message if:
@ -553,7 +557,7 @@ public final class MessageSender : NSObject {
else if let sentTimestamp: Double = message.sentTimestamp.map({ Double($0) }) {
// If we have a threadId then include that in the filter to make the request smaller
if
let threadId: String = message.threadID,
let threadId: String = message.threadId,
!threadId.isEmpty,
let thread: SessionThread = try? SessionThread.fetchOne(db, id: threadId)
{

View File

@ -2,9 +2,11 @@
public extension Notification.Name {
static let initialConfigurationMessageReceived = Notification.Name("initialConfigurationMessageReceived")
static let incomingMessageMarkedAsRead = Notification.Name("incomingMessageMarkedAsRead")
}
@objc public extension NSNotification {
@objc static let initialConfigurationMessageReceived = Notification.Name.initialConfigurationMessageReceived.rawValue as NSString
@objc static let incomingMessageMarkedAsRead = Notification.Name.incomingMessageMarkedAsRead.rawValue as NSString
}

View File

@ -18,53 +18,48 @@ public enum Legacy {
public typealias LegacyOnionRequestAPIPath = [Snode]
@objc(Snode)
public final class Snode: NSObject, NSCoding { // NSObject/NSCoding conformance is needed for YapDatabase compatibility
public final class Snode: NSObject, NSCoding {
public let address: String
public let port: UInt16
public let publicKeySet: KeySet
public var ip: String {
guard let range = address.range(of: "https://"), range.lowerBound == address.startIndex else { return address }
return String(address[range.upperBound..<address.endIndex])
}
// MARK: Nested Types
// MARK: - Nested Types
public struct KeySet {
public let ed25519Key: String
public let x25519Key: String
}
// MARK: Initialization
internal init(address: String, port: UInt16, publicKeySet: KeySet) {
self.address = address
self.port = port
self.publicKeySet = publicKeySet
}
// MARK: Coding
// MARK: - NSCoding
public init?(coder: NSCoder) {
address = coder.decodeObject(forKey: "address") as! String
port = coder.decodeObject(forKey: "port") as! UInt16
guard let idKey = coder.decodeObject(forKey: "idKey") as? String,
let encryptionKey = coder.decodeObject(forKey: "encryptionKey") as? String else { return nil }
guard
let idKey = coder.decodeObject(forKey: "idKey") as? String,
let encryptionKey = coder.decodeObject(forKey: "encryptionKey") as? String
else { return nil }
publicKeySet = KeySet(ed25519Key: idKey, x25519Key: encryptionKey)
super.init()
}
public func encode(with coder: NSCoder) {
coder.encode(address, forKey: "address")
coder.encode(port, forKey: "port")
coder.encode(publicKeySet.ed25519Key, forKey: "idKey")
coder.encode(publicKeySet.x25519Key, forKey: "encryptionKey")
fatalError("encode(with:) should never be called for legacy types")
}
// Note: The 'isEqual' and 'hash' overrides are both needed to ensure the migration
// doesn't try to insert duplicate SNode entries into the new database (which would
// result in unique key constraint violations)
override public func isEqual(_ other: Any?) -> Bool {
guard let other = other as? Snode else { return false }
return address == other.address && port == other.port
}
override public var hash: Int { // Override NSObject.hash and not Hashable.hashValue or Hashable.hash(into:)
override public var hash: Int {
return address.hashValue ^ port.hashValue
}
}

View File

@ -15,6 +15,12 @@ enum _003_YDBToGRDBMigration: Migration {
var snodeSetResult: [String: Set<Legacy.Snode>] = [:]
var lastSnodePoolRefreshDate: Date? = nil
// Map the Legacy types for the NSKeyedUnarchiver
NSKeyedUnarchiver.setClass(
Legacy.Snode.self,
forClassName: "SessionSnodeKit.Snode"
)
Storage.read { transaction in
// Process the lastSnodePoolRefreshDate
lastSnodePoolRefreshDate = transaction.object(

View File

@ -2,6 +2,8 @@
import Foundation
public typealias SUKLegacy = Legacy
public enum Legacy {
// MARK: - Collections and Keys
@ -14,7 +16,7 @@ public enum Legacy {
internal static let identityKeyStoreIdentityKey = "TSStorageManagerIdentityKeyStoreIdentityKey"
internal static let identityKeyStoreCollection = "TSStorageManagerIdentityKeyStoreCollection"
@objc(ECKeyPair)
@objc(LegacyKeyPair)
public class KeyPair: NSObject, NSCoding {
private static let keyLength: Int = 32
private static let publicKeyKey: String = "TSECKeyPairPublicKey"
@ -23,6 +25,14 @@ public enum Legacy {
public let publicKey: Data
public let privateKey: Data
public init(
publicKeyData: Data,
privateKeyData: Data
) {
publicKey = publicKeyData
privateKey = privateKeyData
}
public required init?(coder: NSCoder) {
var pubKeyLength: Int = 0
var privKeyLength: Int = 0

View File

@ -16,6 +16,12 @@ enum _003_YDBToGRDBMigration: Migration {
var userEd25519PublicKeyHexString: String?
var userX25519KeyPair: Legacy.KeyPair?
// Map the Legacy types for the NSKeyedUnarchiver
NSKeyedUnarchiver.setClass(
Legacy.KeyPair.self,
forClassName: "ECKeyPair"
)
Storage.read { transaction in
registeredNumber = transaction.object(
forKey: Legacy.userAccountRegisteredNumberKey,

View File

@ -0,0 +1,14 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import Foundation
import GRDB
public class TypedTableAlias<T> where T: TableRecord, T: ColumnExpressible {
let alias: TableAlias = TableAlias(name: T.databaseTableName)
public init() {}
public subscript(_ column: T.Columns) -> SQLExpression {
return alias[column.name]
}
}

View File

@ -10,7 +10,7 @@ public extension Array where Element: PersistableRecord {
}
}
@discardableResult func saveAll(_ db: Database) throws {
func saveAll(_ db: Database) throws {
try forEach { try $0.save(db) }
}
}

View File

@ -0,0 +1,26 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import Foundation
import GRDB
public extension QueryInterfaceRequest {
/// Returns true if the request matches a row in the database.
///
/// try Player.filter(Column("name") == "Arthur").isEmpty(db)
///
/// - parameter db: A database connection.
/// - returns: Whether the request matches a row in the database.
func isNotEmpty(_ db: Database) throws -> Bool {
return ((try? SQLRequest("SELECT \(exists())").fetchOne(db)) ?? false)
}
}
public extension QueryInterfaceRequest where RowDecoder: ColumnExpressible {
func select(_ selection: RowDecoder.Columns...) -> Self {
select(selection)
}
func order(_ orderings: RowDecoder.Columns...) -> QueryInterfaceRequest {
order(orderings)
}
}

View File

@ -0,0 +1,10 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import Foundation
import GRDB
public extension TableRecord where Self: ColumnExpressible {
static func select(_ selection: Columns...) -> QueryInterfaceRequest<Self> {
return all().select(selection)
}
}

View File

@ -12,12 +12,16 @@ public extension UITableView {
}
func dequeue<T>(type: T.Type, for indexPath: IndexPath) -> T where T: UITableViewCell {
let reuseIdentifier = T.defaultReuseIdentifier
// Note: We need to use `type.defaultReuseIdentifier` rather than `T.defaultReuseIdentifier`
// otherwise we may get a subclass rather than the actual type we specified
let reuseIdentifier = type.defaultReuseIdentifier
return dequeueReusableCell(withIdentifier: reuseIdentifier, for: indexPath) as! T
}
func dequeueHeaderFooterView<T>(type: T.Type) -> T where T: UITableViewHeaderFooterView {
let reuseIdentifier = T.defaultReuseIdentifier
// Note: We need to use `type.defaultReuseIdentifier` rather than `T.defaultReuseIdentifier`
// otherwise we may get a subclass rather than the actual type we specified
let reuseIdentifier = type.defaultReuseIdentifier
return dequeueReusableHeaderFooterView(withIdentifier: reuseIdentifier) as! T
}
}

View File

@ -365,7 +365,7 @@ public final class JobRunner {
db,
Job// TODO: Test this works as expected
.filterPendingJobs(excludeFutureJobs: false)
.select(Job.Columns.nextRunTimestamp)
.select(.nextRunTimestamp)
)
}

View File

@ -1,3 +1,5 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import Foundation
import AVFoundation

View File

@ -0,0 +1,57 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import Foundation
public extension NSAttributedString {
static func with(_ attrStrings: [NSAttributedString]) -> NSAttributedString {
let mutableString: NSMutableAttributedString = NSMutableAttributedString()
for attrString in attrStrings {
mutableString.append(attrString)
}
return mutableString
}
func appending(_ attrString: NSAttributedString) -> NSAttributedString {
let mutableString: NSMutableAttributedString = NSMutableAttributedString(attributedString: self)
mutableString.append(attrString)
return mutableString
}
func appending(string: String, attributes: [Key: Any]? = nil) -> NSAttributedString {
return appending(NSAttributedString(string: string, attributes: attributes))
}
// The actual Swift implementation of 'uppercased' is pretty nuts (see
// https://github.com/apple/swift/blob/main/stdlib/public/core/String.swift#L901)
// this approach is definitely less efficient but is much simpler and less likely to break
private enum CharacterCasing {
static let map: [UTF16.CodeUnit: String.UTF16View] = [
"a": "A", "b": "B", "c": "C", "d": "D", "e": "E", "f": "F", "g": "G",
"h": "H", "i": "I", "j": "J", "k": "K", "l": "L", "m": "M", "n": "N",
"o": "O", "p": "P", "q": "Q", "r": "R", "s": "S", "t": "T", "u": "U",
"v": "V", "w": "W", "x": "X", "y": "Y", "z": "Z"
]
.reduce(into: [:]) { prev, next in
prev[next.key.utf16.first ?? UTF16.CodeUnit()] = next.value.utf16
}
}
func uppercased() -> NSAttributedString {
let result = NSMutableAttributedString(attributedString: self)
let uppercasedCharacters = result.string.utf16.map { utf16Char in
// Try convert the individual utf16 character to it's uppercase variant
// or fallback to the original character
(CharacterCasing.map[utf16Char]?.first ?? utf16Char)
}
result.replaceCharacters(
in: NSRange(location: 0, length: length),
with: String(utf16CodeUnits: uppercasedCharacters, count: length)
)
return result
}
}

View File

@ -21,3 +21,11 @@ extension Optional {
return (self ?? value)
}
}
extension Optional where Wrapped == String {
public func defaulting(to value: Wrapped, useDefaultIfEmpty: Bool = false) -> Wrapped {
guard !useDefaultIfEmpty || self?.isEmpty != true else { return value }
return (self ?? value)
}
}

View File

@ -16,24 +16,24 @@ public class ContactsMigration : OWSDatabaseMigration {
}
private func doMigrationAsync(completion: @escaping OWSDatabaseMigrationCompletion) {
var contacts: [SessionMessagingKit.Legacy.Contact] = []
var contacts: [SMKLegacy.Contact] = []
TSContactThread.enumerateCollectionObjects { object, _ in
guard let thread = object as? TSContactThread else { return }
let sessionID = thread.contactSessionID()
var contact: SessionMessagingKit.Legacy.Contact?
var contact: SMKLegacy.Contact?
Storage.read { transaction in
contact = transaction.object(forKey: sessionID, inCollection: Legacy.contactCollection) as? SessionMessagingKit.Legacy.Contact
contact = transaction.object(forKey: sessionID, inCollection: SMKLegacy.contactCollection) as? SMKLegacy.Contact
}
if let contact: SessionMessagingKit.Legacy.Contact = contact {
if let contact: SMKLegacy.Contact = contact {
contact.isTrusted = true
contacts.append(contact)
}
}
Storage.write(with: { transaction in
contacts.forEach { contact in
transaction.setObject(contact, forKey: contact.sessionID, inCollection: Legacy.contactCollection)
transaction.setObject(contact, forKey: contact.sessionID, inCollection: SMKLegacy.contactCollection)
}
self.save(with: transaction) // Intentionally capture self
}, completion: {

View File

@ -4,10 +4,13 @@ public final class Identicon : NSObject {
@objc public static func generatePlaceholderIcon(seed: String, text: String, size: CGFloat) -> UIImage {
let icon = PlaceholderIcon(seed: seed)
var content = text
if content.count > 2 && content.hasPrefix("05") {
content.removeFirst(2)
}
let initials: String = content
.split(separator: " ")
.compactMap { word in word.first.map { String($0) } }
@ -19,8 +22,10 @@ public final class Identicon : NSObject {
content.substring(to: 2).uppercased()
)
)
let rect = CGRect(origin: CGPoint.zero, size: layer.frame.size)
let renderer = UIGraphicsImageRenderer(size: rect.size)
return renderer.image { layer.render(in: $0.cgContext) }
}
}