diff --git a/Session.xcodeproj/project.pbxproj b/Session.xcodeproj/project.pbxproj index 2a1143247..cd0593208 100644 --- a/Session.xcodeproj/project.pbxproj +++ b/Session.xcodeproj/project.pbxproj @@ -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 = ""; }; 34480B371FD092A900BC14EF /* SignalShareExtension-Bridging-Header.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "SignalShareExtension-Bridging-Header.h"; sourceTree = ""; }; 34480B381FD092E300BC14EF /* SessionShareExtension-Prefix.pch */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "SessionShareExtension-Prefix.pch"; sourceTree = ""; }; - 346129971FD1E4D900532771 /* SignalApp.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SignalApp.m; sourceTree = ""; }; - 346129981FD1E4DA00532771 /* SignalApp.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SignalApp.h; sourceTree = ""; }; 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 = ""; }; 3488F9352191CC4000E524CC /* MediaView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MediaView.swift; sourceTree = ""; }; @@ -1206,7 +1208,6 @@ B821494E25D4E163009C0F2A /* BodyTextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BodyTextView.swift; sourceTree = ""; }; B82149B725D60393009C0F2A /* BlockedModal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlockedModal.swift; sourceTree = ""; }; B82149C025D605C6009C0F2A /* InfoBanner.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InfoBanner.swift; sourceTree = ""; }; - B8214A2A25D63EB9009C0F2A /* MessagesTableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessagesTableView.swift; sourceTree = ""; }; B8269D2825C7A4B400488AB4 /* InputView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InputView.swift; sourceTree = ""; }; B8269D3225C7A8C600488AB4 /* InputViewButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InputViewButton.swift; sourceTree = ""; }; B8269D3C25C7B34D00488AB4 /* InputTextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InputTextView.swift; sourceTree = ""; }; @@ -1288,7 +1289,6 @@ B8D0A24F25E3678700C1835E /* LinkDeviceVC.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LinkDeviceVC.swift; sourceTree = ""; }; 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 = ""; }; - B8D84E9325DF72AF005A043E /* ConversationViewAction.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ConversationViewAction.h; sourceTree = ""; }; B8D84EA225DF745A005A043E /* LinkPreviewState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LinkPreviewState.swift; sourceTree = ""; }; B8D84ECE25E3108A005A043E /* ExpandingAttachmentsButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExpandingAttachmentsButton.swift; sourceTree = ""; }; B8D8F1372566120F0092EF10 /* Storage+ClosedGroups.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Storage+ClosedGroups.swift"; sourceTree = ""; }; @@ -1855,11 +1855,12 @@ FD6A7A6A2818C17C00035AC1 /* UpdateProfilePictureJob.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateProfilePictureJob.swift; sourceTree = ""; }; FD6A7A6C2818C61500035AC1 /* _002_SetupStandardJobs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _002_SetupStandardJobs.swift; sourceTree = ""; }; FD705A8B278CDB5600F16121 /* SAEScreenLockViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SAEScreenLockViewController.swift; sourceTree = ""; }; - FD705A8D278CE29800F16121 /* String+Localization.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+Localization.swift"; sourceTree = ""; }; + FD705A8D278CE29800F16121 /* String+Localized.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+Localized.swift"; sourceTree = ""; }; FD705A8F278CEBBC00F16121 /* ShareAppExtensionContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareAppExtensionContext.swift; sourceTree = ""; }; FD705A91278D051200F16121 /* ReusableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReusableView.swift; sourceTree = ""; }; FD705A93278D052B00F16121 /* UITableView+ReusableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UITableView+ReusableView.swift"; sourceTree = ""; }; FD705A97278E9F4D00F16121 /* UIColor+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIColor+Extensions.swift"; sourceTree = ""; }; + FD7162DA281B6C440060647B /* TypedTableAlias.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TypedTableAlias.swift; sourceTree = ""; }; FD859EFF27C4691300510D0C /* MockDataGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockDataGenerator.swift; sourceTree = ""; }; FD88BAD827A7439C00BBC442 /* MessageRequestsCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageRequestsCell.swift; sourceTree = ""; }; FD88BADA27A750F200BBC442 /* MessageRequestsMigration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageRequestsMigration.swift; sourceTree = ""; }; @@ -1888,6 +1889,11 @@ FDF0B7592807F3A3004C14C5 /* MessageSenderError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageSenderError.swift; sourceTree = ""; }; FDF0B75B2807F41D004C14C5 /* MessageSender+Convenience.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "MessageSender+Convenience.swift"; sourceTree = ""; }; FDF0B75D280AAF35004C14C5 /* Preferences.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Preferences.swift; sourceTree = ""; }; + FDF222062818CECF000A4995 /* ConversationViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConversationViewModel.swift; sourceTree = ""; }; + FDF222082818D2B0000A4995 /* NSAttributedString+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSAttributedString+Utilities.swift"; sourceTree = ""; }; + FDF2220A2818F38D000A4995 /* SessionApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionApp.swift; sourceTree = ""; }; + FDF2220E281B55E6000A4995 /* QueryInterfaceRequest+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "QueryInterfaceRequest+Utilities.swift"; sourceTree = ""; }; + FDF22210281B5E0B000A4995 /* TableRecord+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TableRecord+Utilities.swift"; sourceTree = ""; }; FDFD645727EC1F4000808CA1 /* Atomic.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Atomic.swift; sourceTree = ""; }; 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 = ""; }; 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 = ""; }; @@ -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 = ""; @@ -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 = ""; @@ -3734,6 +3739,7 @@ FD17D7B727F51ECA00122BE0 /* Migration.swift */, FD17D7B927F51F2100122BE0 /* TargetMigrations.swift */, FD17D7C027F5200100122BE0 /* TypedTableDefinition.swift */, + FD7162DA281B6C440060647B /* TypedTableAlias.swift */, ); path = Types; sourceTree = ""; @@ -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 = ""; @@ -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 */, diff --git a/Session/Conversations/ConversationViewAction.h b/Session/Conversations/ConversationViewAction.h deleted file mode 100644 index 17d7cc29a..000000000 --- a/Session/Conversations/ConversationViewAction.h +++ /dev/null @@ -1,8 +0,0 @@ -@import Foundation; - -typedef NS_ENUM(NSUInteger, ConversationViewAction) { - ConversationViewActionNone, - ConversationViewActionCompose, - ConversationViewActionAudioCall, - ConversationViewActionVideoCall, -}; diff --git a/Session/Conversations/ConversationViewModel.swift b/Session/Conversations/ConversationViewModel.swift new file mode 100644 index 000000000..0a8ab0dac --- /dev/null +++ b/Session/Conversations/ConversationViewModel.swift @@ -0,0 +1,3 @@ +// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. + +import Foundation diff --git a/Session/Conversations/Views & Modals/MessagesTableView.swift b/Session/Conversations/Views & Modals/MessagesTableView.swift deleted file mode 100644 index 033664ca8..000000000 --- a/Session/Conversations/Views & Modals/MessagesTableView.swift +++ /dev/null @@ -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 - } -} diff --git a/Session/Home/HomeVC.swift b/Session/Home/HomeVC.swift index 5adcfecde..f3ba0f89c 100644 --- a/Session/Home/HomeVC.swift +++ b/Session/Home/HomeVC.swift @@ -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 { diff --git a/Session/Meta/AppDelegate.swift b/Session/Meta/AppDelegate.swift index 2683b0d5c..e589bfda6 100644 --- a/Session/Meta/AppDelegate.swift +++ b/Session/Meta/AppDelegate.swift @@ -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 } diff --git a/Session/Meta/SessionApp.swift b/Session/Meta/SessionApp.swift new file mode 100644 index 000000000..d23fe650b --- /dev/null +++ b/Session/Meta/SessionApp.swift @@ -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 = 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 + } +} diff --git a/Session/Meta/SignalApp.h b/Session/Meta/SignalApp.h deleted file mode 100644 index 8583ed672..000000000 --- a/Session/Meta/SignalApp.h +++ /dev/null @@ -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 diff --git a/Session/Meta/SignalApp.m b/Session/Meta/SignalApp.m deleted file mode 100644 index 7c1e40fcd..000000000 --- a/Session/Meta/SignalApp.m +++ /dev/null @@ -1,167 +0,0 @@ -// -// Copyright (c) 2019 Open Whisper Systems. All rights reserved. -// - -#import "SignalApp.h" -#import "Session-Swift.h" -#import -#import -#import -#import -#import - -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 diff --git a/Session/Notifications/AppNotifications.swift b/Session/Notifications/AppNotifications.swift index 40964537a..429fd4b1c 100644 --- a/Session/Notifications/AppNotifications.swift +++ b/Session/Notifications/AppNotifications.swift @@ -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 { - signalApp.showHomeView() + SessionApp.showHomeView() return Promise.value(()) } diff --git a/Session/Settings/NotificationSettingsOptionsViewController.m b/Session/Settings/NotificationSettingsOptionsViewController.m index ebb120b40..edc613765 100644 --- a/Session/Settings/NotificationSettingsOptionsViewController.m +++ b/Session/Settings/NotificationSettingsOptionsViewController.m @@ -4,7 +4,6 @@ #import "NotificationSettingsOptionsViewController.h" #import "Session-Swift.h" -#import "SignalApp.h" #import #import diff --git a/Session/Settings/PrivacySettingsTableViewController.m b/Session/Settings/PrivacySettingsTableViewController.m index ea6ac16ef..a6e99f674 100644 --- a/Session/Settings/PrivacySettingsTableViewController.m +++ b/Session/Settings/PrivacySettingsTableViewController.m @@ -13,7 +13,6 @@ #import #import #import -#import #import #import @@ -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; diff --git a/Session/Settings/SettingsVC.swift b/Session/Settings/SettingsVC.swift index cb64e2c5f..3ea7b2fe0 100644 --- a/Session/Settings/SettingsVC.swift +++ b/Session/Settings/SettingsVC.swift @@ -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 } } }, diff --git a/Session/Shared/UserCell.swift b/Session/Shared/UserCell.swift index af665877f..dea13b9ce 100644 --- a/Session/Shared/UserCell.swift +++ b/Session/Shared/UserCell.swift @@ -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 { diff --git a/SessionMessagingKit/Database/LegacyDatabase/SMKLegacyModels.swift b/SessionMessagingKit/Database/LegacyDatabase/SMKLegacyModels.swift index 19a5895f4..693ccde4a 100644 --- a/SessionMessagingKit/Database/LegacyDatabase/SMKLegacyModels.swift +++ b/SessionMessagingKit/Database/LegacyDatabase/SMKLegacyModels.swift @@ -2,8 +2,12 @@ import Foundation import Mantle +import Sodium import YapDatabase import SignalCoreKit +import SessionUtilitiesKit + +public typealias SMKLegacy = Legacy public enum Legacy { // MARK: - Collections and Keys @@ -29,7 +33,7 @@ public enum Legacy { internal static let openGroupServerIdToUniqueIdLookupCollection = "SNOpenGroupServerIdToUniqueIdLookup" internal static let interactionCollection = "TSInteraction" - internal static let attachmentsCollection = "TSAttachements" + internal static let attachmentsCollection = "TSAttachements" // Note: This is how it was previously spelt internal static let outgoingReadReceiptManagerCollection = "kOutgoingReadReceiptManagerCollection" internal static let notifyPushServerJobCollection = "NotifyPNServerJobCollection" @@ -55,16 +59,890 @@ public enum Legacy { internal static let userDefaultsHasHiddenMessageRequests = "hasHiddenMessageRequests" - // MARK: - Types + // MARK: - Types (and NSCoding) - public typealias Contact = _LegacyContact - public typealias DisappearingMessagesConfiguration = _LegacyDisappearingMessagesConfiguration + @objc(SNContact) + public class Contact: NSObject, NSCoding { + @objc public let sessionID: String + @objc public var profilePictureURL: String? + @objc public var profilePictureFileName: String? + @objc public var profileEncryptionKey: OWSAES256Key? + @objc public var threadID: String? + @objc public var isTrusted = false + @objc public var isApproved = false + @objc public var isBlocked = false + @objc public var didApproveMe = false + @objc public var hasBeenBlocked = false + @objc public var name: String? + @objc public var nickname: String? + + // MARK: Coding + + public required init?(coder: NSCoder) { + guard let sessionID = coder.decodeObject(forKey: "sessionID") as! String? else { return nil } + self.sessionID = sessionID + isTrusted = coder.decodeBool(forKey: "isTrusted") + if let name = coder.decodeObject(forKey: "displayName") as! String? { self.name = name } + if let nickname = coder.decodeObject(forKey: "nickname") as! String? { self.nickname = nickname } + if let profilePictureURL = coder.decodeObject(forKey: "profilePictureURL") as! String? { self.profilePictureURL = profilePictureURL } + if let profilePictureFileName = coder.decodeObject(forKey: "profilePictureFileName") as! String? { self.profilePictureFileName = profilePictureFileName } + if let profileEncryptionKey = coder.decodeObject(forKey: "profilePictureEncryptionKey") as! OWSAES256Key? { self.profileEncryptionKey = profileEncryptionKey } + if let threadID = coder.decodeObject(forKey: "threadID") as! String? { self.threadID = threadID } + + let isBlockedFlag: Bool = coder.decodeBool(forKey: "isBlocked") + isApproved = coder.decodeBool(forKey: "isApproved") + isBlocked = isBlockedFlag + didApproveMe = coder.decodeBool(forKey: "didApproveMe") + hasBeenBlocked = (coder.decodeBool(forKey: "hasBeenBlocked") || isBlockedFlag) + } + + public func encode(with coder: NSCoder) { + fatalError("encode(with:) should never be called for legacy types") + } + } + + @objc(OWSDisappearingMessagesConfiguration) + internal class DisappearingMessagesConfiguration: MTLModel { + @objc public let uniqueId: String + @objc public var isEnabled: Bool + @objc public var durationSeconds: UInt32 + + // MARK: - NSCoder + + required init(coder: NSCoder) { + self.uniqueId = coder.decodeObject(forKey: "uniqueId") as! String + self.isEnabled = coder.decodeObject(forKey: "enabled") as! Bool + self.durationSeconds = coder.decodeObject(forKey: "durationSeconds") as! UInt32 + + // Intentionally not calling 'super.init(coder:) here + super.init() + } + + required init(dictionary dictionaryValue: [String : Any]!) throws { + fatalError("init(dictionary:) has not been implemented") + } + + override public func encode(with coder: NSCoder) { + fatalError("encode(with:) should never be called for legacy types") + } + } + + // MARK: - Visible/Control Message NSCoding + + /// Abstract base class for `VisibleMessage` and `ControlMessage`. + @objc(SNMessage) + internal class Message: NSObject, NSCoding { + internal var id: String? + internal var threadID: String? + internal var sentTimestamp: UInt64? + internal var receivedTimestamp: UInt64? + internal var recipient: String? + internal var sender: String? + internal var groupPublicKey: String? + internal var openGroupServerMessageID: UInt64? + internal var openGroupServerTimestamp: UInt64? + internal var serverHash: String? + + // MARK: NSCoding + + 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 } + } + + public func encode(with coder: NSCoder) { + fatalError("encode(with:) should never be called for legacy types") + } + + // MARK: Non-Legacy Conversion + + internal func toNonLegacy(_ instance: SessionMessagingKit.Message? = nil) throws -> SessionMessagingKit.Message { + let result: SessionMessagingKit.Message = (instance ?? SessionMessagingKit.Message()) + result.id = self.id + result.threadId = self.threadID + result.sentTimestamp = self.sentTimestamp + result.receivedTimestamp = self.receivedTimestamp + result.recipient = self.recipient + result.sender = self.sender + result.groupPublicKey = self.groupPublicKey + result.openGroupServerMessageId = self.openGroupServerMessageID + result.openGroupServerTimestamp = self.openGroupServerTimestamp + result.serverHash = self.serverHash + + return result + } + } + + @objc(SNVisibleMessage) + internal final class VisibleMessage: Message { + internal var syncTarget: String? + internal var text: String? + internal var attachmentIDs: [String] = [] + internal var quote: Quote? + internal var linkPreview: LinkPreview? + internal var profile: Profile? + internal var openGroupInvitation: OpenGroupInvitation? + + // MARK: - NSCoding + + 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) { + fatalError("encode(with:) should never be called for legacy types") + } + + // MARK: Non-Legacy Conversion + + override internal func toNonLegacy(_ instance: SessionMessagingKit.Message? = nil) throws -> SessionMessagingKit.Message { + return try super.toNonLegacy( + SessionMessagingKit.VisibleMessage( + syncTarget: syncTarget, + text: text, + attachmentIds: attachmentIDs, + quote: quote?.toNonLegacy(), + linkPreview: linkPreview?.toNonLegacy(), + profile: profile?.toNonLegacy(), + openGroupInvitation: openGroupInvitation?.toNonLegacy() + ) + ) + } + } + + @objc(SNQuote) + internal class Quote: NSObject, NSCoding { + internal var timestamp: UInt64? + internal var publicKey: String? + internal var text: String? + internal var attachmentID: String? + + // MARK: - NSCoding + + 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) { + fatalError("encode(with:) should never be called for legacy types") + } + + // MARK: Non-Legacy Conversion + + internal func toNonLegacy() -> SessionMessagingKit.VisibleMessage.Quote { + return SessionMessagingKit.VisibleMessage.Quote( + timestamp: (timestamp ?? 0), + publicKey: (publicKey ?? ""), + text: text, + attachmentId: attachmentID + ) + } + } + + @objc(SNLinkPreview) + internal class LinkPreview: NSObject, NSCoding { + internal var title: String? + internal var url: String? + internal var attachmentID: String? + + // MARK: - NSCoding + + 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) { + fatalError("encode(with:) should never be called for legacy types") + } + + // MARK: Non-Legacy Conversion + + internal func toNonLegacy() -> SessionMessagingKit.VisibleMessage.LinkPreview { + return SessionMessagingKit.VisibleMessage.LinkPreview( + title: title, + url: (url ?? ""), + attachmentId: attachmentID + ) + } + } @objc(SNProfile) - public class Profile: NSObject, NSCoding { - public var displayName: String? - public var profileKey: Data? - public var profilePictureURL: String? + internal class Profile: NSObject, NSCoding { + internal var displayName: String? + internal var profileKey: Data? + internal var profilePictureURL: String? + + // MARK: - NSCoding + + 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) { + fatalError("encode(with:) should never be called for legacy types") + } + + // MARK: Non-Legacy Conversion + + internal func toNonLegacy() -> SessionMessagingKit.VisibleMessage.Profile { + return SessionMessagingKit.VisibleMessage.Profile( + displayName: (displayName ?? ""), + profileKey: profileKey, + profilePictureUrl: profilePictureURL + ) + } + } + + @objc(SNOpenGroupInvitation) + internal class OpenGroupInvitation: NSObject, NSCoding { + internal var name: String? + internal var url: String? + + // MARK: - NSCoding + + 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) { + fatalError("encode(with:) should never be called for legacy types") + } + + // MARK: Non-Legacy Conversion + + internal func toNonLegacy() -> SessionMessagingKit.VisibleMessage.OpenGroupInvitation { + return SessionMessagingKit.VisibleMessage.OpenGroupInvitation( + name: (name ?? ""), + url: (url ?? "") + ) + } + } + + @objc(SNControlMessage) + internal class ControlMessage: Message {} + + @objc(SNReadReceipt) + internal final class ReadReceipt: ControlMessage { + internal var timestamps: [UInt64]? + + // MARK: - NSCoding + + 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) { + fatalError("encode(with:) should never be called for legacy types") + } + + // MARK: Non-Legacy Conversion + + override internal func toNonLegacy(_ instance: SessionMessagingKit.Message? = nil) throws -> SessionMessagingKit.Message { + return try super.toNonLegacy( + SessionMessagingKit.ReadReceipt( + timestamps: (timestamps ?? []) + ) + ) + } + } + + @objc(SNTypingIndicator) + internal final class TypingIndicator: ControlMessage { + public var rawKind: Int? + + // MARK: - NSCoding + + public required init?(coder: NSCoder) { + super.init(coder: coder) + + self.rawKind = coder.decodeObject(forKey: "action") as! Int? + } + + public override func encode(with coder: NSCoder) { + fatalError("encode(with:) should never be called for legacy types") + } + + // MARK: Non-Legacy Conversion + + override internal func toNonLegacy(_ instance: SessionMessagingKit.Message? = nil) throws -> SessionMessagingKit.Message { + return try super.toNonLegacy( + SessionMessagingKit.TypingIndicator( + kind: SessionMessagingKit.TypingIndicator.Kind( + rawValue: (rawKind ?? SessionMessagingKit.TypingIndicator.Kind.stopped.rawValue) + ) + .defaulting(to: .stopped) + ) + ) + } + } + + @objc(SNClosedGroupControlMessage) + internal final class ClosedGroupControlMessage: ControlMessage { + internal var rawKind: String? + + internal var publicKey: Data? + internal var wrappers: [KeyPairWrapper]? + internal var name: String? + internal var encryptionKeyPair: SUKLegacy.KeyPair? + internal var members: [Data]? + internal var admins: [Data]? + internal var expirationTimer: UInt32 + + // MARK: - Key Pair Wrapper + + @objc(SNKeyPairWrapper) + internal final class KeyPairWrapper: NSObject, NSCoding { + internal var publicKey: String? + internal var encryptedKeyPair: Data? + + // MARK: - NSCoding + + 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) { + fatalError("encode(with:) should never be called for legacy types") + } + } + + // MARK: - NSCoding + + public required init?(coder: NSCoder) { + self.rawKind = coder.decodeObject(forKey: "kind") as? String + + self.publicKey = coder.decodeObject(forKey: "publicKey") as? Data + self.wrappers = coder.decodeObject(forKey: "wrappers") as? [KeyPairWrapper] + self.name = coder.decodeObject(forKey: "name") as? String + self.encryptionKeyPair = coder.decodeObject(forKey: "encryptionKeyPair") as? SUKLegacy.KeyPair + self.members = coder.decodeObject(forKey: "members") as? [Data] + self.admins = coder.decodeObject(forKey: "admins") as? [Data] + self.expirationTimer = (coder.decodeObject(forKey: "expirationTimer") as? UInt32 ?? 0) + + super.init(coder: coder) + } + + public override func encode(with coder: NSCoder) { + fatalError("encode(with:) should never be called for legacy types") + } + + // MARK: Non-Legacy Conversion + + override internal func toNonLegacy(_ instance: SessionMessagingKit.Message? = nil) throws -> SessionMessagingKit.Message { + return try super.toNonLegacy( + SessionMessagingKit.ClosedGroupControlMessage( + kind: try { + switch rawKind { + case "new": + guard + let publicKey: Data = self.publicKey, + let name: String = self.name, + let encryptionKeyPair: SUKLegacy.KeyPair = self.encryptionKeyPair, + let members: [Data] = self.members, + let admins: [Data] = self.admins + else { + SNLog("[Migration Error] Unable to decode Legacy ClosedGroupControlMessage") + throw GRDBStorageError.migrationFailed + } + + return .new( + publicKey: publicKey, + name: name, + encryptionKeyPair: Box.KeyPair( + publicKey: encryptionKeyPair.publicKey.bytes, + secretKey: encryptionKeyPair.privateKey.bytes + ), + members: members, + admins: admins, + expirationTimer: self.expirationTimer + ) + + case "encryptionKeyPair": + guard let wrappers: [KeyPairWrapper] = self.wrappers else { + SNLog("[Migration Error] Unable to decode Legacy ClosedGroupControlMessage") + throw GRDBStorageError.migrationFailed + } + + return .encryptionKeyPair( + publicKey: publicKey, + wrappers: try wrappers.map { wrapper in + guard + let publicKey: String = wrapper.publicKey, + let encryptedKeyPair: Data = wrapper.encryptedKeyPair + else { + SNLog("[Migration Error] Unable to decode Legacy ClosedGroupControlMessage") + throw GRDBStorageError.migrationFailed + } + + return SessionMessagingKit.ClosedGroupControlMessage.KeyPairWrapper( + publicKey: publicKey, + encryptedKeyPair: encryptedKeyPair + ) + } + ) + + case "nameChange": + guard let name: String = self.name else { + SNLog("[Migration Error] Unable to decode Legacy ClosedGroupControlMessage") + throw GRDBStorageError.migrationFailed + } + + return .nameChange( + name: name + ) + + case "membersAdded": + guard let members: [Data] = self.members else { + SNLog("[Migration Error] Unable to decode Legacy ClosedGroupControlMessage") + throw GRDBStorageError.migrationFailed + } + + return .membersAdded(members: members) + + case "membersRemoved": + guard let members: [Data] = self.members else { + SNLog("[Migration Error] Unable to decode Legacy ClosedGroupControlMessage") + throw GRDBStorageError.migrationFailed + } + + return .membersRemoved(members: members) + + case "memberLeft": return .memberLeft + case "encryptionKeyPairRequest": return .encryptionKeyPairRequest + default: throw GRDBStorageError.migrationFailed + } + }() + ) + ) + } + } + + @objc(SNDataExtractionNotification) + internal final class DataExtractionNotification: ControlMessage { + internal let rawKind: String? + internal let timestamp: UInt64? + + // MARK: - NSCoding + + public required init?(coder: NSCoder) { + self.rawKind = coder.decodeObject(forKey: "kind") as? String + self.timestamp = coder.decodeObject(forKey: "timestamp") as? UInt64 + + super.init(coder: coder) + } + + public override func encode(with coder: NSCoder) { + fatalError("encode(with:) should never be called for legacy types") + } + + // MARK: Non-Legacy Conversion + + override internal func toNonLegacy(_ instance: SessionMessagingKit.Message? = nil) throws -> SessionMessagingKit.Message { + return try super.toNonLegacy( + SessionMessagingKit.DataExtractionNotification( + kind: try { + switch rawKind { + case "screenshot": return .screenshot + case "mediaSaved": + guard let timestamp: UInt64 = self.timestamp else { + SNLog("[Migration Error] Unable to decode Legacy DataExtractionNotification") + throw GRDBStorageError.migrationFailed + } + + return .mediaSaved(timestamp: timestamp) + + default: throw GRDBStorageError.migrationFailed + } + }() + ) + ) + } + } + + @objc(SNExpirationTimerUpdate) + internal final class ExpirationTimerUpdate: ControlMessage { + internal var syncTarget: String? + internal var duration: UInt32? + + // MARK: - NSCoding + + 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) { + fatalError("encode(with:) should never be called for legacy types") + } + + // MARK: Non-Legacy Conversion + + override internal func toNonLegacy(_ instance: SessionMessagingKit.Message? = nil) throws -> SessionMessagingKit.Message { + return try super.toNonLegacy( + SessionMessagingKit.ExpirationTimerUpdate( + syncTarget: syncTarget, + duration: (duration ?? 0) + ) + ) + } + } + + @objc(SNConfigurationMessage) + internal final class ConfigurationMessage: ControlMessage { + internal var closedGroups: Set = [] + internal var openGroups: Set = [] + internal var displayName: String? + internal var profilePictureURL: String? + internal var profileKey: Data? + internal var contacts: Set = [] + + // MARK: - NSCoding + + public required init?(coder: NSCoder) { + super.init(coder: coder) + if let closedGroups = coder.decodeObject(forKey: "closedGroups") as! Set? { self.closedGroups = closedGroups } + if let openGroups = coder.decodeObject(forKey: "openGroups") as! Set? { 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? { self.contacts = contacts } + } + + public override func encode(with coder: NSCoder) { + fatalError("encode(with:) should never be called for legacy types") + } + + // MARK: Non-Legacy Conversion + + override internal func toNonLegacy(_ instance: SessionMessagingKit.Message? = nil) throws -> SessionMessagingKit.Message { + return try super.toNonLegacy( + SessionMessagingKit.ConfigurationMessage( + displayName: displayName, + profilePictureUrl: profilePictureURL, + profileKey: profileKey, + closedGroups: closedGroups + .map { $0.toNonLegacy() } + .asSet(), + openGroups: openGroups, + contacts: contacts + .map { $0.toNonLegacy() } + .asSet() + ) + ) + } + } + + @objc(CMClosedGroup) + internal final class CMClosedGroup: NSObject, NSCoding { + internal let publicKey: String + internal let name: String + internal let encryptionKeyPair: SUKLegacy.KeyPair + internal let members: Set + internal let admins: Set + internal let expirationTimer: UInt32 + + // MARK: - NSCoding + + 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! SUKLegacy.KeyPair?, + let members = coder.decodeObject(forKey: "members") as! Set?, + let admins = coder.decodeObject(forKey: "admins") as! Set? + else { return nil } + + self.publicKey = publicKey + self.name = name + self.encryptionKeyPair = encryptionKeyPair + self.members = members + self.admins = admins + self.expirationTimer = (coder.decodeObject(forKey: "expirationTimer") as? UInt32 ?? 0) + } + + public func encode(with coder: NSCoder) { + fatalError("encode(with:) should never be called for legacy types") + } + + // MARK: Non-Legacy Conversion + + internal func toNonLegacy() -> SessionMessagingKit.ConfigurationMessage.CMClosedGroup { + return SessionMessagingKit.ConfigurationMessage.CMClosedGroup( + publicKey: publicKey, + name: name, + encryptionKeyPublicKey: encryptionKeyPair.publicKey, + encryptionKeySecretKey: encryptionKeyPair.privateKey, + members: members, + admins: admins, + expirationTimer: expirationTimer + ) + } + } + + @objc(SNConfigurationMessageContact) + internal final class CMContact: NSObject, NSCoding { + internal var publicKey: String? + internal var displayName: String? + internal var profilePictureURL: String? + internal var profileKey: Data? + + internal var hasIsApproved: Bool + internal var isApproved: Bool + internal var hasIsBlocked: Bool + internal var isBlocked: Bool + internal var hasDidApproveMe: Bool + internal var didApproveMe: Bool + + // MARK: - NSCoding + + 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) { + fatalError("encode(with:) should never be called for legacy types") + } + + // MARK: Non-Legacy Conversion + + internal func toNonLegacy() -> SessionMessagingKit.ConfigurationMessage.CMContact { + return SessionMessagingKit.ConfigurationMessage.CMContact( + publicKey: publicKey, + displayName: displayName, + profilePictureUrl: profilePictureURL, + profileKey: profileKey, + hasIsApproved: hasIsApproved, + isApproved: isApproved, + hasIsBlocked: hasIsBlocked, + isBlocked: isBlocked, + hasDidApproveMe: hasDidApproveMe, + didApproveMe: didApproveMe + ) + } + } + + @objc(SNUnsendRequest) + internal final class UnsendRequest: ControlMessage { + internal var timestamp: UInt64? + internal var author: String? + + // MARK: - NSCoding + + public required init?(coder: NSCoder) { + super.init(coder: coder) + + self.timestamp = coder.decodeObject(forKey: "timestamp") as? UInt64 + self.author = coder.decodeObject(forKey: "author") as? String + } + + public override func encode(with coder: NSCoder) { + fatalError("encode(with:) should never be called for legacy types") + } + + // MARK: Non-Legacy Conversion + + override internal func toNonLegacy(_ instance: SessionMessagingKit.Message? = nil) throws -> SessionMessagingKit.Message { + return try super.toNonLegacy( + SessionMessagingKit.UnsendRequest( + timestamp: (timestamp ?? 0), + author: (author ?? "") + ) + ) + } + } + + @objc(SNMessageRequestResponse) + internal final class MessageRequestResponse: ControlMessage { + internal var isApproved: Bool + + // MARK: - NSCoding + + public required init?(coder: NSCoder) { + self.isApproved = coder.decodeBool(forKey: "isApproved") + + super.init(coder: coder) + } + + public override func encode(with coder: NSCoder) { + fatalError("encode(with:) should never be called for legacy types") + } + + // MARK: Non-Legacy Conversion + + override internal func toNonLegacy(_ instance: SessionMessagingKit.Message? = nil) throws -> SessionMessagingKit.Message { + return try super.toNonLegacy( + SessionMessagingKit.MessageRequestResponse( + isApproved: isApproved + ) + ) + } + } + + // MARK: - Attachments + + @objc(TSAttachment) + internal class Attachment: NSObject, NSCoding { + @objc(TSAttachmentType) + public enum AttachmentType: Int { + case `default` + case voiceMessage + } + + @objc public var serverId: UInt64 + @objc public var encryptionKey: Data? + @objc public var contentType: String + @objc public var isDownloaded: Bool + @objc public var attachmentType: AttachmentType + @objc public var downloadURL: String + @objc public var byteCount: UInt32 + @objc public var sourceFilename: String? + @objc public var caption: String? + @objc public var albumMessageId: String? + + public var isImage: Bool { return MIMETypeUtil.isImage(contentType) } + public var isVideo: Bool { return MIMETypeUtil.isVideo(contentType) } + public var isAudio: Bool { return MIMETypeUtil.isAudio(contentType) } + public var isAnimated: Bool { return MIMETypeUtil.isAnimated(contentType) } + + public var isVisualMedia: Bool { isImage || isVideo || isAnimated } + + // MARK: - NSCoder + + public required init(coder: NSCoder) { + self.serverId = coder.decodeObject(forKey: "serverId") as! UInt64 + self.encryptionKey = coder.decodeObject(forKey: "encryptionKey") as? Data + self.contentType = coder.decodeObject(forKey: "contentType") as! String + self.isDownloaded = (coder.decodeObject(forKey: "isDownloaded") as? Bool == true) + self.attachmentType = AttachmentType( + rawValue: (coder.decodeObject(forKey: "attachmentType") as! NSNumber).intValue + ).defaulting(to: .default) + self.downloadURL = (coder.decodeObject(forKey: "downloadURL") as? String ?? "") + self.byteCount = coder.decodeObject(forKey: "byteCount") as! UInt32 + } + + public func encode(with coder: NSCoder) { + fatalError("encode(with:) should never be called for legacy types") + } + } + + @objc(TSAttachmentPointer) + internal class AttachmentPointer: Attachment { + @objc(TSAttachmentPointerState) + public enum State: Int { + case enqueued + case downloading + case failed + } + + @objc public var state: State + @objc public var mostRecentFailureLocalizedText: String? + @objc public var digest: Data? + @objc public var mediaSize: CGSize + @objc public var lazyRestoreFragmentId: String? + + // MARK: - NSCoder + + public required init(coder: NSCoder) { + self.state = State( + rawValue: coder.decodeObject(forKey: "state") as! Int + ).defaulting(to: .failed) + self.mostRecentFailureLocalizedText = coder.decodeObject(forKey: "mostRecentFailureLocalizedText") as? String + self.digest = coder.decodeObject(forKey: "digest") as? Data + self.mediaSize = coder.decodeObject(forKey: "mediaSize") as! CGSize + self.lazyRestoreFragmentId = coder.decodeObject(forKey: "lazyRestoreFragmentId") as? String + + super.init(coder: coder) + } + + override public func encode(with coder: NSCoder) { + fatalError("encode(with:) should never be called for legacy types") + } + } + + @objc(TSAttachmentStream) + internal class AttachmentStream: Attachment { + @objc public var digest: Data? + @objc public var isUploaded: Bool + @objc public var creationTimestamp: Date + @objc public var localRelativeFilePath: String? + @objc public var cachedImageWidth: NSNumber? + @objc public var cachedImageHeight: NSNumber? + @objc public var cachedAudioDurationSeconds: NSNumber? + @objc public var isValidImageCached: NSNumber? + @objc public var isValidVideoCached: NSNumber? + + public var isValidImage: Bool { return (isValidImageCached?.boolValue == true) } + public var isValidVideo: Bool { return (isValidVideoCached?.boolValue == true) } + + public var isValidVisualMedia: Bool { + if self.isImage && self.isValidImage { return true } + if self.isVideo && self.isValidVideo { return true } + if self.isAnimated && self.isValidImage { return true } + + return false + } + + // MARK: - NSCoder + + public required init(coder: NSCoder) { + self.digest = coder.decodeObject(forKey: "digest") as? Data + self.isUploaded = (coder.decodeObject(forKey: "isUploaded") as? Bool == true) + self.creationTimestamp = coder.decodeObject(forKey: "creationTimestamp") as! Date + self.localRelativeFilePath = coder.decodeObject(forKey: "localRelativeFilePath") as? String + self.cachedImageWidth = coder.decodeObject(forKey: "cachedImageWidth") as? NSNumber + self.cachedImageHeight = coder.decodeObject(forKey: "cachedImageHeight") as? NSNumber + self.cachedAudioDurationSeconds = coder.decodeObject(forKey: "cachedAudioDurationSeconds") as? NSNumber + self.isValidImageCached = coder.decodeObject(forKey: "isValidImageCached") as? NSNumber + self.isValidVideoCached = coder.decodeObject(forKey: "isValidVideoCached") as? NSNumber + + super.init(coder: coder) + } + + override public func encode(with coder: NSCoder) { + fatalError("encode(with:) should never be called for legacy types") + } + } @objc(NotifyPNServerJob) internal final class NotifyPNServerJob: NSObject, NSCoding { @@ -94,10 +972,7 @@ public enum Legacy { } public func encode(with coder: NSCoder) { - coder.encode(recipient, forKey: "recipient") - coder.encode(data, forKey: "data") - coder.encode(ttl, forKey: "ttl") - coder.encode(timestamp, forKey: "timestamp") + fatalError("encode(with:) should never be called for legacy types") } } @@ -119,9 +994,7 @@ public enum Legacy { } public func encode(with coder: NSCoder) { - coder.encode(message, forKey: "message") - coder.encode(id, forKey: "id") - coder.encode(failureCount, forKey: "failureCount") + fatalError("encode(with:) should never be called for legacy types") } } @@ -140,36 +1013,33 @@ public enum Legacy { public init?(coder: NSCoder) { guard let data = coder.decodeObject(forKey: "data") as! Data?, - let id = coder.decodeObject(forKey: "id") as! String?, - let isBackgroundPoll = coder.decodeObject(forKey: "isBackgroundPoll") as! Bool? + let id = coder.decodeObject(forKey: "id") as! String? else { return nil } self.data = data self.serverHash = coder.decodeObject(forKey: "serverHash") as! String? self.openGroupMessageServerID = coder.decodeObject(forKey: "openGroupMessageServerID") as! UInt64? self.openGroupID = coder.decodeObject(forKey: "openGroupID") as! String? - self.isBackgroundPoll = isBackgroundPoll + // Note: This behaviour is changed from the old code but the 'isBackgroundPoll' is only set + // when getting messages from the 'BackgroundPoller' class and since we likely want to process + // these new messages immediately it should be fine to do this (this value seemed to be missing + // in some cases which resulted in the 'Legacy.MessageReceiveJob' failing to parse) + self.isBackgroundPoll = ((coder.decodeObject(forKey: "isBackgroundPoll") as? Bool) ?? false) self.id = id self.failureCount = ((coder.decodeObject(forKey: "failureCount") as? UInt) ?? 0) } public func encode(with coder: NSCoder) { - coder.encode(data, forKey: "data") - coder.encode(serverHash, forKey: "serverHash") - coder.encode(openGroupMessageServerID, forKey: "openGroupMessageServerID") - coder.encode(openGroupID, forKey: "openGroupID") - coder.encode(isBackgroundPoll, forKey: "isBackgroundPoll") - coder.encode(id, forKey: "id") - coder.encode(failureCount, forKey: "failureCount") + fatalError("encode(with:) should never be called for legacy types") } } @objc(SNMessageSendJob) - public final class MessageSendJob: NSObject, NSCoding { - public let message: Message - public let destination: Message.Destination - public var id: String? - public var failureCount: UInt = 0 + internal final class MessageSendJob: NSObject, NSCoding { + internal let message: Message + internal let destination: SessionMessagingKit.Message.Destination + internal var id: String? + internal var failureCount: UInt = 0 // MARK: - Coding @@ -217,24 +1087,7 @@ public enum Legacy { } public func encode(with coder: NSCoder) { - coder.encode(message, forKey: "message") - - switch destination { - case .contact(let publicKey): - coder.encode("contact(\(publicKey))", forKey: "destination") - - case .closedGroup(let groupPublicKey): - coder.encode("closedGroup(\(groupPublicKey))", forKey: "destination") - - case .openGroup(let channel, let server): - coder.encode("openGroup(\(channel), \(server))", forKey: "destination") - - case .openGroupV2(let room, let server): - coder.encode("openGroupV2(\(room), \(server))", forKey: "destination") - } - - coder.encode(id, forKey: "id") - coder.encode(failureCount, forKey: "failureCount") + fatalError("encode(with:) should never be called for legacy types") } // MARK: - Convenience @@ -252,13 +1105,13 @@ public enum Legacy { } @objc(AttachmentUploadJob) - public final class AttachmentUploadJob: NSObject, NSCoding { - public let attachmentID: String - public let threadID: String - public let message: Message - public let messageSendJobID: String - public var id: String? - public var failureCount: UInt = 0 + internal final class AttachmentUploadJob: NSObject, NSCoding { + internal let attachmentID: String + internal let threadID: String + internal let message: Message + internal let messageSendJobID: String + internal var id: String? + internal var failureCount: UInt = 0 // MARK: - Coding @@ -280,12 +1133,7 @@ public enum Legacy { } public func encode(with coder: NSCoder) { - coder.encode(attachmentID, forKey: "attachmentID") - coder.encode(threadID, forKey: "threadID") - coder.encode(message, forKey: "message") - coder.encode(messageSendJobID, forKey: "messageSendJobID") - coder.encode(id, forKey: "id") - coder.encode(failureCount, forKey: "failureCount") + fatalError("encode(with:) should never be called for legacy types") } } @@ -317,235 +1165,34 @@ public enum Legacy { } public func encode(with coder: NSCoder) { - coder.encode(attachmentID, forKey: "attachmentID") - coder.encode(tsMessageID, forKey: "tsIncomingMessageID") - coder.encode(threadID, forKey: "threadID") - coder.encode(id, forKey: "id") - coder.encode(failureCount, forKey: "failureCount") - coder.encode(isDeferred, forKey: "isDeferred") + fatalError("encode(with:) should never be called for legacy types") + } + } + + public final class DisappearingConfigurationUpdateInfoMessage: TSInfoMessage { + // Note: Due to how Mantle works we need to set default values for these as the 'init(dictionary:)' + // method doesn't actually get values for them but the must be set before calling a super.init method + // so this allows us to work around the behaviour until 'init(coder:)' method completes it's super call + var createdByRemoteName: String? + var configurationDurationSeconds: UInt32 = 0 + var configurationIsEnabled: Bool = false + + // MARK: - Coding + + public required init(coder: NSCoder) { + super.init(coder: coder) + + self.createdByRemoteName = coder.decodeObject(forKey: "createdByRemoteName") as? String + self.configurationDurationSeconds = ((coder.decodeObject(forKey: "configurationDurationSeconds") as? UInt32) ?? 0) + self.configurationIsEnabled = ((coder.decodeObject(forKey: "configurationIsEnabled") as? Bool) ?? false) + } + + required init(dictionary dictionaryValue: [String : Any]!) throws { + try super.init(dictionary: dictionaryValue) + } + + public override func encode(with coder: NSCoder) { + fatalError("encode(with:) should never be called for legacy types") } } } - -@objc(SNJob) -public protocol _LegacyJob : NSCoding { - var id: String? { get set } - var failureCount: UInt { get set } - - static var collection: String { get } - static var maxFailureCount: UInt { get } - - func execute() -} - -// Note: Looks like Swift doesn't expose nested types well (in the `-Swift` header this was -// appearing with `SWIFT_CLASS_NAME("Contact")` which conflicts with the new type and has a -// different structure) as a result we cannot nest this cleanly -@objc(SNContact) -public class _LegacyContact: NSObject, NSCoding { // NSObject/NSCoding conformance is needed for YapDatabase compatibility - @objc public let sessionID: String - /// The URL from which to fetch the contact's profile picture. - @objc public var profilePictureURL: String? - /// The file name of the contact's profile picture on local storage. - @objc public var profilePictureFileName: String? - /// The key with which the profile is encrypted. - @objc public var profileEncryptionKey: OWSAES256Key? - /// The ID of the thread associated with this contact. - @objc public var threadID: String? - /// This flag is used to determine whether we should auto-download files sent by this contact. - @objc public var isTrusted = false - /// This flag is used to determine whether message requests from this contact are approved - @objc public var isApproved = false - /// This flag is used to determine whether message requests from this contact are blocked - @objc public var isBlocked = false { - didSet { - if isBlocked { - hasBeenBlocked = true - } - } - } - /// This flag is used to determine whether this contact has approved the current users message request - @objc public var didApproveMe = false - /// This flag is used to determine whether this contact has ever been blocked (will be included in the config message if so) - @objc public var hasBeenBlocked = false - - // MARK: Name - /// The name of the contact. Use this whenever you need the "real", underlying name of a user (e.g. when sending a message). - @objc public var name: String? - /// The contact's nickname, if the user set one. - @objc public var nickname: String? - /// The name to display in the UI. For local use only. - @objc public func displayName(for context: Context) -> String? { - if let nickname = 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. - guard let name = name else { return nil } - let endIndex = sessionID.endIndex - let cutoffIndex = sessionID.index(endIndex, offsetBy: -8) - return "\(name) (...\(sessionID[cutoffIndex.. Bool { - guard let other = other as? _LegacyContact else { return false } - return sessionID == other.sessionID - } - - // MARK: Hashing - override public var hash: Int { // Override NSObject.hash and not Hashable.hashValue or Hashable.hash(into:) - return sessionID.hash - } - - // MARK: Description - override public var description: String { - nickname ?? name ?? sessionID - } - - // MARK: Convenience - @objc(contextForThread:) - public static func context(for thread: TSThread) -> Context { - return ((thread as? TSGroupThread)?.isOpenGroup == true) ? .openGroup : .regular - -@objc(OWSDisappearingMessagesConfiguration) -public class _LegacyDisappearingMessagesConfiguration: MTLModel { - public let uniqueId: String - @objc public var isEnabled: Bool - @objc public var durationSeconds: UInt32 - - @objc public var durationIndex: UInt32 = 0 - @objc public var durationString: String? - - var originalDictionaryValue: [String: Any]? - @objc public var isNewRecord: Bool = false - - @objc public static func defaultWith(_ threadId: String) -> Legacy.DisappearingMessagesConfiguration { - return Legacy.DisappearingMessagesConfiguration( - threadId: threadId, - enabled: false, - durationSeconds: (24 * 60 * 60) - ) - } - - public static func fetch(uniqueId: String, transaction: YapDatabaseReadTransaction? = nil) -> Legacy.DisappearingMessagesConfiguration? { - return nil - } - - @objc public static func fetchObject(uniqueId: String) -> Legacy.DisappearingMessagesConfiguration? { - return nil - } - - @objc public static func fetchOrBuildDefault(threadId: String, transaction: YapDatabaseReadTransaction) -> Legacy.DisappearingMessagesConfiguration? { - return defaultWith(threadId) - } - - @objc public static var validDurationsSeconds: [UInt32] = [] - - // MARK: - Initialization - - init(threadId: String, enabled: Bool, durationSeconds: UInt32) { - self.uniqueId = threadId - self.isEnabled = enabled - self.durationSeconds = durationSeconds - self.isNewRecord = true - - super.init() - } - - required init(coder: NSCoder) { - self.uniqueId = coder.decodeObject(forKey: "uniqueId") as! String - self.isEnabled = coder.decodeObject(forKey: "enabled") as! Bool - self.durationSeconds = coder.decodeObject(forKey: "durationSeconds") as! UInt32 - - // Intentionally not calling 'super.init(coder:) here - super.init() - } - - required init(dictionary dictionaryValue: [String : Any]!) throws { - fatalError("init(dictionary:) has not been implemented") - } - - // MARK: - Dirty Tracking - - @objc public override static func storageBehaviorForProperty(withKey propertyKey: String) -> MTLPropertyStorage { - // Don't persist transient properties - if - propertyKey == "TAG" || - propertyKey == "originalDictionaryValue" || - propertyKey == "newRecord" - { - return MTLPropertyStorageNone - } - - return super.storageBehaviorForProperty(withKey: propertyKey) - } - - @objc public var dictionaryValueDidChange: Bool { - return false - } - - @objc(saveWithTransaction:) - public func save(with transaction: YapDatabaseReadWriteTransaction) { - self.originalDictionaryValue = self.dictionaryValue - self.isNewRecord = false - } -} diff --git a/SessionMessagingKit/Database/Migrations/_001_InitialSetupMigration.swift b/SessionMessagingKit/Database/Migrations/_001_InitialSetupMigration.swift index af8637bfa..67d6bbca2 100644 --- a/SessionMessagingKit/Database/Migrations/_001_InitialSetupMigration.swift +++ b/SessionMessagingKit/Database/Migrations/_001_InitialSetupMigration.swift @@ -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) diff --git a/SessionMessagingKit/Database/Migrations/_003_YDBToGRDBMigration.swift b/SessionMessagingKit/Database/Migrations/_003_YDBToGRDBMigration.swift index 5fd482a8d..59e042b5c 100644 --- a/SessionMessagingKit/Database/Migrations/_003_YDBToGRDBMigration.swift +++ b/SessionMessagingKit/Database/Migrations/_003_YDBToGRDBMigration.swift @@ -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 = [] + var validProfileIds: Set = [] var contactThreadIds: Set = [] var legacyThreadIdToIdMap: [String: String] = [:] var threads: Set = [] 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 = [] var outgoingReadReceiptsTimestampsMs: [String: Set] = [:] + // 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 + // 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 = [] var messageReceiveJobs: Set = [] var messageSendJobs: Set = [] @@ -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 + ) 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 } } diff --git a/SessionMessagingKit/Database/Models/Attachment.swift b/SessionMessagingKit/Database/Models/Attachment.swift index b6ec24101..84e335c79 100644 --- a/SessionMessagingKit/Database/Models/Attachment.swift +++ b/SessionMessagingKit/Database/Models/Attachment.swift @@ -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 { + let attachment: TypedTableAlias = TypedTableAlias() + let interaction: TypedTableAlias = TypedTableAlias() + let quote: TypedTableAlias = TypedTableAlias() + let interactionAttachment: TypedTableAlias = TypedTableAlias() + let linkPreview: TypedTableAlias = 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() diff --git a/SessionMessagingKit/Database/Models/ClosedGroup.swift b/SessionMessagingKit/Database/Models/ClosedGroup.swift index 39fe9d249..60e3e7712 100644 --- a/SessionMessagingKit/Database/Models/ClosedGroup.swift +++ b/SessionMessagingKit/Database/Models/ClosedGroup.swift @@ -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 { diff --git a/SessionMessagingKit/Database/Models/Contact.swift b/SessionMessagingKit/Database/Models/Contact.swift index 2edf6b56b..26574da08 100644 --- a/SessionMessagingKit/Database/Models/Contact.swift +++ b/SessionMessagingKit/Database/Models/Contact.swift @@ -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 { + request(for: Contact.profile) + } + // MARK: - Initialization public init( diff --git a/SessionMessagingKit/Database/Models/DisappearingMessageConfiguration.swift b/SessionMessagingKit/Database/Models/DisappearingMessageConfiguration.swift index 0bfb78415..13c30a7cd 100644 --- a/SessionMessagingKit/Database/Models/DisappearingMessageConfiguration.swift +++ b/SessionMessagingKit/Database/Models/DisappearingMessageConfiguration.swift @@ -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) } } diff --git a/SessionMessagingKit/Database/Models/GroupMember.swift b/SessionMessagingKit/Database/Models/GroupMember.swift index c1a8bef50..f1f8b26f1 100644 --- a/SessionMessagingKit/Database/Models/GroupMember.swift +++ b/SessionMessagingKit/Database/Models/GroupMember.swift @@ -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 { diff --git a/SessionMessagingKit/Database/Models/Interaction.swift b/SessionMessagingKit/Database/Models/Interaction.swift index ad45f5482..daaaf98b0 100644 --- a/SessionMessagingKit/Database/Models/Interaction.swift +++ b/SessionMessagingKit/Database/Models/Interaction.swift @@ -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 = TypedTableAlias() + let linkPreview: TypedTableAlias = 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 { - 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 { @@ -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 { diff --git a/SessionMessagingKit/Database/Models/InteractionAttachment.swift b/SessionMessagingKit/Database/Models/InteractionAttachment.swift index b61007329..ae75a0631 100644 --- a/SessionMessagingKit/Database/Models/InteractionAttachment.swift +++ b/SessionMessagingKit/Database/Models/InteractionAttachment.swift @@ -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) diff --git a/SessionMessagingKit/Database/Models/LinkPreview.swift b/SessionMessagingKit/Database/Models/LinkPreview.swift index 30c33d26e..54b4726e9 100644 --- a/SessionMessagingKit/Database/Models/LinkPreview.swift +++ b/SessionMessagingKit/Database/Models/LinkPreview.swift @@ -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 diff --git a/SessionMessagingKit/Database/Models/OpenGroup.swift b/SessionMessagingKit/Database/Models/OpenGroup.swift index acaa31c76..77d75f091 100644 --- a/SessionMessagingKit/Database/Models/OpenGroup.swift +++ b/SessionMessagingKit/Database/Models/OpenGroup.swift @@ -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 ) { diff --git a/SessionMessagingKit/Database/Models/Profile.swift b/SessionMessagingKit/Database/Models/Profile.swift index cedb3b173..b404d166a 100644 --- a/SessionMessagingKit/Database/Models/Profile.swift +++ b/SessionMessagingKit/Database/Models/Profile.swift @@ -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.. 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 { diff --git a/SessionMessagingKit/Database/Models/Quote.swift b/SessionMessagingKit/Database/Models/Quote.swift index d1d49b86a..3f1c27f86 100644 --- a/SessionMessagingKit/Database/Models/Quote.swift +++ b/SessionMessagingKit/Database/Models/Quote.swift @@ -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 { diff --git a/SessionMessagingKit/Database/Models/RecipientState.swift b/SessionMessagingKit/Database/Models/RecipientState.swift index b35d5e743..e96da4c71 100644 --- a/SessionMessagingKit/Database/Models/RecipientState.swift +++ b/SessionMessagingKit/Database/Models/RecipientState.swift @@ -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 won’t resolve to a contact/profile public let recipientId: String /// The current state for the recipient diff --git a/SessionMessagingKit/Database/Models/SessionThread.swift b/SessionMessagingKit/Database/Models/SessionThread.swift index 82a3dd92b..2f9e95126 100644 --- a/SessionMessagingKit/Database/Models/SessionThread.swift +++ b/SessionMessagingKit/Database/Models/SessionThread.swift @@ -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) } } } diff --git a/SessionMessagingKit/Database/SSKPreferences.swift b/SessionMessagingKit/Database/SSKPreferences.swift index 297266f7b..8afbbf245 100644 --- a/SessionMessagingKit/Database/SSKPreferences.swift +++ b/SessionMessagingKit/Database/SSKPreferences.swift @@ -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 } diff --git a/SessionMessagingKit/Jobs/Types/AttachmentDownloadJob.swift b/SessionMessagingKit/Jobs/Types/AttachmentDownloadJob.swift index 1a238112f..a706bf68d 100644 --- a/SessionMessagingKit/Jobs/Types/AttachmentDownloadJob.swift +++ b/SessionMessagingKit/Jobs/Types/AttachmentDownloadJob.swift @@ -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 { diff --git a/SessionMessagingKit/Jobs/Types/DisappearingMessagesJob.swift b/SessionMessagingKit/Jobs/Types/DisappearingMessagesJob.swift index a54b52ab5..41e9f4336 100644 --- a/SessionMessagingKit/Jobs/Types/DisappearingMessagesJob.swift +++ b/SessionMessagingKit/Jobs/Types/DisappearingMessagesJob.swift @@ -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 } diff --git a/SessionMessagingKit/Jobs/Types/MessageSendJob.swift b/SessionMessagingKit/Jobs/Types/MessageSendJob.swift index 11744912c..5f6b2ac79 100644 --- a/SessionMessagingKit/Jobs/Types/MessageSendJob.swift +++ b/SessionMessagingKit/Jobs/Types/MessageSendJob.swift @@ -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 } } diff --git a/SessionMessagingKit/Jobs/Types/SendReadReceiptsJob.swift b/SessionMessagingKit/Jobs/Types/SendReadReceiptsJob.swift index b2f2d443a..d2f7bd871 100644 --- a/SessionMessagingKit/Jobs/Types/SendReadReceiptsJob.swift +++ b/SessionMessagingKit/Jobs/Types/SendReadReceiptsJob.swift @@ -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) diff --git a/SessionMessagingKit/Jobs/Types/UpdateProfilePictureJob.swift b/SessionMessagingKit/Jobs/Types/UpdateProfilePictureJob.swift index 60e8d39d3..2fa13fe01 100644 --- a/SessionMessagingKit/Jobs/Types/UpdateProfilePictureJob.swift +++ b/SessionMessagingKit/Jobs/Types/UpdateProfilePictureJob.swift @@ -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) } ) } diff --git a/SessionMessagingKit/Messages/Control Messages/ClosedGroupControlMessage.swift b/SessionMessagingKit/Messages/Control Messages/ClosedGroupControlMessage.swift index bf8462f49..3bcbc3232 100644 --- a/SessionMessagingKit/Messages/Control Messages/ClosedGroupControlMessage.swift +++ b/SessionMessagingKit/Messages/Control Messages/ClosedGroupControlMessage.swift @@ -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") diff --git a/SessionMessagingKit/Messages/Control Messages/ConfigurationMessage+Convenience.swift b/SessionMessagingKit/Messages/Control Messages/ConfigurationMessage+Convenience.swift index 880398c0b..fac43e97a 100644 --- a/SessionMessagingKit/Messages/Control Messages/ConfigurationMessage+Convenience.swift +++ b/SessionMessagingKit/Messages/Control Messages/ConfigurationMessage+Convenience.swift @@ -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 = [] + var closedGroups: Set = [] var openGroups: Set = [] 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, diff --git a/SessionMessagingKit/Messages/Control Messages/ConfigurationMessage.swift b/SessionMessagingKit/Messages/Control Messages/ConfigurationMessage.swift index 630c56220..9ffa5e6ce 100644 --- a/SessionMessagingKit/Messages/Control Messages/ConfigurationMessage.swift +++ b/SessionMessagingKit/Messages/Control Messages/ConfigurationMessage.swift @@ -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 = [] + public var closedGroups: Set = [] public var openGroups: Set = [] public var displayName: String? - public var profilePictureURL: String? + public var profilePictureUrl: String? public var profileKey: Data? public var contacts: Set = [] 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, openGroups: Set, contacts: Set) { + public init( + displayName: String?, + profilePictureUrl: String?, + profileKey: Data?, + closedGroups: Set, + openGroups: Set, + contacts: Set + ) { 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? { self.closedGroups = closedGroups } - if let openGroups = coder.decodeObject(forKey: "openGroups") as! Set? { 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? { 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 = try decoder.container(keyedBy: CodingKeys.self) - closedGroups = ((try? container.decode(Set.self, forKey: .closedGroups)) ?? []) + closedGroups = ((try? container.decode(Set.self, forKey: .closedGroups)) ?? []) openGroups = ((try? container.decode(Set.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.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 public let admins: Set public let expirationTimer: UInt32 public var isValid: Bool { !members.isEmpty && !admins.isEmpty } - - public init(publicKey: String, name: String, encryptionKeyPair: ECKeyPair, members: Set, admins: Set, 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?, - let admins = coder.decodeObject(forKey: "admins") as! Set? 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, + admins: Set, + 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 = 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.self, forKey: .members) admins = try container.decode(Set.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 = 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 ?? "" } } } diff --git a/SessionMessagingKit/Messages/Control Messages/ControlMessage.swift b/SessionMessagingKit/Messages/Control Messages/ControlMessage.swift index a9b476153..09504cfd8 100644 --- a/SessionMessagingKit/Messages/Control Messages/ControlMessage.swift +++ b/SessionMessagingKit/Messages/Control Messages/ControlMessage.swift @@ -2,5 +2,4 @@ import Foundation -@objc(SNControlMessage) public class ControlMessage: Message { } diff --git a/SessionMessagingKit/Messages/Control Messages/DataExtractionNotification.swift b/SessionMessagingKit/Messages/Control Messages/DataExtractionNotification.swift index 0e05bdad0..01557954d 100644 --- a/SessionMessagingKit/Messages/Control Messages/DataExtractionNotification.swift +++ b/SessionMessagingKit/Messages/Control Messages/DataExtractionNotification.swift @@ -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") diff --git a/SessionMessagingKit/Messages/Control Messages/ExpirationTimerUpdate.swift b/SessionMessagingKit/Messages/Control Messages/ExpirationTimerUpdate.swift index 5426d8cf1..18379089d 100644 --- a/SessionMessagingKit/Messages/Control Messages/ExpirationTimerUpdate.swift +++ b/SessionMessagingKit/Messages/Control Messages/ExpirationTimerUpdate.swift @@ -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 - } } diff --git a/SessionMessagingKit/Messages/Control Messages/MessageRequestResponse.swift b/SessionMessagingKit/Messages/Control Messages/MessageRequestResponse.swift index b508660c8..fda301631 100644 --- a/SessionMessagingKit/Messages/Control Messages/MessageRequestResponse.swift +++ b/SessionMessagingKit/Messages/Control Messages/MessageRequestResponse.swift @@ -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) diff --git a/SessionMessagingKit/Messages/Control Messages/ReadReceipt.swift b/SessionMessagingKit/Messages/Control Messages/ReadReceipt.swift index 686a58b74..9437e6503 100644 --- a/SessionMessagingKit/Messages/Control Messages/ReadReceipt.swift +++ b/SessionMessagingKit/Messages/Control Messages/ReadReceipt.swift @@ -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") diff --git a/SessionMessagingKit/Messages/Control Messages/TypingIndicator.swift b/SessionMessagingKit/Messages/Control Messages/TypingIndicator.swift index d92881360..d5d9058d4 100644 --- a/SessionMessagingKit/Messages/Control Messages/TypingIndicator.swift +++ b/SessionMessagingKit/Messages/Control Messages/TypingIndicator.swift @@ -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") diff --git a/SessionMessagingKit/Messages/Control Messages/UnsendRequest.swift b/SessionMessagingKit/Messages/Control Messages/UnsendRequest.swift index f784d2c9b..b1b982764 100644 --- a/SessionMessagingKit/Messages/Control Messages/UnsendRequest.swift +++ b/SessionMessagingKit/Messages/Control Messages/UnsendRequest.swift @@ -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") diff --git a/SessionMessagingKit/Messages/Message.swift b/SessionMessagingKit/Messages/Message.swift index 7407e70bb..ee1398d78 100644 --- a/SessionMessagingKit/Messages/Message.swift +++ b/SessionMessagingKit/Messages/Message.swift @@ -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 - } } diff --git a/SessionMessagingKit/Messages/Signal/TSIncomingMessage+Conversion.swift b/SessionMessagingKit/Messages/Signal/TSIncomingMessage+Conversion.swift index 6849fd50c..4284b0867 100644 --- a/SessionMessagingKit/Messages/Signal/TSIncomingMessage+Conversion.swift +++ b/SessionMessagingKit/Messages/Signal/TSIncomingMessage+Conversion.swift @@ -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 } } diff --git a/SessionMessagingKit/Messages/Visible Messages/VisibleMessage+LinkPreview.swift b/SessionMessagingKit/Messages/Visible Messages/VisibleMessage+LinkPreview.swift index ae3183b1f..83020f501 100644 --- a/SessionMessagingKit/Messages/Visible Messages/VisibleMessage+LinkPreview.swift +++ b/SessionMessagingKit/Messages/Visible Messages/VisibleMessage+LinkPreview.swift @@ -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 ) } } diff --git a/SessionMessagingKit/Messages/Visible Messages/VisibleMessage+OpenGroupInvitation.swift b/SessionMessagingKit/Messages/Visible Messages/VisibleMessage+OpenGroupInvitation.swift index 034f61b3b..c1bdf29fe 100644 --- a/SessionMessagingKit/Messages/Visible Messages/VisibleMessage+OpenGroupInvitation.swift +++ b/SessionMessagingKit/Messages/Visible Messages/VisibleMessage+OpenGroupInvitation.swift @@ -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"), diff --git a/SessionMessagingKit/Messages/Visible Messages/VisibleMessage+Profile.swift b/SessionMessagingKit/Messages/Visible Messages/VisibleMessage+Profile.swift index 60a5caec8..64698772e 100644 --- a/SessionMessagingKit/Messages/Visible Messages/VisibleMessage+Profile.swift +++ b/SessionMessagingKit/Messages/Visible Messages/VisibleMessage+Profile.swift @@ -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") ) """ } diff --git a/SessionMessagingKit/Messages/Visible Messages/VisibleMessage+Quote.swift b/SessionMessagingKit/Messages/Visible Messages/VisibleMessage+Quote.swift index 21cbe0fb2..10f4276e7 100644 --- a/SessionMessagingKit/Messages/Visible Messages/VisibleMessage+Quote.swift +++ b/SessionMessagingKit/Messages/Visible Messages/VisibleMessage+Quote.swift @@ -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 + ) } } diff --git a/SessionMessagingKit/Messages/Visible Messages/VisibleMessage.swift b/SessionMessagingKit/Messages/Visible Messages/VisibleMessage.swift index 7ac2758b9..fafa53719 100644 --- a/SessionMessagingKit/Messages/Visible Messages/VisibleMessage.swift +++ b/SessionMessagingKit/Messages/Visible Messages/VisibleMessage.swift @@ -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 = 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") ) diff --git a/SessionMessagingKit/Sending & Receiving/MessageReceiver+Handling.swift b/SessionMessagingKit/Sending & Receiving/MessageReceiver+Handling.swift index c70f5bd22..644ab35ab 100644 --- a/SessionMessagingKit/Sending & Receiving/MessageReceiver+Handling.swift +++ b/SessionMessagingKit/Sending & Receiving/MessageReceiver+Handling.swift @@ -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 } diff --git a/SessionMessagingKit/Sending & Receiving/MessageReceiver.swift b/SessionMessagingKit/Sending & Receiving/MessageReceiver.swift index 2fb678ac1..6235da943 100644 --- a/SessionMessagingKit/Sending & Receiving/MessageReceiver.swift +++ b/SessionMessagingKit/Sending & Receiving/MessageReceiver.swift @@ -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 diff --git a/SessionMessagingKit/Sending & Receiving/MessageSender.swift b/SessionMessagingKit/Sending & Receiving/MessageSender.swift index a16110c55..94794f6d5 100644 --- a/SessionMessagingKit/Sending & Receiving/MessageSender.swift +++ b/SessionMessagingKit/Sending & Receiving/MessageSender.swift @@ -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) { diff --git a/SessionMessagingKit/Sending & Receiving/Notification+MessageReceiver.swift b/SessionMessagingKit/Sending & Receiving/Notification+MessageReceiver.swift index 87cdcfd51..f369ac1ef 100644 --- a/SessionMessagingKit/Sending & Receiving/Notification+MessageReceiver.swift +++ b/SessionMessagingKit/Sending & Receiving/Notification+MessageReceiver.swift @@ -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 } diff --git a/SessionSnodeKit/Database/LegacyDatabase/SSKLegacyModels.swift b/SessionSnodeKit/Database/LegacyDatabase/SSKLegacyModels.swift index f29077117..cb271285b 100644 --- a/SessionSnodeKit/Database/LegacyDatabase/SSKLegacyModels.swift +++ b/SessionSnodeKit/Database/LegacyDatabase/SSKLegacyModels.swift @@ -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.. 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 } } diff --git a/SessionSnodeKit/Database/Migrations/_003_YDBToGRDBMigration.swift b/SessionSnodeKit/Database/Migrations/_003_YDBToGRDBMigration.swift index 6f254aff0..bc760e87e 100644 --- a/SessionSnodeKit/Database/Migrations/_003_YDBToGRDBMigration.swift +++ b/SessionSnodeKit/Database/Migrations/_003_YDBToGRDBMigration.swift @@ -15,6 +15,12 @@ enum _003_YDBToGRDBMigration: Migration { var snodeSetResult: [String: Set] = [:] 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( diff --git a/SessionUtilitiesKit/Database/LegacyDatabase/SUKLegacyModels.swift b/SessionUtilitiesKit/Database/LegacyDatabase/SUKLegacyModels.swift index 0773d38d7..28fd3aaeb 100644 --- a/SessionUtilitiesKit/Database/LegacyDatabase/SUKLegacyModels.swift +++ b/SessionUtilitiesKit/Database/LegacyDatabase/SUKLegacyModels.swift @@ -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 diff --git a/SessionUtilitiesKit/Database/Migrations/_003_YDBToGRDBMigration.swift b/SessionUtilitiesKit/Database/Migrations/_003_YDBToGRDBMigration.swift index 55de4d025..436ad7f85 100644 --- a/SessionUtilitiesKit/Database/Migrations/_003_YDBToGRDBMigration.swift +++ b/SessionUtilitiesKit/Database/Migrations/_003_YDBToGRDBMigration.swift @@ -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, diff --git a/SessionUtilitiesKit/Database/Types/TypedTableAlias.swift b/SessionUtilitiesKit/Database/Types/TypedTableAlias.swift new file mode 100644 index 000000000..9c526ec14 --- /dev/null +++ b/SessionUtilitiesKit/Database/Types/TypedTableAlias.swift @@ -0,0 +1,14 @@ +// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. + +import Foundation +import GRDB + +public class TypedTableAlias where T: TableRecord, T: ColumnExpressible { + let alias: TableAlias = TableAlias(name: T.databaseTableName) + + public init() {} + + public subscript(_ column: T.Columns) -> SQLExpression { + return alias[column.name] + } +} diff --git a/SessionUtilitiesKit/Database/Utilities/PersistableRecord+Utilities.swift b/SessionUtilitiesKit/Database/Utilities/PersistableRecord+Utilities.swift index de92207c1..3da9eba81 100644 --- a/SessionUtilitiesKit/Database/Utilities/PersistableRecord+Utilities.swift +++ b/SessionUtilitiesKit/Database/Utilities/PersistableRecord+Utilities.swift @@ -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) } } } diff --git a/SessionUtilitiesKit/Database/Utilities/QueryInterfaceRequest+Utilities.swift b/SessionUtilitiesKit/Database/Utilities/QueryInterfaceRequest+Utilities.swift new file mode 100644 index 000000000..fa9419022 --- /dev/null +++ b/SessionUtilitiesKit/Database/Utilities/QueryInterfaceRequest+Utilities.swift @@ -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) + } +} diff --git a/SessionUtilitiesKit/Database/Utilities/TableRecord+Utilities.swift b/SessionUtilitiesKit/Database/Utilities/TableRecord+Utilities.swift new file mode 100644 index 000000000..b8c0be2b1 --- /dev/null +++ b/SessionUtilitiesKit/Database/Utilities/TableRecord+Utilities.swift @@ -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 { + return all().select(selection) + } +} diff --git a/SessionUtilitiesKit/General/String+Localization.swift b/SessionUtilitiesKit/General/String+Localized.swift similarity index 100% rename from SessionUtilitiesKit/General/String+Localization.swift rename to SessionUtilitiesKit/General/String+Localized.swift diff --git a/SessionUtilitiesKit/General/UITableView+ReusableView.swift b/SessionUtilitiesKit/General/UITableView+ReusableView.swift index 725faa6b4..48b8425fd 100644 --- a/SessionUtilitiesKit/General/UITableView+ReusableView.swift +++ b/SessionUtilitiesKit/General/UITableView+ReusableView.swift @@ -12,12 +12,16 @@ public extension UITableView { } func dequeue(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(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 } } diff --git a/SessionUtilitiesKit/JobRunner/JobRunner.swift b/SessionUtilitiesKit/JobRunner/JobRunner.swift index 39d6e896b..8947a0032 100644 --- a/SessionUtilitiesKit/JobRunner/JobRunner.swift +++ b/SessionUtilitiesKit/JobRunner/JobRunner.swift @@ -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) ) } diff --git a/SessionUtilitiesKit/Media/OWSMediaUtils.swift b/SessionUtilitiesKit/Media/OWSMediaUtils.swift index fbab78183..42cd8e7ac 100644 --- a/SessionUtilitiesKit/Media/OWSMediaUtils.swift +++ b/SessionUtilitiesKit/Media/OWSMediaUtils.swift @@ -1,3 +1,5 @@ +// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. + import Foundation import AVFoundation diff --git a/SessionUtilitiesKit/Utilities/NSAttributedString+Utilities.swift b/SessionUtilitiesKit/Utilities/NSAttributedString+Utilities.swift new file mode 100644 index 000000000..bc80c9dcb --- /dev/null +++ b/SessionUtilitiesKit/Utilities/NSAttributedString+Utilities.swift @@ -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 + } +} diff --git a/SessionUtilitiesKit/Utilities/Optional+Utilities.swift b/SessionUtilitiesKit/Utilities/Optional+Utilities.swift index cd6374b96..ede1f4e36 100644 --- a/SessionUtilitiesKit/Utilities/Optional+Utilities.swift +++ b/SessionUtilitiesKit/Utilities/Optional+Utilities.swift @@ -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) + } +} diff --git a/SignalUtilitiesKit/Database/Migrations/ContactsMigration.swift b/SignalUtilitiesKit/Database/Migrations/ContactsMigration.swift index 2e0a78b66..23e3534f4 100644 --- a/SignalUtilitiesKit/Database/Migrations/ContactsMigration.swift +++ b/SignalUtilitiesKit/Database/Migrations/ContactsMigration.swift @@ -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: { diff --git a/SignalUtilitiesKit/Profile Pictures/Identicon+ObjC.swift b/SignalUtilitiesKit/Profile Pictures/Identicon+ObjC.swift index 81e2d664b..2e7914aca 100644 --- a/SignalUtilitiesKit/Profile Pictures/Identicon+ObjC.swift +++ b/SignalUtilitiesKit/Profile Pictures/Identicon+ObjC.swift @@ -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) } } }