Cleared out some of the legacy serialisation logic, further UI binding
Refactored the SignalApp class to Swift Fixed a horizontal alignment issue in the ConversationTitleView Fixed an issue where expiration timer update messages weren't migrated or rendering correctly Fixed an issue where expiring messages weren't migrated correctly Fixed an issue where closed groups which had been left were causing migration failures (due to data incorrectly being assumed to be required) Shifted the Legacy Attachment types into the 'SMKLegacy' namespace Moved all of the NSCoding logic for the TSMessage
This commit is contained in:
parent
3baeb981d9
commit
32304ae5dd
|
@ -21,7 +21,6 @@
|
|||
3430FE181F7751D4000EC51B /* GiphyAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3430FE171F7751D4000EC51B /* GiphyAPI.swift */; };
|
||||
34330AA31E79686200DF2FB9 /* OWSProgressView.m in Sources */ = {isa = PBXBuildFile; fileRef = 34330AA21E79686200DF2FB9 /* OWSProgressView.m */; };
|
||||
34386A54207D271D009F5D9C /* NeverClearView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34386A53207D271C009F5D9C /* NeverClearView.swift */; };
|
||||
346129991FD1E4DA00532771 /* SignalApp.m in Sources */ = {isa = PBXBuildFile; fileRef = 346129971FD1E4D900532771 /* SignalApp.m */; };
|
||||
34661FB820C1C0D60056EDD6 /* message_sent.aiff in Resources */ = {isa = PBXBuildFile; fileRef = 34661FB720C1C0D60056EDD6 /* message_sent.aiff */; };
|
||||
346B66311F4E29B200E5122F /* CropScaleImageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 346B66301F4E29B200E5122F /* CropScaleImageViewController.swift */; };
|
||||
3478504C1FD7496D007B8332 /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = B66DBF4919D5BBC8006EA940 /* Images.xcassets */; };
|
||||
|
@ -157,7 +156,6 @@
|
|||
B821494F25D4E163009C0F2A /* BodyTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B821494E25D4E163009C0F2A /* BodyTextView.swift */; };
|
||||
B82149B825D60393009C0F2A /* BlockedModal.swift in Sources */ = {isa = PBXBuildFile; fileRef = B82149B725D60393009C0F2A /* BlockedModal.swift */; };
|
||||
B82149C125D605C6009C0F2A /* InfoBanner.swift in Sources */ = {isa = PBXBuildFile; fileRef = B82149C025D605C6009C0F2A /* InfoBanner.swift */; };
|
||||
B8214A2B25D63EB9009C0F2A /* MessagesTableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8214A2A25D63EB9009C0F2A /* MessagesTableView.swift */; };
|
||||
B8269D2925C7A4B400488AB4 /* InputView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8269D2825C7A4B400488AB4 /* InputView.swift */; };
|
||||
B8269D3325C7A8C600488AB4 /* InputViewButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8269D3225C7A8C600488AB4 /* InputViewButton.swift */; };
|
||||
B8269D3D25C7B34D00488AB4 /* InputTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8269D3C25C7B34D00488AB4 /* InputTextView.swift */; };
|
||||
|
@ -785,11 +783,12 @@
|
|||
FD6A7A6B2818C17C00035AC1 /* UpdateProfilePictureJob.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6A7A6A2818C17C00035AC1 /* UpdateProfilePictureJob.swift */; };
|
||||
FD6A7A6D2818C61500035AC1 /* _002_SetupStandardJobs.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6A7A6C2818C61500035AC1 /* _002_SetupStandardJobs.swift */; };
|
||||
FD705A8C278CDB5600F16121 /* SAEScreenLockViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD705A8B278CDB5600F16121 /* SAEScreenLockViewController.swift */; };
|
||||
FD705A8E278CE29800F16121 /* String+Localization.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD705A8D278CE29800F16121 /* String+Localization.swift */; };
|
||||
FD705A8E278CE29800F16121 /* String+Localized.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD705A8D278CE29800F16121 /* String+Localized.swift */; };
|
||||
FD705A90278CEBBC00F16121 /* ShareAppExtensionContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD705A8F278CEBBC00F16121 /* ShareAppExtensionContext.swift */; };
|
||||
FD705A92278D051200F16121 /* ReusableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD705A91278D051200F16121 /* ReusableView.swift */; };
|
||||
FD705A94278D052B00F16121 /* UITableView+ReusableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD705A93278D052B00F16121 /* UITableView+ReusableView.swift */; };
|
||||
FD705A98278E9F4D00F16121 /* UIColor+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD705A97278E9F4D00F16121 /* UIColor+Extensions.swift */; };
|
||||
FD7162DB281B6C440060647B /* TypedTableAlias.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD7162DA281B6C440060647B /* TypedTableAlias.swift */; };
|
||||
FD859F0027C4691300510D0C /* MockDataGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD859EFF27C4691300510D0C /* MockDataGenerator.swift */; };
|
||||
FD88BAD927A7439C00BBC442 /* MessageRequestsCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD88BAD827A7439C00BBC442 /* MessageRequestsCell.swift */; };
|
||||
FD88BADB27A750F200BBC442 /* MessageRequestsMigration.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD88BADA27A750F200BBC442 /* MessageRequestsMigration.swift */; };
|
||||
|
@ -820,6 +819,11 @@
|
|||
FDF0B75A2807F3A3004C14C5 /* MessageSenderError.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF0B7592807F3A3004C14C5 /* MessageSenderError.swift */; };
|
||||
FDF0B75C2807F41D004C14C5 /* MessageSender+Convenience.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF0B75B2807F41D004C14C5 /* MessageSender+Convenience.swift */; };
|
||||
FDF0B75E280AAF35004C14C5 /* Preferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF0B75D280AAF35004C14C5 /* Preferences.swift */; };
|
||||
FDF222072818CECF000A4995 /* ConversationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF222062818CECF000A4995 /* ConversationViewModel.swift */; };
|
||||
FDF222092818D2B0000A4995 /* NSAttributedString+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF222082818D2B0000A4995 /* NSAttributedString+Utilities.swift */; };
|
||||
FDF2220B2818F38D000A4995 /* SessionApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF2220A2818F38D000A4995 /* SessionApp.swift */; };
|
||||
FDF2220F281B55E6000A4995 /* QueryInterfaceRequest+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF2220E281B55E6000A4995 /* QueryInterfaceRequest+Utilities.swift */; };
|
||||
FDF22211281B5E0B000A4995 /* TableRecord+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF22210281B5E0B000A4995 /* TableRecord+Utilities.swift */; };
|
||||
FDFD645827EC1F4000808CA1 /* Atomic.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDFD645727EC1F4000808CA1 /* Atomic.swift */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
|
@ -1010,8 +1014,6 @@
|
|||
34386A53207D271C009F5D9C /* NeverClearView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NeverClearView.swift; sourceTree = "<group>"; };
|
||||
34480B371FD092A900BC14EF /* SignalShareExtension-Bridging-Header.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "SignalShareExtension-Bridging-Header.h"; sourceTree = "<group>"; };
|
||||
34480B381FD092E300BC14EF /* SessionShareExtension-Prefix.pch */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "SessionShareExtension-Prefix.pch"; sourceTree = "<group>"; };
|
||||
346129971FD1E4D900532771 /* SignalApp.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SignalApp.m; sourceTree = "<group>"; };
|
||||
346129981FD1E4DA00532771 /* SignalApp.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SignalApp.h; sourceTree = "<group>"; };
|
||||
34661FB720C1C0D60056EDD6 /* message_sent.aiff */ = {isa = PBXFileReference; lastKnownFileType = audio.aiff; name = message_sent.aiff; path = Session/Meta/AudioFiles/message_sent.aiff; sourceTree = SOURCE_ROOT; };
|
||||
346B66301F4E29B200E5122F /* CropScaleImageViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CropScaleImageViewController.swift; sourceTree = "<group>"; };
|
||||
3488F9352191CC4000E524CC /* MediaView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MediaView.swift; sourceTree = "<group>"; };
|
||||
|
@ -1206,7 +1208,6 @@
|
|||
B821494E25D4E163009C0F2A /* BodyTextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BodyTextView.swift; sourceTree = "<group>"; };
|
||||
B82149B725D60393009C0F2A /* BlockedModal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlockedModal.swift; sourceTree = "<group>"; };
|
||||
B82149C025D605C6009C0F2A /* InfoBanner.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InfoBanner.swift; sourceTree = "<group>"; };
|
||||
B8214A2A25D63EB9009C0F2A /* MessagesTableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessagesTableView.swift; sourceTree = "<group>"; };
|
||||
B8269D2825C7A4B400488AB4 /* InputView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InputView.swift; sourceTree = "<group>"; };
|
||||
B8269D3225C7A8C600488AB4 /* InputViewButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InputViewButton.swift; sourceTree = "<group>"; };
|
||||
B8269D3C25C7B34D00488AB4 /* InputTextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InputTextView.swift; sourceTree = "<group>"; };
|
||||
|
@ -1288,7 +1289,6 @@
|
|||
B8D0A24F25E3678700C1835E /* LinkDeviceVC.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LinkDeviceVC.swift; sourceTree = "<group>"; };
|
||||
B8D0A25825E367AC00C1835E /* Notification+MessageReceiver.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = "Notification+MessageReceiver.swift"; path = "SessionMessagingKit/Sending & Receiving/Notification+MessageReceiver.swift"; sourceTree = SOURCE_ROOT; };
|
||||
B8D0A26825E4A2C200C1835E /* Onboarding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Onboarding.swift; sourceTree = "<group>"; };
|
||||
B8D84E9325DF72AF005A043E /* ConversationViewAction.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ConversationViewAction.h; sourceTree = "<group>"; };
|
||||
B8D84EA225DF745A005A043E /* LinkPreviewState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LinkPreviewState.swift; sourceTree = "<group>"; };
|
||||
B8D84ECE25E3108A005A043E /* ExpandingAttachmentsButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExpandingAttachmentsButton.swift; sourceTree = "<group>"; };
|
||||
B8D8F1372566120F0092EF10 /* Storage+ClosedGroups.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Storage+ClosedGroups.swift"; sourceTree = "<group>"; };
|
||||
|
@ -1855,11 +1855,12 @@
|
|||
FD6A7A6A2818C17C00035AC1 /* UpdateProfilePictureJob.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateProfilePictureJob.swift; sourceTree = "<group>"; };
|
||||
FD6A7A6C2818C61500035AC1 /* _002_SetupStandardJobs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _002_SetupStandardJobs.swift; sourceTree = "<group>"; };
|
||||
FD705A8B278CDB5600F16121 /* SAEScreenLockViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SAEScreenLockViewController.swift; sourceTree = "<group>"; };
|
||||
FD705A8D278CE29800F16121 /* String+Localization.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+Localization.swift"; sourceTree = "<group>"; };
|
||||
FD705A8D278CE29800F16121 /* String+Localized.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+Localized.swift"; sourceTree = "<group>"; };
|
||||
FD705A8F278CEBBC00F16121 /* ShareAppExtensionContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareAppExtensionContext.swift; sourceTree = "<group>"; };
|
||||
FD705A91278D051200F16121 /* ReusableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReusableView.swift; sourceTree = "<group>"; };
|
||||
FD705A93278D052B00F16121 /* UITableView+ReusableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UITableView+ReusableView.swift"; sourceTree = "<group>"; };
|
||||
FD705A97278E9F4D00F16121 /* UIColor+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIColor+Extensions.swift"; sourceTree = "<group>"; };
|
||||
FD7162DA281B6C440060647B /* TypedTableAlias.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TypedTableAlias.swift; sourceTree = "<group>"; };
|
||||
FD859EFF27C4691300510D0C /* MockDataGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockDataGenerator.swift; sourceTree = "<group>"; };
|
||||
FD88BAD827A7439C00BBC442 /* MessageRequestsCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageRequestsCell.swift; sourceTree = "<group>"; };
|
||||
FD88BADA27A750F200BBC442 /* MessageRequestsMigration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageRequestsMigration.swift; sourceTree = "<group>"; };
|
||||
|
@ -1888,6 +1889,11 @@
|
|||
FDF0B7592807F3A3004C14C5 /* MessageSenderError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageSenderError.swift; sourceTree = "<group>"; };
|
||||
FDF0B75B2807F41D004C14C5 /* MessageSender+Convenience.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "MessageSender+Convenience.swift"; sourceTree = "<group>"; };
|
||||
FDF0B75D280AAF35004C14C5 /* Preferences.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Preferences.swift; sourceTree = "<group>"; };
|
||||
FDF222062818CECF000A4995 /* ConversationViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConversationViewModel.swift; sourceTree = "<group>"; };
|
||||
FDF222082818D2B0000A4995 /* NSAttributedString+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSAttributedString+Utilities.swift"; sourceTree = "<group>"; };
|
||||
FDF2220A2818F38D000A4995 /* SessionApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionApp.swift; sourceTree = "<group>"; };
|
||||
FDF2220E281B55E6000A4995 /* QueryInterfaceRequest+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "QueryInterfaceRequest+Utilities.swift"; sourceTree = "<group>"; };
|
||||
FDF22210281B5E0B000A4995 /* TableRecord+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TableRecord+Utilities.swift"; sourceTree = "<group>"; };
|
||||
FDFD645727EC1F4000808CA1 /* Atomic.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Atomic.swift; sourceTree = "<group>"; };
|
||||
FEDBAE1B98C49BBE8C87F575 /* Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionUtilitiesKit.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionUtilitiesKit.debug.xcconfig"; path = "Pods/Target Support Files/Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionUtilitiesKit/Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionUtilitiesKit.debug.xcconfig"; sourceTree = "<group>"; };
|
||||
FF9BA33D021B115B1F5B4E46 /* Pods-SessionMessagingKit.app store release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SessionMessagingKit.app store release.xcconfig"; path = "Pods/Target Support Files/Pods-SessionMessagingKit/Pods-SessionMessagingKit.app store release.xcconfig"; sourceTree = "<group>"; };
|
||||
|
@ -2267,7 +2273,6 @@
|
|||
B82149B725D60393009C0F2A /* BlockedModal.swift */,
|
||||
C374EEE125DA26740073A857 /* LinkPreviewModal.swift */,
|
||||
B82149C025D605C6009C0F2A /* InfoBanner.swift */,
|
||||
B8214A2A25D63EB9009C0F2A /* MessagesTableView.swift */,
|
||||
C374EEEA25DA3CA70073A857 /* ConversationTitleView.swift */,
|
||||
B848A4C4269EAAA200617031 /* UserDetailsSheet.swift */,
|
||||
);
|
||||
|
@ -2277,21 +2282,21 @@
|
|||
B835246C25C38AA20089A44F /* Conversations */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
B835246D25C38ABF0089A44F /* ConversationVC.swift */,
|
||||
B8569AC225CB5D2900DBA3DB /* ConversationVC+Interaction.swift */,
|
||||
3496744E2076ACCE00080B5F /* LongTextViewController.swift */,
|
||||
4CC613352227A00400E21A3A /* ConversationSearch.swift */,
|
||||
B8D84E9325DF72AF005A043E /* ConversationViewAction.h */,
|
||||
34D1F06F1F8678AA0066283D /* ConversationViewItem.h */,
|
||||
34D1F0701F8678AA0066283D /* ConversationViewItem.m */,
|
||||
341341ED2187467900192D59 /* ConversationViewModel.h */,
|
||||
341341EE2187467900192D59 /* ConversationViewModel.m */,
|
||||
34ABC0E321DD20C500ED9469 /* ConversationMessageMapping.swift */,
|
||||
B887C38125C7C79700E11DAE /* Input View */,
|
||||
B835247725C38D190089A44F /* Message Cells */,
|
||||
C328252E25CA54F70062D0A7 /* Context Menu */,
|
||||
B821493625D4D6A7009C0F2A /* Views & Modals */,
|
||||
C302094625DCDFD3001F572D /* Settings */,
|
||||
FDF222062818CECF000A4995 /* ConversationViewModel.swift */,
|
||||
B835246D25C38ABF0089A44F /* ConversationVC.swift */,
|
||||
B8569AC225CB5D2900DBA3DB /* ConversationVC+Interaction.swift */,
|
||||
3496744E2076ACCE00080B5F /* LongTextViewController.swift */,
|
||||
4CC613352227A00400E21A3A /* ConversationSearch.swift */,
|
||||
34D1F06F1F8678AA0066283D /* ConversationViewItem.h */,
|
||||
34D1F0701F8678AA0066283D /* ConversationViewItem.m */,
|
||||
341341ED2187467900192D59 /* ConversationViewModel.h */,
|
||||
341341EE2187467900192D59 /* ConversationViewModel.m */,
|
||||
34ABC0E321DD20C500ED9469 /* ConversationMessageMapping.swift */,
|
||||
);
|
||||
path = Conversations;
|
||||
sourceTree = "<group>";
|
||||
|
@ -2433,7 +2438,7 @@
|
|||
C33FDB6B255A580F00E217F9 /* SNUserDefaults.swift */,
|
||||
C33FDB3F255A580C00E217F9 /* String+SSK.swift */,
|
||||
C3C2AC2D2553CBEB00C340D1 /* String+Trimming.swift */,
|
||||
FD705A8D278CE29800F16121 /* String+Localization.swift */,
|
||||
FD705A8D278CE29800F16121 /* String+Localized.swift */,
|
||||
C38EF237255B6D65007E1867 /* UIDevice+featureSupport.swift */,
|
||||
C35D0DB425AE5F1200B6BF49 /* UIEdgeInsets.swift */,
|
||||
FD705A91278D051200F16121 /* ReusableView.swift */,
|
||||
|
@ -3507,8 +3512,7 @@
|
|||
C3CA3AA0255CDA7000F4C6D4 /* Mnemonic */,
|
||||
B67EBF5C19194AC60084CCFD /* Settings.bundle */,
|
||||
B657DDC91911A40500F45B0C /* Signal.entitlements */,
|
||||
346129981FD1E4DA00532771 /* SignalApp.h */,
|
||||
346129971FD1E4D900532771 /* SignalApp.m */,
|
||||
FDF2220A2818F38D000A4995 /* SessionApp.swift */,
|
||||
45B201741DAECBFD00C461E0 /* Signal-Bridging-Header.h */,
|
||||
D221A095169C9E5E00537ABF /* Session-Info.plist */,
|
||||
D221A09B169C9E5E00537ABF /* Session-Prefix.pch */,
|
||||
|
@ -3630,11 +3634,12 @@
|
|||
FD09796527F6B0A800936362 /* Utilities */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
FDA8EB0F280F8238002B68E5 /* Codable+Utilities.swift */,
|
||||
FD09796A27F6C67500936362 /* Failable.swift */,
|
||||
FD09797127FAA2F500936362 /* Optional+Utilities.swift */,
|
||||
FD09797C27FBDB2000936362 /* Notification+Utilities.swift */,
|
||||
FDF222082818D2B0000A4995 /* NSAttributedString+Utilities.swift */,
|
||||
C3E7134E251C867C009649BB /* Sodium+Conversion.swift */,
|
||||
FDA8EB0F280F8238002B68E5 /* Codable+Utilities.swift */,
|
||||
);
|
||||
path = Utilities;
|
||||
sourceTree = "<group>";
|
||||
|
@ -3734,6 +3739,7 @@
|
|||
FD17D7B727F51ECA00122BE0 /* Migration.swift */,
|
||||
FD17D7B927F51F2100122BE0 /* TargetMigrations.swift */,
|
||||
FD17D7C027F5200100122BE0 /* TypedTableDefinition.swift */,
|
||||
FD7162DA281B6C440060647B /* TypedTableAlias.swift */,
|
||||
);
|
||||
path = Types;
|
||||
sourceTree = "<group>";
|
||||
|
@ -3746,6 +3752,8 @@
|
|||
FD17D7C627F5207C00122BE0 /* DatabaseMigrator+Utilities.swift */,
|
||||
FD17D7BC27F51F6900122BE0 /* GRDB+Notifications.swift */,
|
||||
FDF0B74C280664E9004C14C5 /* PersistableRecord+Utilities.swift */,
|
||||
FDF22210281B5E0B000A4995 /* TableRecord+Utilities.swift */,
|
||||
FDF2220E281B55E6000A4995 /* QueryInterfaceRequest+Utilities.swift */,
|
||||
);
|
||||
path = Utilities;
|
||||
sourceTree = "<group>";
|
||||
|
@ -4889,6 +4897,7 @@
|
|||
B8AE75A425A6C6A6001A84D2 /* Data+Trimming.swift in Sources */,
|
||||
FD17D7C527F5206300122BE0 /* ColumnDefinition+Utilities.swift in Sources */,
|
||||
B8856DE6256F15F2001CE70E /* String+SSK.swift in Sources */,
|
||||
FDF2220F281B55E6000A4995 /* QueryInterfaceRequest+Utilities.swift in Sources */,
|
||||
C3471ED42555386B00297E91 /* AESGCM.swift in Sources */,
|
||||
FD17D7B827F51ECA00122BE0 /* Migration.swift in Sources */,
|
||||
C32C5DDB256DD9FF003C73A2 /* ContentProxy.swift in Sources */,
|
||||
|
@ -4903,9 +4912,11 @@
|
|||
C3BBE0A72554D4DE0050F1E3 /* Promise+Retrying.swift in Sources */,
|
||||
B8856D7B256F14F4001CE70E /* UIView+OWS.m in Sources */,
|
||||
FD17D7B027F4225C00122BE0 /* Set+Utilities.swift in Sources */,
|
||||
FDF22211281B5E0B000A4995 /* TableRecord+Utilities.swift in Sources */,
|
||||
B88FA7FB26114EA70049422F /* Hex.swift in Sources */,
|
||||
C35D0DB525AE5F1200B6BF49 /* UIEdgeInsets.swift in Sources */,
|
||||
C3D9E4F4256778AF0040E4F3 /* NSData+Image.m in Sources */,
|
||||
FDF222092818D2B0000A4995 /* NSAttributedString+Utilities.swift in Sources */,
|
||||
C32C5E0C256DDAFA003C73A2 /* NSRegularExpression+SSK.swift in Sources */,
|
||||
C3BBE0A92554D4DE0050F1E3 /* HTTP.swift in Sources */,
|
||||
B8856D23256F116B001CE70E /* Weak.swift in Sources */,
|
||||
|
@ -4919,9 +4930,10 @@
|
|||
C300A60D2554B31900555489 /* Logging.swift in Sources */,
|
||||
B8FF8EA625C11FEF004D1F22 /* IPv4.swift in Sources */,
|
||||
C3D9E35525675EE10040E4F3 /* MIMETypeUtil.m in Sources */,
|
||||
FD705A8E278CE29800F16121 /* String+Localization.swift in Sources */,
|
||||
FD705A8E278CE29800F16121 /* String+Localized.swift in Sources */,
|
||||
FD9004162818B46700ABAAF6 /* JobRunnerError.swift in Sources */,
|
||||
FD09797227FAA2F500936362 /* Optional+Utilities.swift in Sources */,
|
||||
FD7162DB281B6C440060647B /* TypedTableAlias.swift in Sources */,
|
||||
C3A7219A2558C1660043A11F /* AnyPromise+Conversion.swift in Sources */,
|
||||
C300A6322554B6D100555489 /* NSDate+Timestamp.mm in Sources */,
|
||||
FD17D7E727F6A16700122BE0 /* _003_YDBToGRDBMigration.swift in Sources */,
|
||||
|
@ -5103,6 +5115,7 @@
|
|||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
FDF2220B2818F38D000A4995 /* SessionApp.swift in Sources */,
|
||||
B8041AA725C90927003C2166 /* TypingIndicatorCell.swift in Sources */,
|
||||
B8CCF63723961D6D0091D419 /* NewDMVC.swift in Sources */,
|
||||
452EC6DF205E9E30000E787C /* MediaGalleryViewController.swift in Sources */,
|
||||
|
@ -5156,7 +5169,6 @@
|
|||
4CC613362227A00400E21A3A /* ConversationSearch.swift in Sources */,
|
||||
B82149B825D60393009C0F2A /* BlockedModal.swift in Sources */,
|
||||
B82B408C239A068800A248E7 /* RegisterVC.swift in Sources */,
|
||||
346129991FD1E4DA00532771 /* SignalApp.m in Sources */,
|
||||
34BECE301F7ABCF800D7438D /* GifPickerLayout.swift in Sources */,
|
||||
C331FFFE2558FF3B00070591 /* ConversationCell.swift in Sources */,
|
||||
FD659AC027A7649600F12C02 /* MessageRequestsViewController.swift in Sources */,
|
||||
|
@ -5167,7 +5179,6 @@
|
|||
FD88BAD927A7439C00BBC442 /* MessageRequestsCell.swift in Sources */,
|
||||
45A6DAD61EBBF85500893231 /* ReminderView.swift in Sources */,
|
||||
B82B408E239DC00D00A248E7 /* DisplayNameVC.swift in Sources */,
|
||||
B8214A2B25D63EB9009C0F2A /* MessagesTableView.swift in Sources */,
|
||||
B835246E25C38ABF0089A44F /* ConversationVC.swift in Sources */,
|
||||
B8AF4BB426A5204600583500 /* SendSeedModal.swift in Sources */,
|
||||
B821494625D4D6FF009C0F2A /* URLModal.swift in Sources */,
|
||||
|
@ -5203,6 +5214,7 @@
|
|||
B8CCF6432397711F0091D419 /* SettingsVC.swift in Sources */,
|
||||
C354E75A23FE2A7600CE22E3 /* BaseVC.swift in Sources */,
|
||||
45C0DC1B1E68FE9000E04C47 /* UIApplication+OWS.swift in Sources */,
|
||||
FDF222072818CECF000A4995 /* ConversationViewModel.swift in Sources */,
|
||||
4539B5861F79348F007141FF /* PushRegistrationManager.swift in Sources */,
|
||||
B8041A9525C8FA1D003C2166 /* MediaLoaderView.swift in Sources */,
|
||||
45F32C232057297A00A300D5 /* MediaPageViewController.swift in Sources */,
|
||||
|
|
|
@ -1,8 +0,0 @@
|
|||
@import Foundation;
|
||||
|
||||
typedef NS_ENUM(NSUInteger, ConversationViewAction) {
|
||||
ConversationViewActionNone,
|
||||
ConversationViewActionCompose,
|
||||
ConversationViewActionAudioCall,
|
||||
ConversationViewActionVideoCall,
|
||||
};
|
|
@ -0,0 +1,3 @@
|
|||
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
|
||||
|
||||
import Foundation
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -0,0 +1,85 @@
|
|||
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
|
||||
|
||||
import Foundation
|
||||
import SessionUtilitiesKit
|
||||
import SessionMessagingKit
|
||||
|
||||
public struct SessionApp {
|
||||
static let homeViewController: Atomic<HomeVC?> = Atomic(nil)
|
||||
|
||||
// MARK: - View Convenience Methods
|
||||
|
||||
public static func presentConversation(for recipientId: String, action: ConversationViewModel.Action = .none, animated: Bool) {
|
||||
let maybeThread: SessionThread? = GRDBStorage.shared.write { db in
|
||||
SessionThread.fetchOrCreate(db, id: recipientId, variant: .contact)
|
||||
}
|
||||
|
||||
guard let thread: SessionThread = maybeThread else { return }
|
||||
|
||||
self.presentConversation(for: thread, action: action, animated: animated)
|
||||
}
|
||||
|
||||
public static func presentConversation(for threadId: String, animated: Bool) {
|
||||
guard let thread: SessionThread = GRDBStorage.shared.read({ db in try SessionThread.fetchOne(db, id: threadId) }) else {
|
||||
SNLog("Unable to find thread with id:\(threadId)")
|
||||
return
|
||||
}
|
||||
|
||||
self.presentConversation(for: thread, animated: animated)
|
||||
}
|
||||
|
||||
public static func presentConversation(
|
||||
for thread: SessionThread,
|
||||
action: ConversationViewModel.Action = .none,
|
||||
focusInteractionId: Int64? = nil,
|
||||
animated: Bool
|
||||
) {
|
||||
guard Thread.isMainThread else {
|
||||
DispatchQueue.main.async {
|
||||
self.presentConversation(
|
||||
for: thread,
|
||||
action: action,
|
||||
focusInteractionId: focusInteractionId,
|
||||
animated: animated
|
||||
)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
homeViewController.wrappedValue?.show(
|
||||
thread,
|
||||
with: action,
|
||||
highlightedInteractionId: focusInteractionId, // TODO: Confirm this
|
||||
animated: animated
|
||||
)
|
||||
}
|
||||
|
||||
// MARK: - Functions
|
||||
|
||||
public static func resetAppData(onReset: (() -> ())? = nil) {
|
||||
// This _should_ be wiped out below.
|
||||
Logger.error("")
|
||||
DDLog.flushLog()
|
||||
|
||||
OWSStorage.resetAllStorage()
|
||||
OWSUserProfile.resetProfileStorage()
|
||||
Environment.shared.preferences.clear()
|
||||
AppEnvironment.shared.notificationPresenter.clearAllNotifications()
|
||||
|
||||
onReset?()
|
||||
exit(0)
|
||||
}
|
||||
|
||||
public static func showHomeView() {
|
||||
guard Thread.isMainThread else {
|
||||
DispatchQueue.main.async {
|
||||
self.showHomeView()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
let homeViewController: HomeVC = HomeVC()
|
||||
let navController: UINavigationController = UINavigationController(rootViewController: homeViewController)
|
||||
(UIApplication.shared.delegate as? AppDelegate)?.window?.rootViewController = navController
|
||||
}
|
||||
}
|
|
@ -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
|
|
@ -1,167 +0,0 @@
|
|||
//
|
||||
// Copyright (c) 2019 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
#import "SignalApp.h"
|
||||
#import "Session-Swift.h"
|
||||
#import <SignalCoreKit/Threading.h>
|
||||
#import <SessionMessagingKit/Environment.h>
|
||||
#import <SessionMessagingKit/OWSPrimaryStorage.h>
|
||||
#import <SessionMessagingKit/TSContactThread.h>
|
||||
#import <SessionMessagingKit/TSGroupThread.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@implementation SignalApp
|
||||
|
||||
+ (instancetype)sharedApp
|
||||
{
|
||||
static SignalApp *sharedApp = nil;
|
||||
static dispatch_once_t onceToken;
|
||||
dispatch_once(&onceToken, ^{
|
||||
sharedApp = [[self alloc] initDefault];
|
||||
});
|
||||
return sharedApp;
|
||||
}
|
||||
|
||||
- (instancetype)initDefault
|
||||
{
|
||||
self = [super init];
|
||||
|
||||
if (!self) {
|
||||
return self;
|
||||
}
|
||||
|
||||
OWSSingletonAssert();
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
#pragma mark - Singletons
|
||||
|
||||
- (void)setup {
|
||||
[[NSNotificationCenter defaultCenter] addObserver:self
|
||||
selector:@selector(didChangeCallLoggingPreference:)
|
||||
name:OWSPreferencesCallLoggingDidChangeNotification
|
||||
object:nil];
|
||||
}
|
||||
|
||||
#pragma mark - View Convenience Methods
|
||||
|
||||
- (void)presentConversationForRecipientId:(NSString *)recipientId animated:(BOOL)isAnimated
|
||||
{
|
||||
[self presentConversationForRecipientId:recipientId action:ConversationViewActionNone animated:(BOOL)isAnimated];
|
||||
}
|
||||
|
||||
- (void)presentConversationForRecipientId:(NSString *)recipientId
|
||||
action:(ConversationViewAction)action
|
||||
animated:(BOOL)isAnimated
|
||||
{
|
||||
__block TSThread *thread = nil;
|
||||
[LKStorage writeSyncWithBlock:^(YapDatabaseReadWriteTransaction *_Nonnull transaction) {
|
||||
thread = [TSContactThread getOrCreateThreadWithContactSessionID:recipientId transaction:transaction];
|
||||
}];
|
||||
[self presentConversationForThread:thread action:action animated:(BOOL)isAnimated];
|
||||
}
|
||||
|
||||
- (void)presentConversationForThreadId:(NSString *)threadId animated:(BOOL)isAnimated
|
||||
{
|
||||
OWSAssertDebug(threadId.length > 0);
|
||||
|
||||
TSThread *thread = [TSThread fetchObjectWithUniqueID:threadId];
|
||||
if (thread == nil) {
|
||||
OWSFailDebug(@"unable to find thread with id: %@", threadId);
|
||||
return;
|
||||
}
|
||||
|
||||
[self presentConversationForThread:thread animated:isAnimated];
|
||||
}
|
||||
|
||||
- (void)presentConversationForThread:(TSThread *)thread animated:(BOOL)isAnimated
|
||||
{
|
||||
[self presentConversationForThread:thread action:ConversationViewActionNone animated:isAnimated];
|
||||
}
|
||||
|
||||
- (void)presentConversationForThread:(TSThread *)thread action:(ConversationViewAction)action animated:(BOOL)isAnimated
|
||||
{
|
||||
[self presentConversationForThread:thread action:action focusMessageId:nil animated:isAnimated];
|
||||
}
|
||||
|
||||
- (void)presentConversationForThread:(TSThread *)thread
|
||||
action:(ConversationViewAction)action
|
||||
focusMessageId:(nullable NSString *)focusMessageId
|
||||
animated:(BOOL)isAnimated
|
||||
{
|
||||
OWSAssertIsOnMainThread();
|
||||
|
||||
OWSLogInfo(@"");
|
||||
|
||||
if (!thread) {
|
||||
OWSFailDebug(@"Can't present nil thread.");
|
||||
return;
|
||||
}
|
||||
|
||||
DispatchMainThreadSafe(^{
|
||||
[self.homeViewController show:thread with:action highlightedMessageID:focusMessageId animated:isAnimated];
|
||||
});
|
||||
}
|
||||
|
||||
- (void)presentConversationAndScrollToFirstUnreadMessageForThreadId:(NSString *)threadId animated:(BOOL)isAnimated
|
||||
{
|
||||
OWSAssertIsOnMainThread();
|
||||
OWSAssertDebug(threadId.length > 0);
|
||||
|
||||
OWSLogInfo(@"");
|
||||
|
||||
TSThread *thread = [TSThread fetchObjectWithUniqueID:threadId];
|
||||
if (thread == nil) {
|
||||
OWSFailDebug(@"unable to find thread with id: %@", threadId);
|
||||
return;
|
||||
}
|
||||
|
||||
DispatchMainThreadSafe(^{
|
||||
[self.homeViewController show:thread with:ConversationViewActionNone highlightedMessageID:nil animated:isAnimated];
|
||||
});
|
||||
}
|
||||
|
||||
- (void)didChangeCallLoggingPreference:(NSNotification *)notitication
|
||||
{
|
||||
// [AppEnvironment.shared.callService createCallUIAdapter];
|
||||
}
|
||||
|
||||
#pragma mark - Methods
|
||||
|
||||
+ (void)resetAppData
|
||||
{
|
||||
[self resetAppData:nil];
|
||||
}
|
||||
|
||||
+ (void)resetAppData:(void (^__nullable)(void))onReset {
|
||||
// This _should_ be wiped out below.
|
||||
OWSLogError(@"");
|
||||
[DDLog flushLog];
|
||||
|
||||
[OWSStorage resetAllStorage];
|
||||
[OWSUserProfile resetProfileStorage];
|
||||
[Environment.shared.preferences clear];
|
||||
[AppEnvironment.shared.notificationPresenter clearAllNotifications];
|
||||
|
||||
if (onReset != nil) { onReset(); }
|
||||
exit(0);
|
||||
}
|
||||
|
||||
- (void)showHomeView
|
||||
{
|
||||
HomeVC *homeView = [HomeVC new];
|
||||
UINavigationController *navigationController = [[UINavigationController alloc] initWithRootViewController:homeView];
|
||||
AppDelegate *appDelegate = (AppDelegate *)[UIApplication sharedApplication].delegate;
|
||||
appDelegate.window.rootViewController = navigationController;
|
||||
OWSAssertDebug([navigationController.topViewController isKindOfClass:[HomeVC class]]);
|
||||
|
||||
// Clear the signUpFlowNavigationController.
|
||||
[self setSignUpFlowNavigationController:nil];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
|
@ -251,7 +251,7 @@ public class NotificationPresenter: NSObject, NotificationsProtocol {
|
|||
DispatchQueue.main.async {
|
||||
notificationBody = MentionUtilities.highlightMentions(
|
||||
in: (notificationBody ?? ""),
|
||||
threadId: thread.id
|
||||
threadVariant: thread.variant
|
||||
)
|
||||
let sound: Preferences.Sound? = self.requestSound(thread: thread)
|
||||
|
||||
|
@ -354,10 +354,6 @@ class NotificationActionHandler {
|
|||
|
||||
// MARK: - Dependencies
|
||||
|
||||
var signalApp: SignalApp {
|
||||
return SignalApp.shared()
|
||||
}
|
||||
|
||||
var notificationPresenter: NotificationPresenter {
|
||||
return AppEnvironment.shared.notificationPresenter
|
||||
}
|
||||
|
@ -421,12 +417,12 @@ class NotificationActionHandler {
|
|||
// can be visible to the user immediately upon opening the app, rather than having to watch
|
||||
// it animate in from the homescreen.
|
||||
let shouldAnimate = UIApplication.shared.applicationState == .active
|
||||
signalApp.presentConversationAndScrollToFirstUnreadMessage(forThreadId: threadId, animated: shouldAnimate)
|
||||
SessionApp.presentConversation(for: threadId, animated: shouldAnimate)
|
||||
return Promise.value(())
|
||||
}
|
||||
|
||||
func showHomeVC() -> Promise<Void> {
|
||||
signalApp.showHomeView()
|
||||
SessionApp.showHomeView()
|
||||
return Promise.value(())
|
||||
}
|
||||
|
||||
|
|
|
@ -4,7 +4,6 @@
|
|||
|
||||
#import "NotificationSettingsOptionsViewController.h"
|
||||
#import "Session-Swift.h"
|
||||
#import "SignalApp.h"
|
||||
#import <SessionMessagingKit/Environment.h>
|
||||
#import <SignalUtilitiesKit/SignalUtilitiesKit-Swift.h>
|
||||
|
||||
|
|
|
@ -13,7 +13,6 @@
|
|||
#import <SignalUtilitiesKit/SignalUtilitiesKit-Swift.h>
|
||||
#import <SessionUtilitiesKit/NSString+SSK.h>
|
||||
#import <SignalUtilitiesKit/ThreadUtil.h>
|
||||
#import <SessionMessagingKit/OWSReadReceiptManager.h>
|
||||
#import <SessionMessagingKit/SessionMessagingKit-Swift.h>
|
||||
#import <SignalUtilitiesKit/SignalUtilitiesKit-Swift.h>
|
||||
|
||||
|
@ -73,7 +72,7 @@ static NSString *const kSealedSenderInfoURL = @"https://signal.org/blog/sealed-s
|
|||
@"Label for the 'read receipts' setting.")
|
||||
accessibilityIdentifier:[NSString stringWithFormat:@"settings.privacy.%@", @"read_receipts"]
|
||||
isOnBlock:^{
|
||||
return [OWSReadReceiptManager.sharedManager areReadReceiptsEnabled];
|
||||
return [SSKPreferences areReadReceiptsEnabled];
|
||||
}
|
||||
isEnabledBlock:^{
|
||||
return YES;
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
@ -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 {
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -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)
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
|
||||
|
||||
import Foundation
|
||||
import AVKit
|
||||
import GRDB
|
||||
import Curve25519Kit
|
||||
import SessionUtilitiesKit
|
||||
|
@ -13,16 +14,17 @@ enum _003_YDBToGRDBMigration: Migration {
|
|||
|
||||
static func migrate(_ db: Database) throws {
|
||||
// MARK: - Process Contacts, Threads & Interactions
|
||||
|
||||
print("RAWR [\(Date().timeIntervalSince1970)] - SessionMessagingKit migration - Start")
|
||||
var shouldFailMigration: Bool = false
|
||||
var contacts: Set<Legacy.Contact> = []
|
||||
var validProfileIds: Set<String> = []
|
||||
var contactThreadIds: Set<String> = []
|
||||
|
||||
var legacyThreadIdToIdMap: [String: String] = [:]
|
||||
var threads: Set<TSThread> = []
|
||||
var disappearingMessagesConfiguration: [String: Legacy.DisappearingMessagesConfiguration] = [:]
|
||||
|
||||
var closedGroupKeys: [String: [TimeInterval: SessionUtilitiesKit.Legacy.KeyPair]] = [:]
|
||||
var closedGroupKeys: [String: [TimeInterval: SUKLegacy.KeyPair]] = [:]
|
||||
var closedGroupName: [String: String] = [:]
|
||||
var closedGroupFormation: [String: UInt64] = [:]
|
||||
var closedGroupModel: [String: TSGroupModel] = [:]
|
||||
|
@ -36,18 +38,41 @@ enum _003_YDBToGRDBMigration: Migration {
|
|||
// var openGroupServerToUniqueIdLookup: [String: [String]] = [:] // TODO: Not needed????
|
||||
|
||||
var interactions: [String: [TSInteraction]] = [:]
|
||||
var attachments: [String: TSAttachment] = [:]
|
||||
var attachments: [String: Legacy.Attachment] = [:]
|
||||
var processedAttachmentIds: Set<String> = []
|
||||
var outgoingReadReceiptsTimestampsMs: [String: Set<Int64>] = [:]
|
||||
|
||||
// Map the Legacy types for the NSKeyedUnarchiver
|
||||
NSKeyedUnarchiver.setClass(
|
||||
Legacy.Contact.self,
|
||||
forClassName: "SNContact"
|
||||
)
|
||||
NSKeyedUnarchiver.setClass(
|
||||
Legacy.Attachment.self,
|
||||
forClassName: "TSAttachment"
|
||||
)
|
||||
NSKeyedUnarchiver.setClass(
|
||||
Legacy.AttachmentStream.self,
|
||||
forClassName: "TSAttachmentStream"
|
||||
)
|
||||
NSKeyedUnarchiver.setClass(
|
||||
Legacy.AttachmentPointer.self,
|
||||
forClassName: "TSAttachmentPointer"
|
||||
)
|
||||
NSKeyedUnarchiver.setClass(
|
||||
Legacy.DisappearingConfigurationUpdateInfoMessage.self,
|
||||
forClassName: "OWSDisappearingConfigurationUpdateInfoMessage"
|
||||
)
|
||||
|
||||
Storage.read { transaction in
|
||||
// Process the Contacts
|
||||
transaction.enumerateRows(inCollection: Legacy.contactCollection) { _, object, _, _ in
|
||||
guard let contact = object as? Legacy.Contact else { return }
|
||||
contacts.insert(contact)
|
||||
validProfileIds.insert(contact.sessionID)
|
||||
}
|
||||
|
||||
print("RAWR [\(Date().timeIntervalSince1970)] - Process threads - Start")
|
||||
let userClosedGroupPublicKeys: [String] = transaction.allKeys(inCollection: Legacy.closedGroupPublicKeyCollection)
|
||||
|
||||
// Process the threads
|
||||
transaction.enumerateKeysAndObjects(inCollection: Legacy.threadCollection) { key, object, _ in
|
||||
|
@ -66,7 +91,6 @@ enum _003_YDBToGRDBMigration: Migration {
|
|||
disappearingMessagesConfiguration[threadId] = transaction
|
||||
.object(forKey: threadId, inCollection: Legacy.disappearingMessagesCollection)
|
||||
.asType(Legacy.DisappearingMessagesConfiguration.self)
|
||||
.defaulting(to: Legacy.DisappearingMessagesConfiguration.defaultWith(threadId))
|
||||
|
||||
// Process group-specific info
|
||||
guard let groupThread: TSGroupThread = thread as? TSGroupThread else {
|
||||
|
@ -89,14 +113,6 @@ enum _003_YDBToGRDBMigration: Migration {
|
|||
shouldFailMigration = true
|
||||
return
|
||||
}
|
||||
guard userClosedGroupPublicKeys.contains(publicKey) else {
|
||||
// TODO: Determine if we want to remove this
|
||||
SNLog("[Migration Error] Found unexpected invalid closed group public key")
|
||||
shouldFailMigration = true
|
||||
return
|
||||
}
|
||||
|
||||
let keyCollection: String = "\(Legacy.closedGroupKeyPairPrefix)\(publicKey)"
|
||||
|
||||
legacyThreadIdToIdMap[threadId] = publicKey
|
||||
closedGroupName[threadId] = groupThread.name(with: transaction)
|
||||
|
@ -107,10 +123,15 @@ enum _003_YDBToGRDBMigration: Migration {
|
|||
inCollection: Legacy.closedGroupZombieMembersCollection
|
||||
) as? Set<String>
|
||||
|
||||
// Note: If the user is no longer in a closed group then the group will still exist but the user
|
||||
// won't have the closed group public key anymore
|
||||
let keyCollection: String = "\(Legacy.closedGroupKeyPairPrefix)\(publicKey)"
|
||||
|
||||
transaction.enumerateKeysAndObjects(inCollection: keyCollection) { key, object, _ in
|
||||
guard let timestamp: TimeInterval = TimeInterval(key), let keyPair: SessionUtilitiesKit.Legacy.KeyPair = object as? SessionUtilitiesKit.Legacy.KeyPair else {
|
||||
return
|
||||
}
|
||||
guard
|
||||
let timestamp: TimeInterval = TimeInterval(key),
|
||||
let keyPair: SUKLegacy.KeyPair = object as? SUKLegacy.KeyPair
|
||||
else { return }
|
||||
|
||||
closedGroupKeys[threadId] = (closedGroupKeys[threadId] ?? [:])
|
||||
.setting(timestamp, keyPair)
|
||||
|
@ -153,7 +174,7 @@ enum _003_YDBToGRDBMigration: Migration {
|
|||
// Process attachments
|
||||
print("RAWR [\(Date().timeIntervalSince1970)] - Process attachments - Start")
|
||||
transaction.enumerateKeysAndObjects(inCollection: Legacy.attachmentsCollection) { key, object, _ in
|
||||
guard let attachment: TSAttachment = object as? TSAttachment else {
|
||||
guard let attachment: Legacy.Attachment = object as? Legacy.Attachment else {
|
||||
SNLog("[Migration Error] Unable to process attachment")
|
||||
shouldFailMigration = true
|
||||
return
|
||||
|
@ -227,7 +248,6 @@ enum _003_YDBToGRDBMigration: Migration {
|
|||
var legacyInteractionToIdMap: [String: Int64] = [:]
|
||||
var legacyInteractionIdentifierToIdMap: [String: Int64] = [:]
|
||||
var legacyInteractionIdentifierToIdFallbackMap: [String: Int64] = [:]
|
||||
var legacyAttachmentToIdMap: [String: String] = [:]
|
||||
|
||||
func identifier(
|
||||
for threadId: String,
|
||||
|
@ -296,7 +316,10 @@ enum _003_YDBToGRDBMigration: Migration {
|
|||
creationDateTimestamp: thread.creationDate.timeIntervalSince1970,
|
||||
shouldBeVisible: thread.shouldBeVisible,
|
||||
isPinned: thread.isPinned,
|
||||
messageDraft: thread.messageDraft,
|
||||
messageDraft: ((thread.messageDraft ?? "").isEmpty ?
|
||||
nil :
|
||||
thread.messageDraft
|
||||
),
|
||||
notificationMode: notificationMode,
|
||||
mutedUntilTimestamp: thread.mutedUntilDate?.timeIntervalSince1970
|
||||
).insert(db)
|
||||
|
@ -309,11 +332,15 @@ enum _003_YDBToGRDBMigration: Migration {
|
|||
durationSeconds: TimeInterval(config.durationSeconds)
|
||||
).insert(db)
|
||||
}
|
||||
else {
|
||||
try DisappearingMessagesConfiguration
|
||||
.defaultWith(threadId)
|
||||
.insert(db)
|
||||
}
|
||||
|
||||
// Closed Groups
|
||||
if (thread as? TSGroupThread)?.isClosedGroup == true {
|
||||
guard
|
||||
let legacyKeys = closedGroupKeys[legacyThreadId],
|
||||
let name: String = closedGroupName[legacyThreadId],
|
||||
let groupModel: TSGroupModel = closedGroupModel[legacyThreadId],
|
||||
let formationTimestamp: UInt64 = closedGroupFormation[legacyThreadId]
|
||||
|
@ -328,7 +355,10 @@ enum _003_YDBToGRDBMigration: Migration {
|
|||
formationTimestamp: TimeInterval(formationTimestamp)
|
||||
).insert(db)
|
||||
|
||||
try legacyKeys.forEach { timestamp, legacyKeys in
|
||||
// Note: If a user has left a closed group then they won't actually have any keys
|
||||
// but they should still be able to browse the old messages so we do want to allow
|
||||
// this case and migrate the rest of the info
|
||||
try closedGroupKeys[legacyThreadId]?.forEach { timestamp, legacyKeys in
|
||||
try ClosedGroupKeyPair(
|
||||
threadId: threadId,
|
||||
publicKey: legacyKeys.publicKey,
|
||||
|
@ -469,7 +499,6 @@ enum _003_YDBToGRDBMigration: Migration {
|
|||
recipientStateMap = [:]
|
||||
mostRecentFailureText = nil
|
||||
|
||||
|
||||
case let outgoingMessage as TSOutgoingMessage:
|
||||
variant = .standardOutgoing
|
||||
authorId = currentUserPublicKey
|
||||
|
@ -481,11 +510,32 @@ enum _003_YDBToGRDBMigration: Migration {
|
|||
mostRecentFailureText = outgoingMessage.mostRecentFailureText
|
||||
|
||||
case let infoMessage as TSInfoMessage:
|
||||
// Note: The legacy 'TSInfoMessage' didn't store the author id so there is no
|
||||
// way to determine who actually triggered the info message
|
||||
authorId = currentUserPublicKey
|
||||
body = ((infoMessage.body ?? "").isEmpty ?
|
||||
infoMessage.customMessage :
|
||||
infoMessage.body
|
||||
)
|
||||
body = {
|
||||
// Note: The 'DisappearingConfigurationUpdateInfoMessage' stored additional info and constructed
|
||||
// a string at display time so we want to continue that behaviour
|
||||
guard
|
||||
infoMessage.messageType == .disappearingMessagesUpdate,
|
||||
let updateMessage: Legacy.DisappearingConfigurationUpdateInfoMessage = infoMessage as? Legacy.DisappearingConfigurationUpdateInfoMessage,
|
||||
let infoMessageData: Data = try? JSONEncoder().encode(
|
||||
DisappearingMessagesConfiguration.MessageInfo(
|
||||
senderName: updateMessage.createdByRemoteName,
|
||||
isEnabled: updateMessage.configurationIsEnabled,
|
||||
durationSeconds: TimeInterval(updateMessage.configurationDurationSeconds)
|
||||
)
|
||||
),
|
||||
let infoMessageString: String = String(data: infoMessageData, encoding: .utf8)
|
||||
else {
|
||||
return ((infoMessage.body ?? "").isEmpty ?
|
||||
infoMessage.customMessage :
|
||||
infoMessage.body
|
||||
)
|
||||
}
|
||||
|
||||
return infoMessageString
|
||||
}()
|
||||
wasRead = infoMessage.wasRead
|
||||
expiresInSeconds = nil // Info messages don't expire
|
||||
expiresStartedAtMs = nil // Info messages don't expire
|
||||
|
@ -522,8 +572,15 @@ enum _003_YDBToGRDBMigration: Migration {
|
|||
timestampMs: Int64(legacyInteraction.timestamp),
|
||||
receivedAtTimestampMs: Int64(legacyInteraction.receivedAtTimestamp),
|
||||
wasRead: wasRead,
|
||||
expiresInSeconds: expiresInSeconds.map { TimeInterval($0) },
|
||||
expiresStartedAtMs: expiresStartedAtMs.map { Double($0) },
|
||||
// For both of these '0' used to be equivalent to null
|
||||
expiresInSeconds: ((expiresInSeconds ?? 0) > 0 ?
|
||||
expiresInSeconds.map { TimeInterval($0) } :
|
||||
nil
|
||||
),
|
||||
expiresStartedAtMs: ((expiresStartedAtMs ?? 0) > 0 ?
|
||||
expiresStartedAtMs.map { Double($0) } :
|
||||
nil
|
||||
),
|
||||
linkPreviewUrl: linkPreview?.urlString, // Only a soft link so save to set
|
||||
openGroupServerMessageId: openGroupServerMessageId.map { Int64($0) },
|
||||
openGroupWhisperMods: false, // TODO: This in SOGSV4
|
||||
|
@ -586,36 +643,75 @@ enum _003_YDBToGRDBMigration: Migration {
|
|||
// Handle any quote
|
||||
|
||||
if let quotedMessage: TSQuotedMessage = quotedMessage {
|
||||
let quoteAttachmentId: String? = quotedMessage.quotedAttachments
|
||||
.compactMap { attachmentInfo in
|
||||
if let attachmentId: String = attachmentInfo.attachmentId {
|
||||
return attachmentId
|
||||
}
|
||||
else if let attachmentId: String = attachmentInfo.thumbnailAttachmentPointerId {
|
||||
return attachmentId
|
||||
}
|
||||
// TODO: Looks like some of these might be busted???
|
||||
return attachmentInfo.thumbnailAttachmentStreamId
|
||||
var quoteAttachmentId: String? = quotedMessage.quotedAttachments
|
||||
.flatMap { attachmentInfo in
|
||||
return [
|
||||
// Prioritise the thumbnail as it means we won't
|
||||
// need to generate a new one
|
||||
attachmentInfo.thumbnailAttachmentStreamId,
|
||||
attachmentInfo.thumbnailAttachmentPointerId,
|
||||
attachmentInfo.attachmentId
|
||||
]
|
||||
.compactMap { $0 }
|
||||
}
|
||||
.first { attachments[$0] != nil }
|
||||
.first { attachmentId -> Bool in attachments[attachmentId] != nil }
|
||||
|
||||
guard quotedMessage.quotedAttachments.isEmpty || quoteAttachmentId != nil else {
|
||||
// TODO: Is it possible to hit this case if a quoted attachment hasn't been downloaded?
|
||||
SNLog("[Migration Error] Missing quote attachment")
|
||||
throw GRDBStorageError.migrationFailed
|
||||
// It looks like there can be cases where a quote can be quoting an
|
||||
// interaction that isn't associated with a profile we know about (eg.
|
||||
// if you join an open group and one of the first messages is a quote of
|
||||
// an older message not cached to the device) - this will cause a foreign
|
||||
// key constraint violation so in these cases just create an empty profile
|
||||
if !validProfileIds.contains(quotedMessage.authorId) {
|
||||
SNLog("[Migration Warning] Quote with unknown author found - Creating empty profile")
|
||||
|
||||
// Note: Need to upsert here because it's possible multiple quotes
|
||||
// will use the same invalid 'authorId' value resulting in a unique
|
||||
// constraint violation
|
||||
try Profile(
|
||||
id: quotedMessage.authorId,
|
||||
name: quotedMessage.authorId
|
||||
).save(db)
|
||||
}
|
||||
|
||||
// Note: It looks like there is a way for a quote to not have it's
|
||||
// associated attachmentId so let's try our best to track down the
|
||||
// original interaction and re-create the attachment link before
|
||||
// falling back to having no attachment in the quote
|
||||
if quoteAttachmentId == nil && !quotedMessage.quotedAttachments.isEmpty {
|
||||
quoteAttachmentId = interactions[legacyThreadId]?
|
||||
.first(where: {
|
||||
$0.timestamp == quotedMessage.timestamp &&
|
||||
(
|
||||
// Outgoing messages don't store the 'authorId' so we
|
||||
// need to compare against the 'currentUserPublicKey'
|
||||
// for those or cast to a TSIncomingMessage otherwise
|
||||
quotedMessage.authorId == currentUserPublicKey ||
|
||||
quotedMessage.authorId == ($0 as? TSIncomingMessage)?.authorId
|
||||
)
|
||||
})
|
||||
.asType(TSMessage.self)?
|
||||
.attachmentIds
|
||||
.firstObject
|
||||
.asType(String.self)
|
||||
|
||||
SNLog([
|
||||
"[Migration Warning] Quote with invalid attachmentId found",
|
||||
(quoteAttachmentId == nil ?
|
||||
"Unable to reconcile, leaving attachment blank" :
|
||||
"Original interaction found, using source attachment"
|
||||
)
|
||||
].joined(separator: " - "))
|
||||
}
|
||||
|
||||
// Setup the attachment and add it to the lookup (if it exists)
|
||||
let attachmentId: String? = try attachmentId(
|
||||
db,
|
||||
for: quoteAttachmentId,
|
||||
attachments: attachments
|
||||
isQuotedMessage: true,
|
||||
attachments: attachments,
|
||||
processedAttachmentIds: &processedAttachmentIds
|
||||
)
|
||||
|
||||
if let quoteAttachmentId: String = quoteAttachmentId, let attachmentId: String = attachmentId {
|
||||
legacyAttachmentToIdMap[quoteAttachmentId] = attachmentId
|
||||
}
|
||||
|
||||
// Create the quote
|
||||
try Quote(
|
||||
interactionId: interactionId,
|
||||
|
@ -642,13 +738,10 @@ enum _003_YDBToGRDBMigration: Migration {
|
|||
let attachmentId: String? = try attachmentId(
|
||||
db,
|
||||
for: linkPreview.imageAttachmentId,
|
||||
attachments: attachments
|
||||
attachments: attachments,
|
||||
processedAttachmentIds: &processedAttachmentIds
|
||||
)
|
||||
|
||||
if let legacyAttachmentId: String = linkPreview.imageAttachmentId, let attachmentId: String = attachmentId {
|
||||
legacyAttachmentToIdMap[legacyAttachmentId] = attachmentId
|
||||
}
|
||||
|
||||
// Note: It's possible for there to be duplicate values here so we use 'save'
|
||||
// instead of insert (ie. upsert)
|
||||
try LinkPreview(
|
||||
|
@ -663,8 +756,13 @@ enum _003_YDBToGRDBMigration: Migration {
|
|||
// Handle any attachments
|
||||
|
||||
try attachmentIds.forEach { legacyAttachmentId in
|
||||
guard let attachmentId: String = try attachmentId(db, for: legacyAttachmentId, interactionVariant: variant, attachments: attachments) else {
|
||||
// TODO: Is it possible to hit this case if an interaction hasn't been viewed?
|
||||
guard let attachmentId: String = try attachmentId(
|
||||
db,
|
||||
for: legacyAttachmentId,
|
||||
interactionVariant: variant,
|
||||
attachments: attachments,
|
||||
processedAttachmentIds: &processedAttachmentIds
|
||||
) else {
|
||||
SNLog("[Migration Error] Missing interaction attachment")
|
||||
throw GRDBStorageError.migrationFailed
|
||||
}
|
||||
|
@ -674,13 +772,13 @@ enum _003_YDBToGRDBMigration: Migration {
|
|||
interactionId: interactionId,
|
||||
attachmentId: attachmentId
|
||||
).insert(db)
|
||||
|
||||
legacyAttachmentToIdMap[legacyAttachmentId] = attachmentId
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
print("RAWR [\(Date().timeIntervalSince1970)] - Process thread inserts - End")
|
||||
|
||||
// Clear out processed data (give the memory a change to be freed)
|
||||
|
||||
contacts = []
|
||||
|
@ -706,6 +804,8 @@ enum _003_YDBToGRDBMigration: Migration {
|
|||
|
||||
// MARK: - Process Legacy Jobs
|
||||
|
||||
print("RAWR [\(Date().timeIntervalSince1970)] - Process jobs - Start")
|
||||
|
||||
var notifyPushServerJobs: Set<Legacy.NotifyPNServerJob> = []
|
||||
var messageReceiveJobs: Set<Legacy.MessageReceiveJob> = []
|
||||
var messageSendJobs: Set<Legacy.MessageSendJob> = []
|
||||
|
@ -737,6 +837,83 @@ enum _003_YDBToGRDBMigration: Migration {
|
|||
Legacy.AttachmentDownloadJob.self,
|
||||
forClassName: "SessionMessagingKit.AttachmentDownloadJob"
|
||||
)
|
||||
NSKeyedUnarchiver.setClass(
|
||||
Legacy.Message.self,
|
||||
forClassName: "SNMessage"
|
||||
)
|
||||
NSKeyedUnarchiver.setClass(
|
||||
Legacy.VisibleMessage.self,
|
||||
forClassName: "SNVisibleMessage"
|
||||
)
|
||||
NSKeyedUnarchiver.setClass(
|
||||
Legacy.Quote.self,
|
||||
forClassName: "SNQuote"
|
||||
)
|
||||
NSKeyedUnarchiver.setClass(
|
||||
Legacy.LinkPreview.self,
|
||||
forClassName: "SessionServiceKit.OWSLinkPreview" // Very old legacy name
|
||||
)
|
||||
NSKeyedUnarchiver.setClass(
|
||||
Legacy.LinkPreview.self,
|
||||
forClassName: "SNLinkPreview"
|
||||
)
|
||||
NSKeyedUnarchiver.setClass(
|
||||
Legacy.Profile.self,
|
||||
forClassName: "SNProfile"
|
||||
)
|
||||
NSKeyedUnarchiver.setClass(
|
||||
Legacy.OpenGroupInvitation.self,
|
||||
forClassName: "SNOpenGroupInvitation"
|
||||
)
|
||||
NSKeyedUnarchiver.setClass(
|
||||
Legacy.ControlMessage.self,
|
||||
forClassName: "SNControlMessage"
|
||||
)
|
||||
NSKeyedUnarchiver.setClass(
|
||||
Legacy.ReadReceipt.self,
|
||||
forClassName: "SNReadReceipt"
|
||||
)
|
||||
NSKeyedUnarchiver.setClass(
|
||||
Legacy.TypingIndicator.self,
|
||||
forClassName: "SNTypingIndicator"
|
||||
)
|
||||
NSKeyedUnarchiver.setClass(
|
||||
Legacy.ClosedGroupControlMessage.self,
|
||||
forClassName: "SessionMessagingKit.ClosedGroupControlMessage"
|
||||
)
|
||||
NSKeyedUnarchiver.setClass(
|
||||
Legacy.ClosedGroupControlMessage.KeyPairWrapper.self,
|
||||
forClassName: "ClosedGroupControlMessage.SNKeyPairWrapper"
|
||||
)
|
||||
NSKeyedUnarchiver.setClass(
|
||||
Legacy.DataExtractionNotification.self,
|
||||
forClassName: "SessionMessagingKit.DataExtractionNotification"
|
||||
)
|
||||
NSKeyedUnarchiver.setClass(
|
||||
Legacy.ExpirationTimerUpdate.self,
|
||||
forClassName: "SNExpirationTimerUpdate"
|
||||
)
|
||||
NSKeyedUnarchiver.setClass(
|
||||
Legacy.ConfigurationMessage.self,
|
||||
forClassName: "SNConfigurationMessage"
|
||||
)
|
||||
NSKeyedUnarchiver.setClass(
|
||||
Legacy.CMClosedGroup.self,
|
||||
forClassName: "SNClosedGroup"
|
||||
)
|
||||
NSKeyedUnarchiver.setClass(
|
||||
Legacy.CMContact.self,
|
||||
forClassName: "SNConfigurationMessage.SNConfigurationMessageContact"
|
||||
)
|
||||
NSKeyedUnarchiver.setClass(
|
||||
Legacy.UnsendRequest.self,
|
||||
forClassName: "SNUnsendRequest"
|
||||
)
|
||||
NSKeyedUnarchiver.setClass(
|
||||
Legacy.MessageRequestResponse.self,
|
||||
forClassName: "SNMessageRequestResponse"
|
||||
)
|
||||
|
||||
Storage.read { transaction in
|
||||
transaction.enumerateRows(inCollection: Legacy.notifyPushServerJobCollection) { _, object, _, _ in
|
||||
guard let job = object as? Legacy.NotifyPNServerJob else { return }
|
||||
|
@ -764,8 +941,12 @@ enum _003_YDBToGRDBMigration: Migration {
|
|||
}
|
||||
}
|
||||
|
||||
print("RAWR [\(Date().timeIntervalSince1970)] - Process jobs - End")
|
||||
|
||||
// MARK: - Insert Jobs
|
||||
|
||||
print("RAWR [\(Date().timeIntervalSince1970)] - Process job inserts - Start")
|
||||
|
||||
// MARK: - --notifyPushServer
|
||||
|
||||
try autoreleasepool {
|
||||
|
@ -805,7 +986,18 @@ enum _003_YDBToGRDBMigration: Migration {
|
|||
return
|
||||
}
|
||||
|
||||
let threadId: String? = MessageReceiver.extractSenderPublicKey(db, from: envelope)
|
||||
let threadId: String?
|
||||
|
||||
switch envelope.type {
|
||||
// For closed group messages the 'groupPublicKey' is stored in the
|
||||
// 'envelope.source' value and that should be used for the 'threadId'
|
||||
case .closedGroupMessage:
|
||||
threadId = envelope.source
|
||||
break
|
||||
|
||||
default:
|
||||
threadId = MessageReceiver.extractSenderPublicKey(db, from: envelope)
|
||||
}
|
||||
|
||||
_ = try Job(
|
||||
failureCount: legacyJob.failureCount,
|
||||
|
@ -873,7 +1065,8 @@ enum _003_YDBToGRDBMigration: Migration {
|
|||
destination: legacyJob.destination,
|
||||
variant: {
|
||||
switch legacyJob.message {
|
||||
case is ExpirationTimerUpdate: return .infoDisappearingMessagesUpdate
|
||||
case is Legacy.ExpirationTimerUpdate:
|
||||
return .infoDisappearingMessagesUpdate
|
||||
default: return nil
|
||||
}
|
||||
}(),
|
||||
|
@ -895,7 +1088,7 @@ enum _003_YDBToGRDBMigration: Migration {
|
|||
// in these cases the 'interactionId' value will be nil
|
||||
interactionId: interactionId,
|
||||
destination: legacyJob.destination,
|
||||
message: legacyJob.message
|
||||
message: legacyJob.message.toNonLegacy()
|
||||
)
|
||||
)?.inserted(db)
|
||||
|
||||
|
@ -936,7 +1129,7 @@ enum _003_YDBToGRDBMigration: Migration {
|
|||
SNLog("[Migration Error] attachmentDownload job unable to find interaction")
|
||||
throw GRDBStorageError.migrationFailed
|
||||
}
|
||||
guard let attachmentId: String = legacyAttachmentToIdMap[legacyJob.attachmentID] else {
|
||||
guard processedAttachmentIds.contains(legacyJob.attachmentID) else {
|
||||
SNLog("[Migration Error] attachmentDownload job unable to find attachment")
|
||||
throw GRDBStorageError.migrationFailed
|
||||
}
|
||||
|
@ -949,7 +1142,7 @@ enum _003_YDBToGRDBMigration: Migration {
|
|||
threadId: legacyThreadIdToIdMap[legacyJob.threadID],
|
||||
interactionId: interactionId,
|
||||
details: AttachmentDownloadJob.Details(
|
||||
attachmentId: attachmentId
|
||||
attachmentId: legacyJob.attachmentID
|
||||
)
|
||||
)?.inserted(db)
|
||||
}
|
||||
|
@ -971,8 +1164,12 @@ enum _003_YDBToGRDBMigration: Migration {
|
|||
}
|
||||
}
|
||||
|
||||
print("RAWR [\(Date().timeIntervalSince1970)] - Process job inserts - End")
|
||||
|
||||
// MARK: - Process Preferences
|
||||
|
||||
print("RAWR [\(Date().timeIntervalSince1970)] - Process preferences inserts - Start")
|
||||
|
||||
var legacyPreferences: [String: Any] = [:]
|
||||
|
||||
Storage.read { transaction in
|
||||
|
@ -1027,24 +1224,39 @@ enum _003_YDBToGRDBMigration: Migration {
|
|||
db[.hasHiddenMessageRequests] = CurrentAppContext().appUserDefaults()
|
||||
.bool(forKey: Legacy.userDefaultsHasHiddenMessageRequests)
|
||||
|
||||
print("RAWR [\(Date().timeIntervalSince1970)] - Process thread inserts - End")
|
||||
print("RAWR [\(Date().timeIntervalSince1970)] - Process preferences inserts - End")
|
||||
|
||||
print("RAWR Done!!!")
|
||||
}
|
||||
|
||||
// MARK: - Convenience
|
||||
|
||||
private static func attachmentId(_ db: Database, for legacyAttachmentId: String?, interactionVariant: Interaction.Variant? = nil, attachments: [String: TSAttachment]) throws -> String? {
|
||||
private static func attachmentId(
|
||||
_ db: Database,
|
||||
for legacyAttachmentId: String?,
|
||||
interactionVariant: Interaction.Variant? = nil,
|
||||
isQuotedMessage: Bool = false,
|
||||
attachments: [String: Legacy.Attachment],
|
||||
processedAttachmentIds: inout Set<String>
|
||||
) throws -> String? {
|
||||
guard let legacyAttachmentId: String = legacyAttachmentId else { return nil }
|
||||
|
||||
guard let legacyAttachment: TSAttachment = attachments[legacyAttachmentId] else {
|
||||
SNLog("[Migration Error] Missing attachment")
|
||||
throw GRDBStorageError.migrationFailed
|
||||
guard !processedAttachmentIds.contains(legacyAttachmentId) else {
|
||||
guard isQuotedMessage else {
|
||||
SNLog("[Migration Error] Attempted to process duplicate attachment")
|
||||
throw GRDBStorageError.migrationFailed
|
||||
}
|
||||
|
||||
return legacyAttachmentId
|
||||
}
|
||||
|
||||
guard let legacyAttachment: Legacy.Attachment = attachments[legacyAttachmentId] else {
|
||||
SNLog("[Migration Warning] Missing attachment - interaction will appear as blank")
|
||||
return nil
|
||||
}
|
||||
|
||||
let state: Attachment.State = {
|
||||
switch legacyAttachment {
|
||||
case let stream as TSAttachmentStream: // Outgoing or already downloaded
|
||||
case let stream as Legacy.AttachmentStream: // Outgoing or already downloaded
|
||||
switch interactionVariant {
|
||||
case .standardOutgoing: return (stream.isUploaded ? .uploaded : .pending)
|
||||
default: return .downloaded
|
||||
|
@ -1056,28 +1268,90 @@ enum _003_YDBToGRDBMigration: Migration {
|
|||
}()
|
||||
let size: CGSize = {
|
||||
switch legacyAttachment {
|
||||
case let stream as TSAttachmentStream: return stream.calculateImageSize()
|
||||
case let pointer as TSAttachmentPointer: return pointer.mediaSize
|
||||
case let stream as Legacy.AttachmentStream:
|
||||
guard let originalFilePath: String = Attachment.originalFilePath(id: legacyAttachmentId, mimeType: stream.contentType, sourceFilename: stream.localRelativeFilePath) else {
|
||||
return .zero
|
||||
}
|
||||
|
||||
return Attachment
|
||||
.imageSize(
|
||||
contentType: stream.contentType,
|
||||
originalFilePath: originalFilePath
|
||||
)
|
||||
.defaulting(to: .zero)
|
||||
|
||||
case let pointer as Legacy.AttachmentPointer: return pointer.mediaSize
|
||||
default: return CGSize.zero
|
||||
}
|
||||
}()
|
||||
let (isValid, duration): (Bool, TimeInterval?) = {
|
||||
guard
|
||||
let stream: Legacy.AttachmentStream = legacyAttachment as? Legacy.AttachmentStream,
|
||||
let originalFilePath: String = Attachment.originalFilePath(
|
||||
id: legacyAttachmentId,
|
||||
mimeType: stream.contentType,
|
||||
sourceFilename: stream.localRelativeFilePath
|
||||
)
|
||||
else {
|
||||
return (false, nil)
|
||||
}
|
||||
|
||||
if stream.isAudio {
|
||||
if let cachedDuration: TimeInterval = stream.cachedAudioDurationSeconds?.doubleValue, cachedDuration > 0 {
|
||||
return (true, cachedDuration)
|
||||
}
|
||||
|
||||
let (isValid, duration): (Bool, TimeInterval?) = Attachment.determineValidityAndDuration(
|
||||
contentType: stream.contentType,
|
||||
originalFilePath: originalFilePath
|
||||
)
|
||||
|
||||
return (isValid, duration)
|
||||
}
|
||||
|
||||
if stream.isVideo {
|
||||
let videoPlayer: AVPlayer = AVPlayer(url: URL(fileURLWithPath: originalFilePath))
|
||||
let duration: TimeInterval? = videoPlayer.currentItem
|
||||
.map { item -> TimeInterval in
|
||||
// Accorting to the CMTime docs "value/timescale = seconds"
|
||||
(TimeInterval(item.duration.value) / TimeInterval(item.duration.timescale))
|
||||
}
|
||||
|
||||
return ((duration ?? 0) > 0, duration)
|
||||
}
|
||||
|
||||
if stream.isVisualMedia {
|
||||
return (stream.isValidVisualMedia, nil)
|
||||
}
|
||||
|
||||
return (true, nil)
|
||||
}()
|
||||
|
||||
let attachment: Attachment = try Attachment(
|
||||
|
||||
_ = try Attachment(
|
||||
// Note: The legacy attachment object used a UUID string for it's id as well
|
||||
// and saved files using these id's so just used the existing id so we don't
|
||||
// need to bother renaming files as part of the migration
|
||||
id: legacyAttachmentId,
|
||||
serverId: "\(legacyAttachment.serverId)",
|
||||
variant: (legacyAttachment.isVoiceMessage ? .voiceMessage : .standard),
|
||||
variant: (legacyAttachment.attachmentType == .voiceMessage ? .voiceMessage : .standard),
|
||||
state: state,
|
||||
contentType: legacyAttachment.contentType,
|
||||
byteCount: UInt(legacyAttachment.byteCount),
|
||||
creationTimestamp: (legacyAttachment as? TSAttachmentStream)?.creationTimestamp.timeIntervalSince1970,
|
||||
creationTimestamp: (legacyAttachment as? Legacy.AttachmentStream)?.creationTimestamp.timeIntervalSince1970,
|
||||
sourceFilename: legacyAttachment.sourceFilename,
|
||||
downloadUrl: legacyAttachment.downloadURL,
|
||||
width: (size == .zero ? nil : UInt(size.width)),
|
||||
height: (size == .zero ? nil : UInt(size.height)),
|
||||
duration: duration,
|
||||
isValid: isValid,
|
||||
encryptionKey: legacyAttachment.encryptionKey,
|
||||
digest: (legacyAttachment as? TSAttachmentStream)?.digest,
|
||||
digest: (legacyAttachment as? Legacy.AttachmentStream)?.digest,
|
||||
caption: legacyAttachment.caption
|
||||
).inserted(db)
|
||||
|
||||
return attachment.id
|
||||
processedAttachmentIds.insert(legacyAttachmentId)
|
||||
|
||||
return legacyAttachmentId
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,9 +2,13 @@
|
|||
|
||||
import Foundation
|
||||
import GRDB
|
||||
import PromiseKit
|
||||
import SignalCoreKit
|
||||
import SessionUtilitiesKit
|
||||
import AVFAudio
|
||||
import AVFoundation
|
||||
|
||||
public struct Attachment: Codable, Identifiable, FetchableRecord, PersistableRecord, TableRecord, ColumnExpressible {
|
||||
public struct Attachment: Codable, Identifiable, Equatable, FetchableRecord, PersistableRecord, TableRecord, ColumnExpressible {
|
||||
public static var databaseTableName: String { "attachment" }
|
||||
internal static let interactionAttachments = belongsTo(InteractionAttachment.self)
|
||||
fileprivate static let quote = belongsTo(Quote.self)
|
||||
|
@ -24,6 +28,8 @@ public struct Attachment: Codable, Identifiable, FetchableRecord, PersistableRec
|
|||
case localRelativeFilePath
|
||||
case width
|
||||
case height
|
||||
case duration
|
||||
case isValid
|
||||
case encryptionKey
|
||||
case digest
|
||||
case caption
|
||||
|
@ -44,7 +50,7 @@ public struct Attachment: Codable, Identifiable, FetchableRecord, PersistableRec
|
|||
}
|
||||
|
||||
/// A unique identifier for the attachment
|
||||
public let id: String = UUID().uuidString
|
||||
public let id: String
|
||||
|
||||
/// The id for the attachment returned by the server
|
||||
///
|
||||
|
@ -93,6 +99,12 @@ public struct Attachment: Codable, Identifiable, FetchableRecord, PersistableRec
|
|||
/// The height of the attachment, this will be `null` for non-visual attachment types
|
||||
public let height: UInt?
|
||||
|
||||
/// The number of seconds the attachment plays for (this will only be set for video and audio attachment types)
|
||||
public let duration: TimeInterval?
|
||||
|
||||
/// A flag indicating whether the attachment data downloaded is valid for it's content type
|
||||
public let isValid: Bool
|
||||
|
||||
/// The key used to decrypt the attachment
|
||||
public let encryptionKey: Data?
|
||||
|
||||
|
@ -105,6 +117,7 @@ public struct Attachment: Codable, Identifiable, FetchableRecord, PersistableRec
|
|||
// MARK: - Initialization
|
||||
|
||||
public init(
|
||||
id: String = UUID().uuidString,
|
||||
serverId: String? = nil,
|
||||
variant: Variant,
|
||||
state: State = .pending,
|
||||
|
@ -116,10 +129,13 @@ public struct Attachment: Codable, Identifiable, FetchableRecord, PersistableRec
|
|||
localRelativeFilePath: String? = nil,
|
||||
width: UInt? = nil,
|
||||
height: UInt? = nil,
|
||||
duration: TimeInterval? = nil,
|
||||
isValid: Bool = false,
|
||||
encryptionKey: Data? = nil,
|
||||
digest: Data? = nil,
|
||||
caption: String? = nil
|
||||
) {
|
||||
self.id = id
|
||||
self.serverId = serverId
|
||||
self.variant = variant
|
||||
self.state = state
|
||||
|
@ -131,19 +147,21 @@ public struct Attachment: Codable, Identifiable, FetchableRecord, PersistableRec
|
|||
self.localRelativeFilePath = localRelativeFilePath
|
||||
self.width = width
|
||||
self.height = height
|
||||
self.duration = duration
|
||||
self.isValid = isValid
|
||||
self.encryptionKey = encryptionKey
|
||||
self.digest = digest
|
||||
self.caption = caption
|
||||
}
|
||||
|
||||
/// This initializer should only be used when converting from either a LinkPreview or a SignalAttachment to an Attachment (prior to upload)
|
||||
public init?(
|
||||
id: String = UUID().uuidString,
|
||||
variant: Variant = .standard,
|
||||
contentType: String,
|
||||
dataSource: DataSource
|
||||
) {
|
||||
guard
|
||||
let originalFilePath: String = Attachment.originalFilePath(id: self.id, mimeType: contentType, sourceFilename: nil)
|
||||
else {
|
||||
guard let originalFilePath: String = Attachment.originalFilePath(id: id, mimeType: contentType, sourceFilename: nil) else {
|
||||
return nil
|
||||
}
|
||||
guard dataSource.write(toPath: originalFilePath) else { return nil }
|
||||
|
@ -152,7 +170,12 @@ public struct Attachment: Codable, Identifiable, FetchableRecord, PersistableRec
|
|||
contentType: contentType,
|
||||
originalFilePath: originalFilePath
|
||||
)
|
||||
let (isValid, duration): (Bool, TimeInterval?) = Attachment.determineValidityAndDuration(
|
||||
contentType: contentType,
|
||||
originalFilePath: originalFilePath
|
||||
)
|
||||
|
||||
self.id = id
|
||||
self.serverId = nil
|
||||
self.variant = variant
|
||||
self.state = .pending
|
||||
|
@ -164,6 +187,8 @@ public struct Attachment: Codable, Identifiable, FetchableRecord, PersistableRec
|
|||
self.localRelativeFilePath = nil
|
||||
self.width = imageSize.map { UInt(floor($0.width)) }
|
||||
self.height = imageSize.map { UInt(floor($0.height)) }
|
||||
self.duration = duration
|
||||
self.isValid = isValid
|
||||
self.encryptionKey = nil
|
||||
self.digest = nil
|
||||
self.caption = nil
|
||||
|
@ -223,7 +248,24 @@ public extension Attachment {
|
|||
encryptionKey: Data? = nil,
|
||||
digest: Data? = nil
|
||||
) -> Attachment {
|
||||
let (isValid, duration): (Bool, TimeInterval?) = {
|
||||
switch (self.state, state) {
|
||||
case (_, .downloaded):
|
||||
return Attachment.determineValidityAndDuration(
|
||||
contentType: contentType,
|
||||
originalFilePath: originalFilePath
|
||||
)
|
||||
|
||||
// Assume the data is already correct for "uploading" attachments (and don't override it)
|
||||
case (.uploading, .failed), (.uploaded, .failed): return (self.isValid, self.duration)
|
||||
case (_, .failed): return (false, nil)
|
||||
|
||||
default: return (self.isValid, self.duration)
|
||||
}
|
||||
}()
|
||||
|
||||
return Attachment(
|
||||
id: self.id,
|
||||
serverId: (serverId ?? self.serverId),
|
||||
variant: variant,
|
||||
state: (state ?? self.state),
|
||||
|
@ -235,6 +277,8 @@ public extension Attachment {
|
|||
localRelativeFilePath: (localRelativeFilePath ?? self.localRelativeFilePath),
|
||||
width: width,
|
||||
height: height,
|
||||
duration: duration,
|
||||
isValid: isValid,
|
||||
encryptionKey: (encryptionKey ?? self.encryptionKey),
|
||||
digest: (digest ?? self.digest),
|
||||
caption: self.caption
|
||||
|
@ -255,7 +299,8 @@ public extension Attachment {
|
|||
return (MIMETypeUtil.mimeType(forFileExtension: fileExtension) ?? OWSMimeTypeApplicationOctetStream)
|
||||
}
|
||||
|
||||
self.serverId = nil
|
||||
self.id = UUID().uuidString
|
||||
self.serverId = "\(proto.id)"
|
||||
self.variant = {
|
||||
let voiceMessageFlag: Int32 = SNProtoAttachmentPointer.SNProtoAttachmentPointerFlags
|
||||
.voiceMessage
|
||||
|
@ -276,6 +321,8 @@ public extension Attachment {
|
|||
self.localRelativeFilePath = nil
|
||||
self.width = (proto.hasWidth && proto.width > 0 ? UInt(proto.width) : nil)
|
||||
self.height = (proto.hasHeight && proto.height > 0 ? UInt(proto.height) : nil)
|
||||
self.duration = nil // Needs to be downloaded to be set
|
||||
self.isValid = false // Needs to be downloaded to be set
|
||||
self.encryptionKey = proto.key
|
||||
self.digest = proto.digest
|
||||
self.caption = (proto.hasCaption ? proto.caption : nil)
|
||||
|
@ -335,31 +382,56 @@ public extension Attachment {
|
|||
// MARK: - GRDB Interactions
|
||||
|
||||
public extension Attachment {
|
||||
static func fetchAllPendingAttachments(_ db: Database, for threadId: String) throws -> [Attachment] {
|
||||
return try Attachment
|
||||
.select(Attachment.Columns.allCases + [Interaction.Columns.id])
|
||||
.filter(Columns.variant == Variant.standard)
|
||||
.filter(Columns.state == State.pending)
|
||||
.joining(
|
||||
optional: Attachment.interactionAttachments
|
||||
.filter(Interaction.Columns.threadId == threadId)
|
||||
)
|
||||
.joining(
|
||||
optional: Attachment.quote
|
||||
.joining(
|
||||
required: Quote.interaction
|
||||
.filter(Interaction.Columns.threadId == threadId)
|
||||
)
|
||||
)//tmp.authorId
|
||||
.joining(
|
||||
optional: Attachment.linkPreview
|
||||
.joining(
|
||||
required: LinkPreview.interactions
|
||||
.filter(Interaction.Columns.threadId == threadId)
|
||||
)
|
||||
)
|
||||
.order(Interaction.Columns.id.desc) // Newest attachments first
|
||||
.fetchAll(db)
|
||||
struct DownloadInfo: FetchableRecord, Decodable {
|
||||
public let attachmentId: String
|
||||
public let interactionId: Int64
|
||||
}
|
||||
|
||||
static func pendingAttachmentDownloadInfo(for authorId: String) -> SQLRequest<Attachment.DownloadInfo> {
|
||||
let attachment: TypedTableAlias<Attachment> = TypedTableAlias()
|
||||
let interaction: TypedTableAlias<Interaction> = TypedTableAlias()
|
||||
let quote: TypedTableAlias<Quote> = TypedTableAlias()
|
||||
let interactionAttachment: TypedTableAlias<InteractionAttachment> = TypedTableAlias()
|
||||
let linkPreview: TypedTableAlias<LinkPreview> = TypedTableAlias()
|
||||
|
||||
// Note: In GRDB all joins need to run via their "association" system which doesn't support the type
|
||||
// of query we have below (a required join based on one of 3 optional joins) so we have to construct
|
||||
// the query manually
|
||||
return """
|
||||
SELECT DISTINCT
|
||||
\(attachment[.id]) AS attachmentId,
|
||||
\(interaction[.id]) AS interactionId
|
||||
|
||||
FROM \(Attachment.self)
|
||||
|
||||
JOIN \(Interaction.self) ON
|
||||
\(interaction[.authorId]) = \(SQL(sql: ":authorId", arguments: StatementArguments(["authorId": authorId]))) AND (
|
||||
\(interaction[.id]) = \(quote[.interactionId]) OR
|
||||
\(interaction[.id]) = \(interactionAttachment[.interactionId]) OR
|
||||
\(interaction[.linkPreviewUrl]) = \(linkPreview[.url])
|
||||
)
|
||||
|
||||
LEFT JOIN \(Quote.self) ON \(quote[.attachmentId]) = \(attachment[.id])
|
||||
LEFT JOIN \(InteractionAttachment.self) ON \(interactionAttachment[.attachmentId]) = \(attachment[.id])
|
||||
LEFT JOIN \(LinkPreview.self) ON
|
||||
\(linkPreview[.attachmentId]) = \(attachment[.id]) AND
|
||||
\(linkPreview[.variant]) = \(SQL(
|
||||
sql: ":variant",
|
||||
arguments: StatementArguments(["variant": LinkPreview.Variant.standard])
|
||||
))
|
||||
|
||||
WHERE
|
||||
\(attachment[.variant]) = \(SQL(
|
||||
sql: ":attachmentVariant",
|
||||
arguments: StatementArguments(["attachmentVariant": Attachment.Variant.standard])
|
||||
)) AND
|
||||
\(attachment[.state]) = \(SQL(
|
||||
sql: ":state",
|
||||
arguments: StatementArguments(["state": Attachment.State.pending])
|
||||
))
|
||||
|
||||
ORDER BY interactionId DESC
|
||||
"""
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -370,11 +442,11 @@ public extension Attachment {
|
|||
private static let thumbnailDimensionMedium: UInt = 450
|
||||
|
||||
/// This size is large enough to render full screen
|
||||
private static var thumbnailDimensionsLarge: CGFloat = {
|
||||
private static var thumbnailDimensionLarge: UInt = {
|
||||
let screenSizePoints: CGSize = UIScreen.main.bounds.size
|
||||
let minZoomFactor: CGFloat = 2 // TODO: Should this be screen scale?
|
||||
let minZoomFactor: CGFloat = UIScreen.main.scale
|
||||
|
||||
return (max(screenSizePoints.width, screenSizePoints.height) * minZoomFactor)
|
||||
return UInt(floor(max(screenSizePoints.width, screenSizePoints.height) * minZoomFactor))
|
||||
}()
|
||||
|
||||
private static var sharedDataAttachmentsDirPath: String = {
|
||||
|
@ -404,7 +476,7 @@ public extension Attachment {
|
|||
)
|
||||
}
|
||||
|
||||
static func imageSize(contentType: String, originalFilePath: String) -> CGSize? {
|
||||
internal static func imageSize(contentType: String, originalFilePath: String) -> CGSize? {
|
||||
let isVideo: Bool = MIMETypeUtil.isVideo(contentType)
|
||||
let isImage: Bool = MIMETypeUtil.isImage(contentType)
|
||||
let isAnimated: Bool = MIMETypeUtil.isAnimated(contentType)
|
||||
|
@ -423,15 +495,77 @@ public extension Attachment {
|
|||
static func videoStillImage(filePath: String) -> UIImage? {
|
||||
return try? OWSMediaUtils.thumbnail(
|
||||
forVideoAtPath: filePath,
|
||||
maxDimension: Attachment.thumbnailDimensionsLarge
|
||||
maxDimension: CGFloat(Attachment.thumbnailDimensionLarge)
|
||||
)
|
||||
}
|
||||
|
||||
internal static func determineValidityAndDuration(contentType: String, originalFilePath: String?) -> (isValid: Bool, duration: TimeInterval?) {
|
||||
guard let originalFilePath: String = originalFilePath else { return (false, nil) }
|
||||
|
||||
// Process audio attachments
|
||||
if MIMETypeUtil.isAudio(contentType) {
|
||||
do {
|
||||
let audioPlayer: AVAudioPlayer = try AVAudioPlayer(contentsOf: URL(fileURLWithPath: originalFilePath))
|
||||
|
||||
return ((audioPlayer.duration > 0), audioPlayer.duration)
|
||||
}
|
||||
catch {
|
||||
switch (error as NSError).code {
|
||||
case Int(kAudioFileInvalidFileError), Int(kAudioFileStreamError_InvalidFile):
|
||||
// Ignore "invalid audio file" errors
|
||||
return (false, nil) // TODO: Confirm this behaviour (previously returned 0)
|
||||
|
||||
default: return (false, nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Process image attachments
|
||||
if MIMETypeUtil.isImage(contentType) {
|
||||
return (
|
||||
NSData.ows_isValidImage(atPath: originalFilePath, mimeType: contentType),
|
||||
nil
|
||||
)
|
||||
}
|
||||
|
||||
// Process video attachments
|
||||
if MIMETypeUtil.isVideo(contentType) {
|
||||
let videoPlayer: AVPlayer = AVPlayer(url: URL(fileURLWithPath: originalFilePath))
|
||||
let durationSeconds: TimeInterval? = videoPlayer.currentItem
|
||||
.map { item -> TimeInterval in
|
||||
// Accorting to the CMTime docs "value/timescale = seconds"
|
||||
(TimeInterval(item.duration.value) / TimeInterval(item.duration.timescale))
|
||||
}
|
||||
|
||||
return (
|
||||
OWSMediaUtils.isValidVideo(path: originalFilePath),
|
||||
durationSeconds
|
||||
)
|
||||
}
|
||||
|
||||
// Any other attachment types are valid and have no duration
|
||||
return (true, nil)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Convenience
|
||||
|
||||
extension Attachment {
|
||||
var originalFilePath: String? {
|
||||
public enum ThumbnailSize {
|
||||
case small
|
||||
case medium
|
||||
case large
|
||||
|
||||
var dimension: UInt {
|
||||
switch self {
|
||||
case .small: return Attachment.thumbnailDimensionSmall
|
||||
case .medium: return Attachment.thumbnailDimensionMedium
|
||||
case .large: return Attachment.thumbnailDimensionLarge
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public var originalFilePath: String? {
|
||||
return Attachment.originalFilePath(
|
||||
id: self.id,
|
||||
mimeType: self.contentType,
|
||||
|
@ -453,18 +587,19 @@ extension Attachment {
|
|||
}
|
||||
|
||||
guard isImage || isAnimated else { return nil }
|
||||
guard NSData.ows_isValidImage(atPath: originalFilePath, mimeType: contentType) else {
|
||||
return nil
|
||||
}
|
||||
guard isValid else { return nil }
|
||||
|
||||
return UIImage(contentsOfFile: originalFilePath)
|
||||
}
|
||||
|
||||
var isImage: Bool { MIMETypeUtil.isImage(contentType) }
|
||||
var isVideo: Bool { MIMETypeUtil.isVideo(contentType) }
|
||||
var isAnimated: Bool { MIMETypeUtil.isAnimated(contentType) }
|
||||
public var isImage: Bool { MIMETypeUtil.isImage(contentType) }
|
||||
public var isVideo: Bool { MIMETypeUtil.isVideo(contentType) }
|
||||
public var isAnimated: Bool { MIMETypeUtil.isAnimated(contentType) }
|
||||
public var isAudio: Bool { MIMETypeUtil.isAudio(contentType) }
|
||||
|
||||
func readDataFromFile() throws -> Data? {
|
||||
public var isVisualMedia: Bool { isImage || isVideo || isAnimated }
|
||||
|
||||
public func readDataFromFile() throws -> Data? {
|
||||
guard let filePath: String = Attachment.originalFilePath(id: self.id, mimeType: self.contentType, sourceFilename: self.sourceFilename) else {
|
||||
return nil
|
||||
}
|
||||
|
@ -514,14 +649,18 @@ extension Attachment {
|
|||
)
|
||||
}
|
||||
|
||||
func thumbnailImageSmallSync() -> UIImage? {
|
||||
public func thumbnail(size: ThumbnailSize, success: @escaping (UIImage) -> (), failure: @escaping () -> ()) {
|
||||
loadThumbnail(with: size.dimension, success: success, failure: failure)
|
||||
}
|
||||
|
||||
func thumbnailSync(size: ThumbnailSize) -> UIImage? {
|
||||
guard isVideo || isImage || isAnimated else { return nil }
|
||||
|
||||
let semaphore: DispatchSemaphore = DispatchSemaphore(value: 0)
|
||||
var image: UIImage?
|
||||
|
||||
loadThumbnail(
|
||||
with: Attachment.thumbnailDimensionSmall,
|
||||
thumbnail(
|
||||
size: size,
|
||||
success: { loadedImage in
|
||||
image = loadedImage
|
||||
semaphore.signal()
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -7,6 +7,7 @@ import SessionUtilitiesKit
|
|||
public struct Contact: Codable, Identifiable, FetchableRecord, PersistableRecord, TableRecord, ColumnExpressible {
|
||||
public static var databaseTableName: String { "contact" }
|
||||
internal static let threadForeignKey = ForeignKey([Columns.id], to: [SessionThread.Columns.id])
|
||||
public static let profile = hasOne(Profile.self, using: Profile.contactForeignKey)
|
||||
|
||||
public typealias Columns = CodingKeys
|
||||
public enum CodingKeys: String, CodingKey, ColumnExpression {
|
||||
|
@ -37,6 +38,12 @@ public struct Contact: Codable, Identifiable, FetchableRecord, PersistableRecord
|
|||
/// This flag is used to determine whether this contact has ever been blocked (will be included in the config message if so)
|
||||
public let hasBeenBlocked: Bool
|
||||
|
||||
// MARK: - Relationships
|
||||
|
||||
public var profile: QueryInterfaceRequest<Profile> {
|
||||
request(for: Contact.profile)
|
||||
}
|
||||
|
||||
// MARK: - Initialization
|
||||
|
||||
public init(
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -7,25 +7,31 @@ import SessionUtilitiesKit
|
|||
public struct Interaction: Codable, Identifiable, Equatable, FetchableRecord, MutablePersistableRecord, TableRecord, ColumnExpressible {
|
||||
public static var databaseTableName: String { "interaction" }
|
||||
internal static let threadForeignKey = ForeignKey([Columns.threadId], to: [SessionThread.Columns.id])
|
||||
internal static let profileForeignKey = ForeignKey([Columns.authorId], to: [Profile.Columns.id])
|
||||
internal static let linkPreviewForeignKey = ForeignKey(
|
||||
[Columns.linkPreviewUrl],
|
||||
to: [LinkPreview.Columns.url]
|
||||
)
|
||||
internal static let thread = belongsTo(SessionThread.self, using: threadForeignKey)
|
||||
private static let profile = hasOne(Profile.self, using: profileForeignKey)
|
||||
public static let profile = hasOne(Profile.self, using: Profile.interactionForeignKey)
|
||||
internal static let interactionAttachments = hasMany(
|
||||
InteractionAttachment.self,
|
||||
using: InteractionAttachment.interactionForeignKey
|
||||
)
|
||||
internal static let attachments = hasMany(
|
||||
public static let attachments = hasMany(
|
||||
Attachment.self,
|
||||
through: interactionAttachments,
|
||||
using: InteractionAttachment.attachment
|
||||
)
|
||||
public static let quote = hasOne(Quote.self, using: Quote.interactionForeignKey)
|
||||
internal static let linkPreview = hasOne(LinkPreview.self, using: LinkPreview.interactionForeignKey)
|
||||
private static let recipientStates = hasMany(RecipientState.self, using: RecipientState.interactionForeignKey)
|
||||
|
||||
/// Whenever using this `linkPreview` association make sure to filter the result using `.filter(literal: Interaction.linkPreviewFilterLiteral)` to ensure the correct LinkPreview is returned
|
||||
public static let linkPreview = hasOne(LinkPreview.self, using: LinkPreview.interactionForeignKey)
|
||||
public static let linkPreviewFilterLiteral: SQL = {
|
||||
let interaction: TypedTableAlias<Interaction> = TypedTableAlias()
|
||||
let linkPreview: TypedTableAlias<LinkPreview> = TypedTableAlias()
|
||||
return "(ROUND((\(interaction[.timestampMs]) / 1000 / 100000) - 0.5) * 100000) = \(linkPreview[.timestamp])"
|
||||
}()
|
||||
public static let recipientStates = hasMany(RecipientState.self, using: RecipientState.interactionForeignKey)
|
||||
|
||||
public typealias Columns = CodingKeys
|
||||
public enum CodingKeys: String, CodingKey, ColumnExpression {
|
||||
|
@ -67,6 +73,20 @@ public struct Interaction: Codable, Identifiable, Equatable, FetchableRecord, Mu
|
|||
case infoMediaSavedNotification
|
||||
|
||||
case infoMessageRequestAccepted = 4000
|
||||
|
||||
// MARK: - Convenience
|
||||
|
||||
public var isInfoMessage: Bool {
|
||||
switch self {
|
||||
case .infoClosedGroupCreated, .infoClosedGroupUpdated, .infoClosedGroupCurrentUserLeft,
|
||||
.infoDisappearingMessagesUpdate, .infoScreenshotNotification, .infoMediaSavedNotification,
|
||||
.infoMessageRequestAccepted:
|
||||
return true
|
||||
|
||||
case .standardIncoming, .standardOutgoing, .standardIncomingDeleted:
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The `id` value is auto incremented by the database, if the `Interaction` hasn't been inserted into
|
||||
|
@ -83,6 +103,10 @@ public struct Interaction: Codable, Identifiable, Equatable, FetchableRecord, Mu
|
|||
public let threadId: String
|
||||
|
||||
/// The id of the user who sent the interaction, also used to expose the `profile` variable)
|
||||
///
|
||||
/// **Note:** For any "info" messages this value will always be the current user public key (this is because these
|
||||
/// messages are created locally based on control messages and the initiator of a control message doesn't always
|
||||
/// get transmitted)
|
||||
public let authorId: String
|
||||
|
||||
/// The type of interaction
|
||||
|
@ -156,20 +180,10 @@ public struct Interaction: Codable, Identifiable, Equatable, FetchableRecord, Mu
|
|||
}
|
||||
|
||||
public var linkPreview: QueryInterfaceRequest<LinkPreview> {
|
||||
let linkPreviewAlias: TableAlias = TableAlias()
|
||||
|
||||
return LinkPreview
|
||||
.aliased(linkPreviewAlias)
|
||||
.joining(
|
||||
required: LinkPreview.interactions
|
||||
.filter(literal: [
|
||||
"(ROUND((\(Interaction.Columns.timestampMs) / 1000 / 100000) - 0.5) * 100000)",
|
||||
"=",
|
||||
"\(linkPreviewAlias[LinkPreview.Columns.timestamp])"
|
||||
].joined(separator: " "))
|
||||
.limit(1) // Avoid joining to multiple interactions
|
||||
)
|
||||
.limit(1) // Avoid joining to multiple interactions
|
||||
/// **Note:** This equation **MUST** match the `linkPreviewFilterLiteral` logic
|
||||
let roundedTimestamp: Double = (round(((Double(timestampMs) / 1000) / 100000) - 0.5) * 100000)
|
||||
return request(for: Interaction.linkPreview)
|
||||
.filter(LinkPreview.Columns.timestamp == roundedTimestamp)
|
||||
}
|
||||
|
||||
public var recipientStates: QueryInterfaceRequest<RecipientState> {
|
||||
|
@ -320,11 +334,7 @@ public struct Interaction: Codable, Identifiable, Equatable, FetchableRecord, Mu
|
|||
.aliased(interactionAlias)
|
||||
.joining(
|
||||
required: Interaction.linkPreview
|
||||
.filter(literal: [
|
||||
"(ROUND((\(interactionAlias[Columns.timestampMs]) / 1000 / 100000) - 0.5) * 100000)",
|
||||
"=",
|
||||
"\(LinkPreview.Columns.timestamp)"
|
||||
].joined(separator: " "))
|
||||
.filter(literal: Interaction.linkPreviewFilterLiteral)
|
||||
)
|
||||
.fetchCount(db)
|
||||
let tmp = try linkPreview.fetchAll(db)
|
||||
|
@ -449,7 +459,7 @@ public extension Interaction {
|
|||
scheduleJobs(
|
||||
interactionIds: try Int64.fetchAll(
|
||||
db,
|
||||
interactionQuery.select(Interaction.Columns.id)
|
||||
interactionQuery.select(.id)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
@ -538,10 +548,10 @@ public extension Interaction {
|
|||
)
|
||||
}
|
||||
|
||||
/// Use the `Interaction.previewText` method directly where possible rather than this method as it
|
||||
/// makes it's own database queries
|
||||
func previewText(_ db: Database) -> String {
|
||||
switch variant {
|
||||
case .standardIncomingDeleted: return ""
|
||||
|
||||
case .standardIncoming, .standardOutgoing:
|
||||
struct AttachmentDescriptionInfo: Decodable, FetchableRecord {
|
||||
let id: String
|
||||
|
@ -549,36 +559,19 @@ public extension Interaction {
|
|||
let contentType: String
|
||||
let sourceFilename: String?
|
||||
}
|
||||
|
||||
var bodyDescription: String?
|
||||
|
||||
if let body: String = self.body, !body.isEmpty {
|
||||
bodyDescription = body
|
||||
}
|
||||
|
||||
if bodyDescription == nil {
|
||||
struct AttachmentBodyInfo: Decodable, FetchableRecord {
|
||||
let id: String
|
||||
let variant: Attachment.Variant
|
||||
let contentType: String
|
||||
let sourceFilename: String?
|
||||
}
|
||||
|
||||
|
||||
var targetBody: String? = self.body
|
||||
|
||||
if self.body == nil || self.body?.isEmpty == true {
|
||||
let maybeTextInfo: AttachmentDescriptionInfo? = try? AttachmentDescriptionInfo
|
||||
.fetchOne(
|
||||
db,
|
||||
attachments
|
||||
.select(
|
||||
Attachment.Columns.id,
|
||||
Attachment.Columns.state,
|
||||
Attachment.Columns.variant,
|
||||
Attachment.Columns.contentType,
|
||||
Attachment.Columns.sourceFilename
|
||||
)
|
||||
.select(.id, .state, .variant, .contentType, .sourceFilename)
|
||||
.filter(Attachment.Columns.contentType == OWSMimeTypeOversizeTextMessage)
|
||||
.filter(Attachment.Columns.state == Attachment.State.downloaded)
|
||||
)
|
||||
|
||||
|
||||
if
|
||||
let textInfo: AttachmentDescriptionInfo = maybeTextInfo,
|
||||
let filePath: String = Attachment.originalFilePath(
|
||||
|
@ -589,20 +582,15 @@ public extension Interaction {
|
|||
let data: Data = try? Data(contentsOf: URL(fileURLWithPath: filePath)),
|
||||
let dataString: String = String(data: data, encoding: .utf8)
|
||||
{
|
||||
bodyDescription = dataString.filterForDisplay
|
||||
targetBody = dataString.filterForDisplay
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
let attachmentDescription: String? = try? AttachmentDescriptionInfo
|
||||
.fetchOne(
|
||||
db,
|
||||
attachments
|
||||
.select(
|
||||
Attachment.Columns.id,
|
||||
Attachment.Columns.variant,
|
||||
Attachment.Columns.contentType,
|
||||
Attachment.Columns.sourceFilename
|
||||
)
|
||||
.select(.id, .variant, .contentType, .sourceFilename)
|
||||
.filter(Attachment.Columns.contentType != OWSMimeTypeOversizeTextMessage)
|
||||
)
|
||||
.map { info -> String in
|
||||
|
@ -612,6 +600,70 @@ public extension Interaction {
|
|||
sourceFilename: info.sourceFilename
|
||||
)
|
||||
}
|
||||
let isOpenGroupInvitation: Bool = (try? linkPreview
|
||||
.filter(LinkPreview.Columns.variant == LinkPreview.Variant.openGroupInvitation)
|
||||
.isNotEmpty(db))
|
||||
.defaulting(to: false)
|
||||
|
||||
return Interaction.previewText(
|
||||
variant: self.variant,
|
||||
body: targetBody,
|
||||
attachments: [],
|
||||
customAttachmentDescription: attachmentDescription,
|
||||
isOpenGroupInvitation: isOpenGroupInvitation
|
||||
)
|
||||
|
||||
case .infoMediaSavedNotification, .infoScreenshotNotification:
|
||||
// Note: This should only occur in 'contact' threads so the `threadId`
|
||||
// is the contact id
|
||||
return Interaction.previewText(
|
||||
variant: self.variant,
|
||||
body: self.body,
|
||||
authorDisplayName: Profile.displayName(db, id: threadId),
|
||||
attachments: []
|
||||
)
|
||||
|
||||
default: return Interaction.previewText(
|
||||
variant: self.variant,
|
||||
body: self.body,
|
||||
attachments: []
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// This menthod generates the preview text for a given transaction
|
||||
static func previewText(
|
||||
variant: Variant,
|
||||
body: String?,
|
||||
authorDisplayName: String = "",
|
||||
attachments: [Attachment],
|
||||
customAttachmentDescription: String? = nil,
|
||||
isOpenGroupInvitation: Bool = false
|
||||
) -> String {
|
||||
switch variant {
|
||||
case .standardIncomingDeleted: return ""
|
||||
|
||||
case .standardIncoming, .standardOutgoing:
|
||||
var bodyDescription: String?
|
||||
let attachmentDescription: String? = (customAttachmentDescription ?? attachments
|
||||
.first(where: { $0.contentType != OWSMimeTypeOversizeTextMessage })?
|
||||
.description
|
||||
)
|
||||
|
||||
if let body: String = body, !body.isEmpty {
|
||||
bodyDescription = body
|
||||
}
|
||||
else if
|
||||
let textAttachment: Attachment = attachments.first(where: { attachment in
|
||||
attachment.state == .downloaded &&
|
||||
attachment.contentType == OWSMimeTypeOversizeTextMessage
|
||||
}),
|
||||
let filePath: String = textAttachment.originalFilePath,
|
||||
let data: Data = try? Data(contentsOf: URL(fileURLWithPath: filePath)),
|
||||
let dataString: String = String(data: data, encoding: .utf8)
|
||||
{
|
||||
bodyDescription = dataString.filterForDisplay
|
||||
}
|
||||
|
||||
if
|
||||
let attachmentDescription: String = attachmentDescription,
|
||||
|
@ -634,7 +686,7 @@ public extension Interaction {
|
|||
return attachmentDescription
|
||||
}
|
||||
|
||||
if let linkPreview: LinkPreview = try? linkPreview.fetchOne(db), linkPreview.variant == .openGroupInvitation {
|
||||
if isOpenGroupInvitation {
|
||||
return "😎 Open group invitation"
|
||||
}
|
||||
|
||||
|
@ -642,19 +694,11 @@ public extension Interaction {
|
|||
return ""
|
||||
|
||||
case .infoMediaSavedNotification:
|
||||
// Note: This should only occur in 'contact' threads so the `threadId`
|
||||
// is the contact id
|
||||
let displayName: String = Profile.displayName(id: threadId)
|
||||
|
||||
// TODO: Use referencedAttachmentTimestamp to tell the user * which * media was saved
|
||||
return String(format: "media_saved".localized(), displayName)
|
||||
return String(format: "media_saved".localized(), authorDisplayName)
|
||||
|
||||
case .infoScreenshotNotification:
|
||||
// Note: This should only occur in 'contact' threads so the `threadId`
|
||||
// is the contact id
|
||||
let displayName: String = Profile.displayName(id: threadId)
|
||||
|
||||
return String(format: "screenshot_taken".localized(), displayName)
|
||||
return String(format: "screenshot_taken".localized(), authorDisplayName)
|
||||
|
||||
case .infoClosedGroupCreated: return "GROUP_CREATED".localized()
|
||||
case .infoClosedGroupCurrentUserLeft: return "GROUP_YOU_LEFT".localized()
|
||||
|
@ -662,8 +706,15 @@ public extension Interaction {
|
|||
case .infoMessageRequestAccepted: return (body ?? "MESSAGE_REQUESTS_ACCEPTED".localized())
|
||||
|
||||
case .infoDisappearingMessagesUpdate:
|
||||
// TODO: We should do better here
|
||||
return (body ?? "")
|
||||
guard
|
||||
let infoMessageData: Data = (body ?? "").data(using: .utf8),
|
||||
let messageInfo: DisappearingMessagesConfiguration.MessageInfo = try? JSONDecoder().decode(
|
||||
DisappearingMessagesConfiguration.MessageInfo.self,
|
||||
from: infoMessageData
|
||||
)
|
||||
else { return (body ?? "") }
|
||||
|
||||
return messageInfo.previewText
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -671,9 +722,17 @@ public extension Interaction {
|
|||
let states: [RecipientState.State] = try RecipientState.State
|
||||
.fetchAll(
|
||||
db,
|
||||
recipientStates
|
||||
.select(RecipientState.Columns.state)
|
||||
recipientStates.select(.state)
|
||||
)
|
||||
|
||||
return Interaction.state(for: states)
|
||||
}
|
||||
|
||||
static func state(for states: [RecipientState.State]) -> RecipientState.State {
|
||||
// If there are no states then assume this is a new interaction which hasn't been
|
||||
// saved yet so has no states
|
||||
guard !states.isEmpty else { return .sending }
|
||||
|
||||
var hasFailed: Bool = false
|
||||
|
||||
for state in states {
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
) {
|
||||
|
|
|
@ -5,8 +5,11 @@ import GRDB
|
|||
import SignalCoreKit
|
||||
import SessionUtilitiesKit
|
||||
|
||||
public struct Profile: Codable, Identifiable, FetchableRecord, PersistableRecord, TableRecord, ColumnExpressible, CustomStringConvertible {
|
||||
public struct Profile: Codable, Identifiable, Equatable, FetchableRecord, PersistableRecord, TableRecord, ColumnExpressible, CustomStringConvertible {
|
||||
public static var databaseTableName: String { "profile" }
|
||||
internal static let interactionForeignKey = ForeignKey([Columns.id], to: [Interaction.Columns.authorId])
|
||||
internal static let contactForeignKey = ForeignKey([Columns.id], to: [Contact.Columns.id])
|
||||
public static let groupMembers = hasMany(GroupMember.self, using: GroupMember.profileForeignKey)
|
||||
|
||||
public typealias Columns = CodingKeys
|
||||
public enum CodingKeys: String, CodingKey, ColumnExpression {
|
||||
|
@ -38,6 +41,24 @@ public struct Profile: Codable, Identifiable, FetchableRecord, PersistableRecord
|
|||
/// The key with which the profile is encrypted.
|
||||
public let profileEncryptionKey: OWSAES256Key?
|
||||
|
||||
// MARK: - Initialization
|
||||
|
||||
public init(
|
||||
id: String,
|
||||
name: String,
|
||||
nickname: String? = nil,
|
||||
profilePictureUrl: String? = nil,
|
||||
profilePictureFileName: String? = nil,
|
||||
profileEncryptionKey: OWSAES256Key? = nil
|
||||
) {
|
||||
self.id = id
|
||||
self.name = name
|
||||
self.nickname = nickname
|
||||
self.profilePictureUrl = profilePictureUrl
|
||||
self.profilePictureFileName = profilePictureFileName
|
||||
self.profileEncryptionKey = profileEncryptionKey
|
||||
}
|
||||
|
||||
// MARK: - Description
|
||||
|
||||
public var description: String {
|
||||
|
@ -177,7 +198,7 @@ public extension Profile {
|
|||
}
|
||||
}
|
||||
|
||||
// MARK: - Convenience
|
||||
// MARK: - Mutation
|
||||
|
||||
public extension Profile {
|
||||
func with(
|
||||
|
@ -196,29 +217,6 @@ public extension Profile {
|
|||
profileEncryptionKey: (profileEncryptionKey ?? self.profileEncryptionKey)
|
||||
)
|
||||
}
|
||||
|
||||
// MARK: - Context
|
||||
|
||||
@objc enum Context: Int {
|
||||
case regular
|
||||
case openGroup
|
||||
}
|
||||
|
||||
/// The name to display in the UI. For local use only.
|
||||
func displayName(for context: Context = .regular) -> String {
|
||||
if let nickname: String = nickname { return nickname }
|
||||
|
||||
switch context {
|
||||
case .regular: return name
|
||||
|
||||
case .openGroup:
|
||||
// In open groups, where it's more likely that multiple users have the same name, we display a bit of the Session ID after
|
||||
// a user's display name for added context.
|
||||
let endIndex = id.endIndex
|
||||
let cutoffIndex = id.index(endIndex, offsetBy: -8)
|
||||
return "\(name) (...\(id[cutoffIndex..<endIndex]))"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - GRDB Interactions
|
||||
|
@ -313,6 +311,65 @@ public extension Profile {
|
|||
}
|
||||
}
|
||||
|
||||
// MARK: - Convenience
|
||||
|
||||
public extension Profile {
|
||||
// MARK: - Context
|
||||
|
||||
@objc enum Context: Int {
|
||||
case regular
|
||||
case openGroup
|
||||
}
|
||||
|
||||
// MARK: - Truncation
|
||||
|
||||
enum Truncation {
|
||||
case start
|
||||
case middle
|
||||
case end
|
||||
}
|
||||
|
||||
/// A standardised mechanism for truncating a user id for a given thread
|
||||
static func truncated(id: String, thread: SessionThread) -> String {
|
||||
switch thread.variant {
|
||||
case .openGroup: return truncated(id: id, truncating: .start)
|
||||
default: return truncated(id: id, truncating: .middle)
|
||||
}
|
||||
}
|
||||
|
||||
/// A standardised mechanism for truncating a user id
|
||||
static func truncated(id: String, truncating: Truncation = .start) -> String {
|
||||
guard id.count > 8 else { return id }
|
||||
|
||||
switch truncating {
|
||||
case .start: return "...\(id.suffix(8))"
|
||||
case .middle: return "\(id.prefix(4))...\(id.suffix(4))"
|
||||
case .end: return "\(id.prefix(8))..."
|
||||
}
|
||||
}
|
||||
|
||||
/// The name to display in the UI for a given thread variant
|
||||
func displayName(for threadVariant: SessionThread.Variant) -> String {
|
||||
return displayName(
|
||||
for: (threadVariant == .openGroup ? .openGroup : .regular)
|
||||
)
|
||||
}
|
||||
|
||||
/// The name to display in the UI
|
||||
func displayName(for context: Context = .regular) -> String {
|
||||
if let nickname: String = nickname { return nickname }
|
||||
|
||||
switch context {
|
||||
case .regular: return name
|
||||
|
||||
case .openGroup:
|
||||
// In open groups, where it's more likely that multiple users have the same name,
|
||||
// we display a bit of the Session ID after a user's display name for added context
|
||||
return "\(name) (\(Profile.truncated(id: id, truncating: .start)))"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Objective-C Support
|
||||
@objc(SMKProfile)
|
||||
public class SMKProfile: NSObject {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 }
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 }
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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) }
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -12,7 +12,7 @@ extension ConfigurationMessage {
|
|||
let displayName: String = profile.name
|
||||
let profilePictureUrl: String? = profile.profilePictureUrl
|
||||
let profileKey: Data? = profile.profileEncryptionKey?.keyData
|
||||
var closedGroups: Set<ClosedGroup> = []
|
||||
var closedGroups: Set<CMClosedGroup> = []
|
||||
var openGroups: Set<String> = []
|
||||
|
||||
Storage.read { transaction in
|
||||
|
@ -66,7 +66,7 @@ extension ConfigurationMessage {
|
|||
return CMContact(
|
||||
publicKey: contact.id,
|
||||
displayName: (profile?.name ?? contact.id),
|
||||
profilePictureURL: profile?.profilePictureUrl,
|
||||
profilePictureUrl: profile?.profilePictureUrl,
|
||||
profileKey: profile?.profileEncryptionKey?.keyData,
|
||||
hasIsApproved: true,
|
||||
isApproved: contact.isApproved,
|
||||
|
@ -80,7 +80,7 @@ extension ConfigurationMessage {
|
|||
|
||||
return ConfigurationMessage(
|
||||
displayName: displayName,
|
||||
profilePictureURL: profilePictureUrl,
|
||||
profilePictureUrl: profilePictureUrl,
|
||||
profileKey: profileKey,
|
||||
closedGroups: closedGroups,
|
||||
openGroups: openGroups,
|
||||
|
|
|
@ -1,63 +1,49 @@
|
|||
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
|
||||
|
||||
import Foundation
|
||||
import Sodium
|
||||
import GRDB
|
||||
import Curve25519Kit
|
||||
import SessionUtilitiesKit
|
||||
|
||||
@objc(SNConfigurationMessage)
|
||||
public final class ConfigurationMessage : ControlMessage {
|
||||
public final class ConfigurationMessage: ControlMessage {
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case closedGroups
|
||||
case openGroups
|
||||
case displayName
|
||||
case profilePictureURL
|
||||
case profilePictureUrl
|
||||
case profileKey
|
||||
case contacts
|
||||
}
|
||||
|
||||
public var closedGroups: Set<ClosedGroup> = []
|
||||
public var closedGroups: Set<CMClosedGroup> = []
|
||||
public var openGroups: Set<String> = []
|
||||
public var displayName: String?
|
||||
public var profilePictureURL: String?
|
||||
public var profilePictureUrl: String?
|
||||
public var profileKey: Data?
|
||||
public var contacts: Set<CMContact> = []
|
||||
|
||||
public override var isSelfSendValid: Bool { true }
|
||||
|
||||
// MARK: Initialization
|
||||
public override init() { super.init() }
|
||||
// MARK: - Initialization
|
||||
|
||||
public init(displayName: String?, profilePictureURL: String?, profileKey: Data?, closedGroups: Set<ClosedGroup>, openGroups: Set<String>, contacts: Set<CMContact>) {
|
||||
public init(
|
||||
displayName: String?,
|
||||
profilePictureUrl: String?,
|
||||
profileKey: Data?,
|
||||
closedGroups: Set<CMClosedGroup>,
|
||||
openGroups: Set<String>,
|
||||
contacts: Set<CMContact>
|
||||
) {
|
||||
super.init()
|
||||
|
||||
self.displayName = displayName
|
||||
self.profilePictureURL = profilePictureURL
|
||||
self.profilePictureUrl = profilePictureUrl
|
||||
self.profileKey = profileKey
|
||||
self.closedGroups = closedGroups
|
||||
self.openGroups = openGroups
|
||||
self.contacts = contacts
|
||||
}
|
||||
|
||||
// MARK: Coding
|
||||
public required init?(coder: NSCoder) {
|
||||
super.init(coder: coder)
|
||||
if let closedGroups = coder.decodeObject(forKey: "closedGroups") as! Set<ClosedGroup>? { self.closedGroups = closedGroups }
|
||||
if let openGroups = coder.decodeObject(forKey: "openGroups") as! Set<String>? { self.openGroups = openGroups }
|
||||
if let displayName = coder.decodeObject(forKey: "displayName") as! String? { self.displayName = displayName }
|
||||
if let profilePictureURL = coder.decodeObject(forKey: "profilePictureURL") as! String? { self.profilePictureURL = profilePictureURL }
|
||||
if let profileKey = coder.decodeObject(forKey: "profileKey") as! Data? { self.profileKey = profileKey }
|
||||
if let contacts = coder.decodeObject(forKey: "contacts") as! Set<CMContact>? { self.contacts = contacts }
|
||||
}
|
||||
|
||||
public override func encode(with coder: NSCoder) {
|
||||
super.encode(with: coder)
|
||||
coder.encode(closedGroups, forKey: "closedGroups")
|
||||
coder.encode(openGroups, forKey: "openGroups")
|
||||
coder.encode(displayName, forKey: "displayName")
|
||||
coder.encode(profilePictureURL, forKey: "profilePictureURL")
|
||||
coder.encode(profileKey, forKey: "profileKey")
|
||||
coder.encode(contacts, forKey: "contacts")
|
||||
}
|
||||
|
||||
// MARK: - Codable
|
||||
|
||||
|
@ -66,10 +52,10 @@ public final class ConfigurationMessage : ControlMessage {
|
|||
|
||||
let container: KeyedDecodingContainer<CodingKeys> = try decoder.container(keyedBy: CodingKeys.self)
|
||||
|
||||
closedGroups = ((try? container.decode(Set<ClosedGroup>.self, forKey: .closedGroups)) ?? [])
|
||||
closedGroups = ((try? container.decode(Set<CMClosedGroup>.self, forKey: .closedGroups)) ?? [])
|
||||
openGroups = ((try? container.decode(Set<String>.self, forKey: .openGroups)) ?? [])
|
||||
displayName = try? container.decode(String.self, forKey: .displayName)
|
||||
profilePictureURL = try? container.decode(String.self, forKey: .profilePictureURL)
|
||||
profilePictureUrl = try? container.decode(String.self, forKey: .profilePictureUrl)
|
||||
profileKey = try? container.decode(Data.self, forKey: .profileKey)
|
||||
contacts = ((try? container.decode(Set<CMContact>.self, forKey: .contacts)) ?? [])
|
||||
}
|
||||
|
@ -82,28 +68,36 @@ public final class ConfigurationMessage : ControlMessage {
|
|||
try container.encodeIfPresent(closedGroups, forKey: .closedGroups)
|
||||
try container.encodeIfPresent(openGroups, forKey: .openGroups)
|
||||
try container.encodeIfPresent(displayName, forKey: .displayName)
|
||||
try container.encodeIfPresent(profilePictureURL, forKey: .profilePictureURL)
|
||||
try container.encodeIfPresent(profilePictureUrl, forKey: .profilePictureUrl)
|
||||
try container.encodeIfPresent(profileKey, forKey: .profileKey)
|
||||
try container.encodeIfPresent(contacts, forKey: .contacts)
|
||||
}
|
||||
|
||||
// MARK: Proto Conversion
|
||||
// MARK: - Proto Conversion
|
||||
|
||||
public override class func fromProto(_ proto: SNProtoContent, sender: String) -> ConfigurationMessage? {
|
||||
guard let configurationProto = proto.configurationMessage else { return nil }
|
||||
let displayName = configurationProto.displayName
|
||||
let profilePictureURL = configurationProto.profilePicture
|
||||
let profilePictureUrl = configurationProto.profilePicture
|
||||
let profileKey = configurationProto.profileKey
|
||||
let closedGroups = Set(configurationProto.closedGroups.compactMap { ClosedGroup.fromProto($0) })
|
||||
let closedGroups = Set(configurationProto.closedGroups.compactMap { CMClosedGroup.fromProto($0) })
|
||||
let openGroups = Set(configurationProto.openGroups)
|
||||
let contacts = Set(configurationProto.contacts.compactMap { CMContact.fromProto($0) })
|
||||
return ConfigurationMessage(displayName: displayName, profilePictureURL: profilePictureURL, profileKey: profileKey,
|
||||
closedGroups: closedGroups, openGroups: openGroups, contacts: contacts)
|
||||
|
||||
return ConfigurationMessage(
|
||||
displayName: displayName,
|
||||
profilePictureUrl: profilePictureUrl,
|
||||
profileKey: profileKey,
|
||||
closedGroups: closedGroups,
|
||||
openGroups: openGroups,
|
||||
contacts: contacts
|
||||
)
|
||||
}
|
||||
|
||||
public override func toProto(_ db: Database) -> SNProtoContent? {
|
||||
let configurationProto = SNProtoConfigurationMessage.builder()
|
||||
if let displayName = displayName { configurationProto.setDisplayName(displayName) }
|
||||
if let profilePictureURL = profilePictureURL { configurationProto.setProfilePicture(profilePictureURL) }
|
||||
if let profilePictureUrl = profilePictureUrl { configurationProto.setProfilePicture(profilePictureUrl) }
|
||||
if let profileKey = profileKey { configurationProto.setProfileKey(profileKey) }
|
||||
configurationProto.setClosedGroups(closedGroups.compactMap { $0.toProto() })
|
||||
configurationProto.setOpenGroups([String](openGroups))
|
||||
|
@ -118,14 +112,15 @@ public final class ConfigurationMessage : ControlMessage {
|
|||
}
|
||||
}
|
||||
|
||||
// MARK: Description
|
||||
public override var description: String {
|
||||
// MARK: - Description
|
||||
|
||||
public var description: String {
|
||||
"""
|
||||
ConfigurationMessage(
|
||||
closedGroups: \([ClosedGroup](closedGroups).prettifiedDescription),
|
||||
closedGroups: \([CMClosedGroup](closedGroups).prettifiedDescription),
|
||||
openGroups: \([String](openGroups).prettifiedDescription),
|
||||
displayName: \(displayName ?? "null"),
|
||||
profilePictureURL: \(profilePictureURL ?? "null"),
|
||||
profilePictureUrl: \(profilePictureUrl ?? "null"),
|
||||
profileKey: \(profileKey?.toHexString() ?? "null"),
|
||||
contacts: \([CMContact](contacts).prettifiedDescription)
|
||||
)
|
||||
|
@ -133,11 +128,10 @@ public final class ConfigurationMessage : ControlMessage {
|
|||
}
|
||||
}
|
||||
|
||||
// MARK: Closed Group
|
||||
extension ConfigurationMessage {
|
||||
// MARK: - Closed Group
|
||||
|
||||
@objc(SNClosedGroup)
|
||||
public final class ClosedGroup: NSObject, Codable, NSCoding { // NSObject/NSCoding conformance is needed for YapDatabase compatibility
|
||||
extension ConfigurationMessage {
|
||||
public struct CMClosedGroup: Codable, Hashable, CustomStringConvertible {
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case publicKey
|
||||
case name
|
||||
|
@ -150,57 +144,43 @@ extension ConfigurationMessage {
|
|||
|
||||
public let publicKey: String
|
||||
public let name: String
|
||||
public let encryptionKeyPair: ECKeyPair
|
||||
public let encryptionKeyPublicKey: Data
|
||||
public let encryptionKeySecretKey: Data
|
||||
public let members: Set<String>
|
||||
public let admins: Set<String>
|
||||
public let expirationTimer: UInt32
|
||||
|
||||
public var isValid: Bool { !members.isEmpty && !admins.isEmpty }
|
||||
|
||||
public init(publicKey: String, name: String, encryptionKeyPair: ECKeyPair, members: Set<String>, admins: Set<String>, expirationTimer: UInt32) {
|
||||
self.publicKey = publicKey
|
||||
self.name = name
|
||||
self.encryptionKeyPair = encryptionKeyPair
|
||||
self.members = members
|
||||
self.admins = admins
|
||||
self.expirationTimer = expirationTimer
|
||||
}
|
||||
|
||||
public required init?(coder: NSCoder) {
|
||||
guard let publicKey = coder.decodeObject(forKey: "publicKey") as! String?,
|
||||
let name = coder.decodeObject(forKey: "name") as! String?,
|
||||
let encryptionKeyPair = coder.decodeObject(forKey: "encryptionKeyPair") as! ECKeyPair?,
|
||||
let members = coder.decodeObject(forKey: "members") as! Set<String>?,
|
||||
let admins = coder.decodeObject(forKey: "admins") as! Set<String>? else { return nil }
|
||||
let expirationTimer = coder.decodeObject(forKey: "expirationTimer") as? UInt32 ?? 0
|
||||
self.publicKey = publicKey
|
||||
self.name = name
|
||||
self.encryptionKeyPair = encryptionKeyPair
|
||||
self.members = members
|
||||
self.admins = admins
|
||||
self.expirationTimer = expirationTimer
|
||||
}
|
||||
|
||||
public func encode(with coder: NSCoder) {
|
||||
coder.encode(publicKey, forKey: "publicKey")
|
||||
coder.encode(name, forKey: "name")
|
||||
coder.encode(encryptionKeyPair, forKey: "encryptionKeyPair")
|
||||
coder.encode(members, forKey: "members")
|
||||
coder.encode(admins, forKey: "admins")
|
||||
coder.encode(expirationTimer, forKey: "expirationTimer")
|
||||
}
|
||||
|
||||
// MARK: - Initialization
|
||||
|
||||
public init(
|
||||
publicKey: String,
|
||||
name: String,
|
||||
encryptionKeyPublicKey: Data,
|
||||
encryptionKeySecretKey: Data,
|
||||
members: Set<String>,
|
||||
admins: Set<String>,
|
||||
expirationTimer: UInt32
|
||||
) {
|
||||
self.publicKey = publicKey
|
||||
self.name = name
|
||||
self.encryptionKeyPublicKey = encryptionKeyPublicKey
|
||||
self.encryptionKeySecretKey = encryptionKeySecretKey
|
||||
self.members = members
|
||||
self.admins = admins
|
||||
self.expirationTimer = expirationTimer
|
||||
}
|
||||
|
||||
// MARK: - Codable
|
||||
|
||||
public required init(from decoder: Decoder) throws {
|
||||
public init(from decoder: Decoder) throws {
|
||||
let container: KeyedDecodingContainer<CodingKeys> = try decoder.container(keyedBy: CodingKeys.self)
|
||||
|
||||
publicKey = try container.decode(String.self, forKey: .publicKey)
|
||||
name = try container.decode(String.self, forKey: .name)
|
||||
encryptionKeyPair = try ECKeyPair(
|
||||
publicKeyData: try container.decode(Data.self, forKey: .encryptionKeyPublicKey),
|
||||
privateKeyData: try container.decode(Data.self, forKey: .encryptionKeySecretKey)
|
||||
)
|
||||
encryptionKeyPublicKey = try container.decode(Data.self, forKey: .encryptionKeyPublicKey)
|
||||
encryptionKeySecretKey = try container.decode(Data.self, forKey: .encryptionKeySecretKey)
|
||||
members = try container.decode(Set<String>.self, forKey: .members)
|
||||
admins = try container.decode(Set<String>.self, forKey: .admins)
|
||||
expirationTimer = try container.decode(UInt32.self, forKey: .expirationTimer)
|
||||
|
@ -211,28 +191,33 @@ extension ConfigurationMessage {
|
|||
|
||||
try container.encode(publicKey, forKey: .publicKey)
|
||||
try container.encode(name, forKey: .name)
|
||||
try container.encode(encryptionKeyPair.publicKey, forKey: .encryptionKeyPublicKey)
|
||||
try container.encode(encryptionKeyPair.privateKey, forKey: .encryptionKeySecretKey)
|
||||
try container.encode(encryptionKeyPublicKey, forKey: .encryptionKeyPublicKey)
|
||||
try container.encode(encryptionKeySecretKey, forKey: .encryptionKeySecretKey)
|
||||
try container.encode(members, forKey: .members)
|
||||
try container.encode(admins, forKey: .admins)
|
||||
try container.encode(expirationTimer, forKey: .expirationTimer)
|
||||
}
|
||||
|
||||
public static func fromProto(_ proto: SNProtoConfigurationMessageClosedGroup) -> ClosedGroup? {
|
||||
guard let publicKey = proto.publicKey?.toHexString(),
|
||||
public static func fromProto(_ proto: SNProtoConfigurationMessageClosedGroup) -> CMClosedGroup? {
|
||||
guard
|
||||
let publicKey = proto.publicKey?.toHexString(),
|
||||
let name = proto.name,
|
||||
let encryptionKeyPairAsProto = proto.encryptionKeyPair else { return nil }
|
||||
let encryptionKeyPair: ECKeyPair
|
||||
do {
|
||||
encryptionKeyPair = try ECKeyPair(publicKeyData: encryptionKeyPairAsProto.publicKey, privateKeyData: encryptionKeyPairAsProto.privateKey)
|
||||
} catch {
|
||||
SNLog("Couldn't construct closed group from proto: \(self).")
|
||||
return nil
|
||||
}
|
||||
let encryptionKeyPairAsProto = proto.encryptionKeyPair
|
||||
else { return nil }
|
||||
|
||||
let members = Set(proto.members.map { $0.toHexString() })
|
||||
let admins = Set(proto.admins.map { $0.toHexString() })
|
||||
let expirationTimer = proto.expirationTimer
|
||||
let result = ClosedGroup(publicKey: publicKey, name: name, encryptionKeyPair: encryptionKeyPair, members: members, admins: admins, expirationTimer: expirationTimer)
|
||||
let result = CMClosedGroup(
|
||||
publicKey: publicKey,
|
||||
name: name,
|
||||
encryptionKeyPublicKey: encryptionKeyPairAsProto.publicKey,
|
||||
encryptionKeySecretKey: encryptionKeyPairAsProto.privateKey,
|
||||
members: members,
|
||||
admins: admins,
|
||||
expirationTimer: expirationTimer
|
||||
)
|
||||
|
||||
guard result.isValid else { return nil }
|
||||
return result
|
||||
}
|
||||
|
@ -243,7 +228,10 @@ extension ConfigurationMessage {
|
|||
result.setPublicKey(Data(hex: publicKey))
|
||||
result.setName(name)
|
||||
do {
|
||||
let encryptionKeyPairAsProto = try SNProtoKeyPair.builder(publicKey: encryptionKeyPair.publicKey, privateKey: encryptionKeyPair.privateKey).build()
|
||||
let encryptionKeyPairAsProto = try SNProtoKeyPair.builder(
|
||||
publicKey: encryptionKeyPublicKey,
|
||||
privateKey: encryptionKeySecretKey
|
||||
).build()
|
||||
result.setEncryptionKeyPair(encryptionKeyPairAsProto)
|
||||
} catch {
|
||||
SNLog("Couldn't construct closed group proto from: \(self).")
|
||||
|
@ -260,19 +248,18 @@ extension ConfigurationMessage {
|
|||
}
|
||||
}
|
||||
|
||||
public override var description: String { name }
|
||||
public var description: String { name }
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Contact
|
||||
extension ConfigurationMessage {
|
||||
// MARK: - Contact
|
||||
|
||||
@objc(SNConfigurationMessageContact)
|
||||
public final class CMContact: NSObject, Codable, NSCoding { // NSObject/NSCoding conformance is needed for YapDatabase compatibility
|
||||
extension ConfigurationMessage {
|
||||
public struct CMContact: Codable, Hashable, CustomStringConvertible {
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case publicKey
|
||||
case displayName
|
||||
case profilePictureURL
|
||||
case profilePictureUrl
|
||||
case profileKey
|
||||
|
||||
case hasIsApproved
|
||||
|
@ -285,7 +272,7 @@ extension ConfigurationMessage {
|
|||
|
||||
public var publicKey: String?
|
||||
public var displayName: String?
|
||||
public var profilePictureURL: String?
|
||||
public var profilePictureUrl: String?
|
||||
public var profileKey: Data?
|
||||
|
||||
public var hasIsApproved: Bool
|
||||
|
@ -298,9 +285,9 @@ extension ConfigurationMessage {
|
|||
public var isValid: Bool { publicKey != nil && displayName != nil }
|
||||
|
||||
public init(
|
||||
publicKey: String,
|
||||
displayName: String,
|
||||
profilePictureURL: String?,
|
||||
publicKey: String?,
|
||||
displayName: String?,
|
||||
profilePictureUrl: String?,
|
||||
profileKey: Data?,
|
||||
hasIsApproved: Bool,
|
||||
isApproved: Bool,
|
||||
|
@ -311,7 +298,7 @@ extension ConfigurationMessage {
|
|||
) {
|
||||
self.publicKey = publicKey
|
||||
self.displayName = displayName
|
||||
self.profilePictureURL = profilePictureURL
|
||||
self.profilePictureUrl = profilePictureUrl
|
||||
self.profileKey = profileKey
|
||||
self.hasIsApproved = hasIsApproved
|
||||
self.isApproved = isApproved
|
||||
|
@ -320,43 +307,15 @@ extension ConfigurationMessage {
|
|||
self.hasDidApproveMe = hasDidApproveMe
|
||||
self.didApproveMe = didApproveMe
|
||||
}
|
||||
|
||||
public required init?(coder: NSCoder) {
|
||||
guard let publicKey = coder.decodeObject(forKey: "publicKey") as! String?,
|
||||
let displayName = coder.decodeObject(forKey: "displayName") as! String? else { return nil }
|
||||
self.publicKey = publicKey
|
||||
self.displayName = displayName
|
||||
self.profilePictureURL = coder.decodeObject(forKey: "profilePictureURL") as! String?
|
||||
self.profileKey = coder.decodeObject(forKey: "profileKey") as! Data?
|
||||
self.hasIsApproved = (coder.decodeObject(forKey: "hasIsApproved") as? Bool ?? false)
|
||||
self.isApproved = (coder.decodeObject(forKey: "isApproved") as? Bool ?? false)
|
||||
self.hasIsBlocked = (coder.decodeObject(forKey: "hasIsBlocked") as? Bool ?? false)
|
||||
self.isBlocked = (coder.decodeObject(forKey: "isBlocked") as? Bool ?? false)
|
||||
self.hasDidApproveMe = (coder.decodeObject(forKey: "hasDidApproveMe") as? Bool ?? false)
|
||||
self.didApproveMe = (coder.decodeObject(forKey: "didApproveMe") as? Bool ?? false)
|
||||
}
|
||||
|
||||
public func encode(with coder: NSCoder) {
|
||||
coder.encode(publicKey, forKey: "publicKey")
|
||||
coder.encode(displayName, forKey: "displayName")
|
||||
coder.encode(profilePictureURL, forKey: "profilePictureURL")
|
||||
coder.encode(profileKey, forKey: "profileKey")
|
||||
coder.encode(hasIsApproved, forKey: "hasIsApproved")
|
||||
coder.encode(isApproved, forKey: "isApproved")
|
||||
coder.encode(hasIsBlocked, forKey: "hasIsBlocked")
|
||||
coder.encode(isBlocked, forKey: "isBlocked")
|
||||
coder.encode(hasDidApproveMe, forKey: "hasDidApproveMe")
|
||||
coder.encode(didApproveMe, forKey: "didApproveMe")
|
||||
}
|
||||
|
||||
// MARK: - Codable
|
||||
|
||||
public required init(from decoder: Decoder) throws {
|
||||
public init(from decoder: Decoder) throws {
|
||||
let container: KeyedDecodingContainer<CodingKeys> = try decoder.container(keyedBy: CodingKeys.self)
|
||||
|
||||
publicKey = try? container.decode(String.self, forKey: .publicKey)
|
||||
displayName = try? container.decode(String.self, forKey: .displayName)
|
||||
profilePictureURL = try? container.decode(String.self, forKey: .profilePictureURL)
|
||||
profilePictureUrl = try? container.decode(String.self, forKey: .profilePictureUrl)
|
||||
profileKey = try? container.decode(Data.self, forKey: .profileKey)
|
||||
|
||||
hasIsApproved = try container.decode(Bool.self, forKey: .hasIsApproved)
|
||||
|
@ -371,7 +330,7 @@ extension ConfigurationMessage {
|
|||
let result: CMContact = CMContact(
|
||||
publicKey: proto.publicKey.toHexString(),
|
||||
displayName: proto.name,
|
||||
profilePictureURL: proto.profilePicture,
|
||||
profilePictureUrl: proto.profilePicture,
|
||||
profileKey: proto.profileKey,
|
||||
hasIsApproved: proto.hasIsApproved,
|
||||
isApproved: proto.isApproved,
|
||||
|
@ -389,7 +348,7 @@ extension ConfigurationMessage {
|
|||
guard isValid else { return nil }
|
||||
guard let publicKey = publicKey, let displayName = displayName else { return nil }
|
||||
let result = SNProtoConfigurationMessageContact.builder(publicKey: Data(hex: publicKey), name: displayName)
|
||||
if let profilePictureURL = profilePictureURL { result.setProfilePicture(profilePictureURL) }
|
||||
if let profilePictureUrl = profilePictureUrl { result.setProfilePicture(profilePictureUrl) }
|
||||
if let profileKey = profileKey { result.setProfileKey(profileKey) }
|
||||
|
||||
if hasIsApproved { result.setIsApproved(isApproved) }
|
||||
|
@ -404,6 +363,6 @@ extension ConfigurationMessage {
|
|||
}
|
||||
}
|
||||
|
||||
public override var description: String { displayName ?? "" }
|
||||
public var description: String { displayName ?? "" }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,5 +2,4 @@
|
|||
|
||||
import Foundation
|
||||
|
||||
@objc(SNControlMessage)
|
||||
public class ControlMessage: Message { }
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"),
|
||||
|
|
|
@ -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")
|
||||
)
|
||||
"""
|
||||
}
|
||||
|
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,12 +4,11 @@ import Foundation
|
|||
import GRDB
|
||||
import SessionUtilitiesKit
|
||||
|
||||
@objc(SNVisibleMessage)
|
||||
public final class VisibleMessage: Message {
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case syncTarget
|
||||
case text = "body"
|
||||
case attachmentIDs = "attachments"
|
||||
case attachmentIds = "attachments"
|
||||
case quote
|
||||
case linkPreview
|
||||
case profile
|
||||
|
@ -20,65 +19,68 @@ public final class VisibleMessage: Message {
|
|||
///
|
||||
/// - Note: `nil` if this isn't a sync message.
|
||||
public var syncTarget: String?
|
||||
@objc public var text: String?
|
||||
@objc public var attachmentIDs: [String] = []
|
||||
@objc public var quote: Quote?
|
||||
@objc public var linkPreview: LinkPreview?
|
||||
@objc public var contact: Legacy.Contact?
|
||||
@objc public var profile: Profile?
|
||||
@objc public var openGroupInvitation: OpenGroupInvitation?
|
||||
public let text: String?
|
||||
public var attachmentIds: [String]
|
||||
public let quote: Quote?
|
||||
public let linkPreview: LinkPreview?
|
||||
public var profile: Profile?
|
||||
public let openGroupInvitation: OpenGroupInvitation?
|
||||
|
||||
public override var isSelfSendValid: Bool { true }
|
||||
|
||||
// MARK: Initialization
|
||||
public override init() { super.init() }
|
||||
|
||||
// MARK: Validation
|
||||
// MARK: - Validation
|
||||
|
||||
public override var isValid: Bool {
|
||||
guard super.isValid else { return false }
|
||||
if !attachmentIDs.isEmpty { return true }
|
||||
if !attachmentIds.isEmpty { return true }
|
||||
if openGroupInvitation != nil { return true }
|
||||
if let text = text?.trimmingCharacters(in: .whitespacesAndNewlines), !text.isEmpty { return true }
|
||||
return false
|
||||
}
|
||||
|
||||
// MARK: Coding
|
||||
public required init?(coder: NSCoder) {
|
||||
super.init(coder: coder)
|
||||
if let syncTarget = coder.decodeObject(forKey: "syncTarget") as! String? { self.syncTarget = syncTarget }
|
||||
if let text = coder.decodeObject(forKey: "body") as! String? { self.text = text }
|
||||
if let attachmentIDs = coder.decodeObject(forKey: "attachments") as! [String]? { self.attachmentIDs = attachmentIDs }
|
||||
if let quote = coder.decodeObject(forKey: "quote") as! Quote? { self.quote = quote }
|
||||
if let linkPreview = coder.decodeObject(forKey: "linkPreview") as! LinkPreview? { self.linkPreview = linkPreview }
|
||||
if let profile = coder.decodeObject(forKey: "profile") as! Profile? { self.profile = profile }
|
||||
if let openGroupInvitation = coder.decodeObject(forKey: "openGroupInvitation") as! OpenGroupInvitation? { self.openGroupInvitation = openGroupInvitation }
|
||||
}
|
||||
|
||||
public override func encode(with coder: NSCoder) {
|
||||
super.encode(with: coder)
|
||||
coder.encode(syncTarget, forKey: "syncTarget")
|
||||
coder.encode(text, forKey: "body")
|
||||
coder.encode(attachmentIDs, forKey: "attachments")
|
||||
coder.encode(quote, forKey: "quote")
|
||||
coder.encode(linkPreview, forKey: "linkPreview")
|
||||
coder.encode(profile, forKey: "profile")
|
||||
coder.encode(openGroupInvitation, forKey: "openGroupInvitation")
|
||||
// MARK: - Initialization
|
||||
|
||||
public init(
|
||||
sentTimestamp: UInt64? = nil,
|
||||
recipient: String? = nil,
|
||||
groupPublicKey: String? = nil,
|
||||
syncTarget: String? = nil,
|
||||
text: String?,
|
||||
attachmentIds: [String] = [],
|
||||
quote: Quote? = nil,
|
||||
linkPreview: LinkPreview? = nil,
|
||||
profile: Profile? = nil,
|
||||
openGroupInvitation: OpenGroupInvitation? = nil
|
||||
) {
|
||||
self.syncTarget = syncTarget
|
||||
self.text = text
|
||||
self.attachmentIds = attachmentIds
|
||||
self.quote = quote
|
||||
self.linkPreview = linkPreview
|
||||
self.profile = profile
|
||||
self.openGroupInvitation = openGroupInvitation
|
||||
|
||||
super.init(
|
||||
sentTimestamp: sentTimestamp,
|
||||
recipient: recipient,
|
||||
groupPublicKey: groupPublicKey
|
||||
)
|
||||
}
|
||||
|
||||
// MARK: - Codable
|
||||
|
||||
required init(from decoder: Decoder) throws {
|
||||
try super.init(from: decoder)
|
||||
|
||||
let container: KeyedDecodingContainer<CodingKeys> = try decoder.container(keyedBy: CodingKeys.self)
|
||||
|
||||
syncTarget = try? container.decode(String.self, forKey: .syncTarget)
|
||||
text = try? container.decode(String.self, forKey: .text)
|
||||
attachmentIDs = ((try? container.decode([String].self, forKey: .attachmentIDs)) ?? [])
|
||||
attachmentIds = ((try? container.decode([String].self, forKey: .attachmentIds)) ?? [])
|
||||
quote = try? container.decode(Quote.self, forKey: .quote)
|
||||
linkPreview = try? container.decode(LinkPreview.self, forKey: .linkPreview)
|
||||
profile = try? container.decode(Profile.self, forKey: .profile)
|
||||
openGroupInvitation = try? container.decode(OpenGroupInvitation.self, forKey: .openGroupInvitation)
|
||||
|
||||
try super.init(from: decoder)
|
||||
}
|
||||
|
||||
public override func encode(to encoder: Encoder) throws {
|
||||
|
@ -88,32 +90,32 @@ public final class VisibleMessage: Message {
|
|||
|
||||
try container.encodeIfPresent(syncTarget, forKey: .syncTarget)
|
||||
try container.encodeIfPresent(text, forKey: .text)
|
||||
try container.encodeIfPresent(attachmentIDs, forKey: .attachmentIDs)
|
||||
try container.encodeIfPresent(attachmentIds, forKey: .attachmentIds)
|
||||
try container.encodeIfPresent(quote, forKey: .quote)
|
||||
try container.encodeIfPresent(linkPreview, forKey: .linkPreview)
|
||||
try container.encodeIfPresent(profile, forKey: .profile)
|
||||
try container.encodeIfPresent(openGroupInvitation, forKey: .openGroupInvitation)
|
||||
}
|
||||
|
||||
// MARK: Proto Conversion
|
||||
// MARK: - Proto Conversion
|
||||
|
||||
public override class func fromProto(_ proto: SNProtoContent, sender: String) -> VisibleMessage? {
|
||||
guard let dataMessage = proto.dataMessage else { return nil }
|
||||
let result = VisibleMessage()
|
||||
result.text = dataMessage.body
|
||||
// Attachments are handled in MessageReceiver
|
||||
if let quoteProto = dataMessage.quote, let quote = Quote.fromProto(quoteProto) { result.quote = quote }
|
||||
if let linkPreviewProto = dataMessage.preview.first, let linkPreview = LinkPreview.fromProto(linkPreviewProto) { result.linkPreview = linkPreview }
|
||||
// TODO: Contact
|
||||
if let profile = Profile.fromProto(dataMessage) { result.profile = profile }
|
||||
if let openGroupInvitationProto = dataMessage.openGroupInvitation,
|
||||
let openGroupInvitation = OpenGroupInvitation.fromProto(openGroupInvitationProto) { result.openGroupInvitation = openGroupInvitation }
|
||||
result.syncTarget = dataMessage.syncTarget
|
||||
return result
|
||||
|
||||
return VisibleMessage(
|
||||
syncTarget: dataMessage.syncTarget,
|
||||
text: dataMessage.body,
|
||||
attachmentIds: [], // Attachments are handled in MessageReceiver
|
||||
quote: dataMessage.quote.map { Quote.fromProto($0) },
|
||||
linkPreview: dataMessage.preview.first.map { LinkPreview.fromProto($0) },
|
||||
profile: Profile.fromProto(dataMessage),
|
||||
openGroupInvitation: dataMessage.openGroupInvitation.map { OpenGroupInvitation.fromProto($0) }
|
||||
)
|
||||
}
|
||||
|
||||
public override func toProto(_ db: Database) -> SNProtoContent? {
|
||||
let proto = SNProtoContent.builder()
|
||||
var attachmentIDs = self.attachmentIDs
|
||||
var attachmentIds = self.attachmentIds
|
||||
let dataMessage: SNProtoDataMessage.SNProtoDataMessageBuilder
|
||||
|
||||
// Profile
|
||||
|
@ -138,8 +140,8 @@ public final class VisibleMessage: Message {
|
|||
}
|
||||
|
||||
// Link preview
|
||||
if let linkPreviewAttachmentID = linkPreview?.attachmentID, let index = attachmentIDs.firstIndex(of: linkPreviewAttachmentID) {
|
||||
attachmentIDs.remove(at: index)
|
||||
if let linkPreviewAttachmentId = linkPreview?.attachmentId, let index = attachmentIds.firstIndex(of: linkPreviewAttachmentId) {
|
||||
attachmentIds.remove(at: index)
|
||||
}
|
||||
|
||||
if let linkPreview = linkPreview, let linkPreviewProto = linkPreview.toProto(db) {
|
||||
|
@ -148,7 +150,7 @@ public final class VisibleMessage: Message {
|
|||
|
||||
// Attachments
|
||||
|
||||
let attachments: [SessionMessagingKit.Attachment]? = try? SessionMessagingKit.Attachment.fetchAll(db, ids: self.attachmentIDs)
|
||||
let attachments: [SessionMessagingKit.Attachment]? = try? SessionMessagingKit.Attachment.fetchAll(db, ids: self.attachmentIds)
|
||||
|
||||
if !(attachments ?? []).allSatisfy({ $0.state == .uploaded }) {
|
||||
#if DEBUG
|
||||
|
@ -158,8 +160,6 @@ public final class VisibleMessage: Message {
|
|||
let attachmentProtos = (attachments ?? []).compactMap { $0.buildProto() }
|
||||
dataMessage.setAttachments(attachmentProtos)
|
||||
|
||||
// TODO: Contact
|
||||
|
||||
// Open group invitation
|
||||
if let openGroupInvitation = openGroupInvitation, let openGroupInvitationProto = openGroupInvitation.toProto() { dataMessage.setOpenGroupInvitation(openGroupInvitationProto) }
|
||||
|
||||
|
@ -184,15 +184,15 @@ public final class VisibleMessage: Message {
|
|||
}
|
||||
}
|
||||
|
||||
// MARK: Description
|
||||
public override var description: String {
|
||||
// MARK: - Description
|
||||
|
||||
public var description: String {
|
||||
"""
|
||||
VisibleMessage(
|
||||
text: \(text ?? "null"),
|
||||
attachmentIDs: \(attachmentIDs),
|
||||
attachmentIds: \(attachmentIds),
|
||||
quote: \(quote?.description ?? "null"),
|
||||
linkPreview: \(linkPreview?.description ?? "null"),
|
||||
contact: \(contact?.description ?? "null"),
|
||||
profile: \(profile?.description ?? "null")
|
||||
"openGroupInvitation": \(openGroupInvitation?.description ?? "null")
|
||||
)
|
||||
|
|
|
@ -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 }
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
{
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -18,53 +18,48 @@ public enum Legacy {
|
|||
public typealias LegacyOnionRequestAPIPath = [Snode]
|
||||
|
||||
@objc(Snode)
|
||||
public final class Snode: NSObject, NSCoding { // NSObject/NSCoding conformance is needed for YapDatabase compatibility
|
||||
public final class Snode: NSObject, NSCoding {
|
||||
public let address: String
|
||||
public let port: UInt16
|
||||
public let publicKeySet: KeySet
|
||||
|
||||
public var ip: String {
|
||||
guard let range = address.range(of: "https://"), range.lowerBound == address.startIndex else { return address }
|
||||
return String(address[range.upperBound..<address.endIndex])
|
||||
}
|
||||
|
||||
// MARK: Nested Types
|
||||
// MARK: - Nested Types
|
||||
|
||||
public struct KeySet {
|
||||
public let ed25519Key: String
|
||||
public let x25519Key: String
|
||||
}
|
||||
|
||||
// MARK: Initialization
|
||||
internal init(address: String, port: UInt16, publicKeySet: KeySet) {
|
||||
self.address = address
|
||||
self.port = port
|
||||
self.publicKeySet = publicKeySet
|
||||
}
|
||||
|
||||
// MARK: Coding
|
||||
|
||||
// MARK: - NSCoding
|
||||
|
||||
public init?(coder: NSCoder) {
|
||||
address = coder.decodeObject(forKey: "address") as! String
|
||||
port = coder.decodeObject(forKey: "port") as! UInt16
|
||||
guard let idKey = coder.decodeObject(forKey: "idKey") as? String,
|
||||
let encryptionKey = coder.decodeObject(forKey: "encryptionKey") as? String else { return nil }
|
||||
|
||||
guard
|
||||
let idKey = coder.decodeObject(forKey: "idKey") as? String,
|
||||
let encryptionKey = coder.decodeObject(forKey: "encryptionKey") as? String
|
||||
else { return nil }
|
||||
|
||||
publicKeySet = KeySet(ed25519Key: idKey, x25519Key: encryptionKey)
|
||||
|
||||
super.init()
|
||||
}
|
||||
|
||||
public func encode(with coder: NSCoder) {
|
||||
coder.encode(address, forKey: "address")
|
||||
coder.encode(port, forKey: "port")
|
||||
coder.encode(publicKeySet.ed25519Key, forKey: "idKey")
|
||||
coder.encode(publicKeySet.x25519Key, forKey: "encryptionKey")
|
||||
fatalError("encode(with:) should never be called for legacy types")
|
||||
}
|
||||
|
||||
|
||||
// Note: The 'isEqual' and 'hash' overrides are both needed to ensure the migration
|
||||
// doesn't try to insert duplicate SNode entries into the new database (which would
|
||||
// result in unique key constraint violations)
|
||||
override public func isEqual(_ other: Any?) -> Bool {
|
||||
guard let other = other as? Snode else { return false }
|
||||
|
||||
return address == other.address && port == other.port
|
||||
}
|
||||
|
||||
override public var hash: Int { // Override NSObject.hash and not Hashable.hashValue or Hashable.hash(into:)
|
||||
override public var hash: Int {
|
||||
return address.hashValue ^ port.hashValue
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,6 +15,12 @@ enum _003_YDBToGRDBMigration: Migration {
|
|||
var snodeSetResult: [String: Set<Legacy.Snode>] = [:]
|
||||
var lastSnodePoolRefreshDate: Date? = nil
|
||||
|
||||
// Map the Legacy types for the NSKeyedUnarchiver
|
||||
NSKeyedUnarchiver.setClass(
|
||||
Legacy.Snode.self,
|
||||
forClassName: "SessionSnodeKit.Snode"
|
||||
)
|
||||
|
||||
Storage.read { transaction in
|
||||
// Process the lastSnodePoolRefreshDate
|
||||
lastSnodePoolRefreshDate = transaction.object(
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
|
||||
|
||||
import Foundation
|
||||
import GRDB
|
||||
|
||||
public class TypedTableAlias<T> where T: TableRecord, T: ColumnExpressible {
|
||||
let alias: TableAlias = TableAlias(name: T.databaseTableName)
|
||||
|
||||
public init() {}
|
||||
|
||||
public subscript(_ column: T.Columns) -> SQLExpression {
|
||||
return alias[column.name]
|
||||
}
|
||||
}
|
|
@ -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) }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
|
||||
|
||||
import Foundation
|
||||
import GRDB
|
||||
|
||||
public extension TableRecord where Self: ColumnExpressible {
|
||||
static func select(_ selection: Columns...) -> QueryInterfaceRequest<Self> {
|
||||
return all().select(selection)
|
||||
}
|
||||
}
|
|
@ -12,12 +12,16 @@ public extension UITableView {
|
|||
}
|
||||
|
||||
func dequeue<T>(type: T.Type, for indexPath: IndexPath) -> T where T: UITableViewCell {
|
||||
let reuseIdentifier = T.defaultReuseIdentifier
|
||||
// Note: We need to use `type.defaultReuseIdentifier` rather than `T.defaultReuseIdentifier`
|
||||
// otherwise we may get a subclass rather than the actual type we specified
|
||||
let reuseIdentifier = type.defaultReuseIdentifier
|
||||
return dequeueReusableCell(withIdentifier: reuseIdentifier, for: indexPath) as! T
|
||||
}
|
||||
|
||||
func dequeueHeaderFooterView<T>(type: T.Type) -> T where T: UITableViewHeaderFooterView {
|
||||
let reuseIdentifier = T.defaultReuseIdentifier
|
||||
// Note: We need to use `type.defaultReuseIdentifier` rather than `T.defaultReuseIdentifier`
|
||||
// otherwise we may get a subclass rather than the actual type we specified
|
||||
let reuseIdentifier = type.defaultReuseIdentifier
|
||||
return dequeueReusableHeaderFooterView(withIdentifier: reuseIdentifier) as! T
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
|
||||
|
||||
import Foundation
|
||||
import AVFoundation
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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) }
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue