From eb0118ac10c2d226c89af829bbe06263c79f6260 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Fri, 1 Jul 2022 12:52:41 +1000 Subject: [PATCH] Fixed a few more bugs and tweaked attachment download logic Updated the code to only auto-start attachment downloads when a user opens a conversation (and only for the current page of messages) Updated the GarbageCollectionJob to default to handling all cases (instead of requiring the cases to be defined) - this means we can add future cases without having to recreate the default job Added logic to remove approved blinded contact records as part of the GarbageCollectionJob Added code to better handle "invalid" attachments when migrating Added a mechanism to retrieve the details for currently running jobs (ie. allows us to check for duplicate concurrent jobs) Resolved the remaining TODOs in the GRDB migration code Cleaned up DB update logic to update only the targeted columns Fixed a bug due to a typo in a localised string Fixed a bug where link previews without images or with custom copy weren't being processed as link previews Fixed a bug where Open Groups could display with an empty name value --- Session.xcodeproj/project.pbxproj | 24 ++--- .../ConversationVC+Interaction.swift | 2 +- .../Conversations/ConversationViewModel.swift | 44 +++++++++ .../Content Views/LinkPreviewState.swift | 2 +- .../Views & Modals/BlockedModal.swift | 23 +++-- .../MediaDetailViewController.swift | 4 +- .../Translations/de.lproj/Localizable.strings | 2 +- .../Translations/en.lproj/Localizable.strings | 3 +- .../Translations/es.lproj/Localizable.strings | 2 +- .../Translations/fa.lproj/Localizable.strings | 2 +- .../Translations/fi.lproj/Localizable.strings | 2 +- .../Translations/fr.lproj/Localizable.strings | 2 +- .../Translations/hi.lproj/Localizable.strings | 2 +- .../Translations/hr.lproj/Localizable.strings | 2 +- .../id-ID.lproj/Localizable.strings | 2 +- .../Translations/it.lproj/Localizable.strings | 2 +- .../Translations/ja.lproj/Localizable.strings | 2 +- .../Translations/nl.lproj/Localizable.strings | 2 +- .../Translations/pl.lproj/Localizable.strings | 2 +- .../pt_BR.lproj/Localizable.strings | 2 +- .../Translations/ru.lproj/Localizable.strings | 2 +- .../Translations/si.lproj/Localizable.strings | 2 +- .../Translations/sk.lproj/Localizable.strings | 2 +- .../Translations/sv.lproj/Localizable.strings | 2 +- .../Translations/th.lproj/Localizable.strings | 2 +- .../vi-VN.lproj/Localizable.strings | 2 +- .../zh-Hant.lproj/Localizable.strings | 2 +- .../zh_CN.lproj/Localizable.strings | 2 +- .../Common Networking/Header.swift | 2 - .../Contacts/BlindedIdMapping.swift | 40 --------- .../Migrations/_002_SetupStandardJobs.swift | 7 +- .../Migrations/_003_YDBToGRDBMigration.swift | 74 ++++++++++++--- .../Database/Models/Attachment.swift | 46 ++++++---- .../Database/Models/ClosedGroup.swift | 12 --- .../Database/Models/LinkPreview.swift | 5 -- .../Database/Models/OpenGroup.swift | 2 +- .../Database/Models/Profile.swift | 9 +- .../Jobs/Types/AttachmentDownloadJob.swift | 89 +++++++++++++++---- .../Jobs/Types/GarbageCollectionJob.swift | 73 +++++++++------ .../MessageReceiver+ClosedGroups.swift | 6 +- .../MessageReceiver+VisibleMessages.swift | 32 +------ .../MessageSender+ClosedGroups.swift | 5 +- .../MessageSender+Convenience.swift | 1 + .../Sending & Receiving/MessageSender.swift | 22 ++--- .../Utilities/ProfileManager.swift | 6 +- .../Contacts/BlindedIdLookupSpec.swift | 32 +++++++ .../Contacts/BlindedIdMappingSpec.swift | 48 ---------- .../General/Dictionary+Utilities.swift | 8 +- SessionUtilitiesKit/JobRunner/JobRunner.swift | 16 ++++ 49 files changed, 377 insertions(+), 302 deletions(-) delete mode 100644 SessionMessagingKit/Contacts/BlindedIdMapping.swift create mode 100644 SessionMessagingKitTests/Contacts/BlindedIdLookupSpec.swift delete mode 100644 SessionMessagingKitTests/Contacts/BlindedIdMappingSpec.swift diff --git a/Session.xcodeproj/project.pbxproj b/Session.xcodeproj/project.pbxproj index 95da2432b..e66c454b1 100644 --- a/Session.xcodeproj/project.pbxproj +++ b/Session.xcodeproj/project.pbxproj @@ -593,7 +593,6 @@ FD09C5E828264937000CE219 /* MediaDetailViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD09C5E728264937000CE219 /* MediaDetailViewController.swift */; }; FD09C5EA282A1BB2000CE219 /* ThreadTypingIndicator.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD09C5E9282A1BB2000CE219 /* ThreadTypingIndicator.swift */; }; FD09C5EC282B8F18000CE219 /* AttachmentError.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD09C5EB282B8F17000CE219 /* AttachmentError.swift */; }; - FD0BA51B27CD88EC00CC6805 /* BlindedIdMapping.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD0BA51A27CD88EC00CC6805 /* BlindedIdMapping.swift */; }; FD17D79927F40AB800122BE0 /* _003_YDBToGRDBMigration.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD17D79827F40AB800122BE0 /* _003_YDBToGRDBMigration.swift */; }; FD17D79C27F40B2E00122BE0 /* SMKLegacy.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD17D79B27F40B2E00122BE0 /* SMKLegacy.swift */; }; FD17D7A027F40CC800122BE0 /* _001_InitialSetupMigration.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD17D79F27F40CC800122BE0 /* _001_InitialSetupMigration.swift */; }; @@ -653,7 +652,7 @@ FD3C906027E410F700CD579F /* FileUploadResponseSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD3C905F27E410F700CD579F /* FileUploadResponseSpec.swift */; }; FD3C906227E411AF00CD579F /* HeaderSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD3C906127E411AF00CD579F /* HeaderSpec.swift */; }; FD3C906427E4122F00CD579F /* RequestSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD3C906327E4122F00CD579F /* RequestSpec.swift */; }; - FD3C906727E416AF00CD579F /* BlindedIdMappingSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD3C906627E416AF00CD579F /* BlindedIdMappingSpec.swift */; }; + FD3C906727E416AF00CD579F /* BlindedIdLookupSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD3C906627E416AF00CD579F /* BlindedIdLookupSpec.swift */; }; FD3C906A27E417CE00CD579F /* SodiumUtilitiesSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD3C906927E417CE00CD579F /* SodiumUtilitiesSpec.swift */; }; FD3C906D27E43C4B00CD579F /* MessageSenderEncryptionSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD3C906C27E43C4B00CD579F /* MessageSenderEncryptionSpec.swift */; }; FD3C906F27E43E8700CD579F /* MockBox.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD3C906E27E43E8700CD579F /* MockBox.swift */; }; @@ -1658,7 +1657,6 @@ FD09C5E728264937000CE219 /* MediaDetailViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaDetailViewController.swift; sourceTree = ""; }; FD09C5E9282A1BB2000CE219 /* ThreadTypingIndicator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadTypingIndicator.swift; sourceTree = ""; }; FD09C5EB282B8F17000CE219 /* AttachmentError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttachmentError.swift; sourceTree = ""; }; - FD0BA51A27CD88EC00CC6805 /* BlindedIdMapping.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlindedIdMapping.swift; sourceTree = ""; }; FD17D79527F3E04600122BE0 /* _001_InitialSetupMigration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _001_InitialSetupMigration.swift; sourceTree = ""; }; FD17D79827F40AB800122BE0 /* _003_YDBToGRDBMigration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _003_YDBToGRDBMigration.swift; sourceTree = ""; }; FD17D79B27F40B2E00122BE0 /* SMKLegacy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SMKLegacy.swift; sourceTree = ""; }; @@ -1694,7 +1692,7 @@ FD3C905F27E410F700CD579F /* FileUploadResponseSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileUploadResponseSpec.swift; sourceTree = ""; }; FD3C906127E411AF00CD579F /* HeaderSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HeaderSpec.swift; sourceTree = ""; }; FD3C906327E4122F00CD579F /* RequestSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RequestSpec.swift; sourceTree = ""; }; - FD3C906627E416AF00CD579F /* BlindedIdMappingSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlindedIdMappingSpec.swift; sourceTree = ""; }; + FD3C906627E416AF00CD579F /* BlindedIdLookupSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlindedIdLookupSpec.swift; sourceTree = ""; }; FD3C906927E417CE00CD579F /* SodiumUtilitiesSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SodiumUtilitiesSpec.swift; sourceTree = ""; }; FD3C906C27E43C4B00CD579F /* MessageSenderEncryptionSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageSenderEncryptionSpec.swift; sourceTree = ""; }; FD3C906E27E43E8700CD579F /* MockBox.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockBox.swift; sourceTree = ""; }; @@ -2424,14 +2422,6 @@ path = General; sourceTree = ""; }; - B8B3201F258B1A540020074B /* Contacts */ = { - isa = PBXGroup; - children = ( - FD0BA51A27CD88EC00CC6805 /* BlindedIdMapping.swift */, - ); - path = Contacts; - sourceTree = ""; - }; B8B558ED26C4B55F00693325 /* Calls */ = { isa = PBXGroup; children = ( @@ -3153,7 +3143,6 @@ C3C2A70A25539DF900C340D1 /* Meta */, FDC4384D27B47FD600C60D73 /* Common Networking */, B8DE1FB226C22F1F0079C9CE /* Calls */, - B8B3201F258B1A540020074B /* Contacts */, C32C5BCB256DC818003C73A2 /* Database */, C300A5BB2554AFFB00555489 /* Messages */, C300A5F02554B08500555489 /* Sending & Receiving */, @@ -3615,7 +3604,7 @@ FD3C906527E416A200CD579F /* Contacts */ = { isa = PBXGroup; children = ( - FD3C906627E416AF00CD579F /* BlindedIdMappingSpec.swift */, + FD3C906627E416AF00CD579F /* BlindedIdLookupSpec.swift */, ); path = Contacts; sourceTree = ""; @@ -5113,7 +5102,6 @@ FD09797527FAB64300936362 /* ProfileManager.swift in Sources */, FD245C57285065F100B966DD /* Poller.swift in Sources */, FDA8EAFE280E8B78002B68E5 /* FailedMessageSendsJob.swift in Sources */, - FD0BA51B27CD88EC00CC6805 /* BlindedIdMapping.swift in Sources */, FD245C6A2850666F00B966DD /* FileServerAPI.swift in Sources */, FDC4386927B4E6B800C60D73 /* String+Utlities.swift in Sources */, FD716E6628502EE200C96BF4 /* CurrentCallProtocol.swift in Sources */, @@ -5487,7 +5475,7 @@ FD3C906D27E43C4B00CD579F /* MessageSenderEncryptionSpec.swift in Sources */, FDC2908D27D70905005DAE71 /* UpdateMessageRequestSpec.swift in Sources */, FD078E5427E197CA000769AF /* OpenGroupManagerSpec.swift in Sources */, - FD3C906727E416AF00CD579F /* BlindedIdMappingSpec.swift in Sources */, + FD3C906727E416AF00CD579F /* BlindedIdLookupSpec.swift in Sources */, FD83B9D227D59495005E1583 /* MockUserDefaults.swift in Sources */, FD078E5C27E29F78000769AF /* MockNonce24Generator.swift in Sources */, ); @@ -6818,7 +6806,7 @@ CODE_SIGN_ENTITLEMENTS = Session/Meta/Signal.entitlements; CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - CURRENT_PROJECT_VERSION = 350; + CURRENT_PROJECT_VERSION = 354; DEVELOPMENT_TEAM = SUQ8J2PCT7; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", @@ -6890,7 +6878,7 @@ CODE_SIGN_ENTITLEMENTS = Session/Meta/Signal.entitlements; CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - CURRENT_PROJECT_VERSION = 350; + CURRENT_PROJECT_VERSION = 354; DEVELOPMENT_TEAM = SUQ8J2PCT7; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", diff --git a/Session/Conversations/ConversationVC+Interaction.swift b/Session/Conversations/ConversationVC+Interaction.swift index 2aea6b825..2324dd93c 100644 --- a/Session/Conversations/ConversationVC+Interaction.swift +++ b/Session/Conversations/ConversationVC+Interaction.swift @@ -751,7 +751,7 @@ extension ConversationVC: guard let mediaView = albumView.mediaView(forLocation: locationInAlbumView) else { return } switch mediaView.attachment.state { - case .pendingDownload, .downloading, .uploading: break + case .pendingDownload, .downloading, .uploading, .invalid: break // Failed uploads should be handled via the "resend" process instead case .failedUpload: break diff --git a/Session/Conversations/ConversationViewModel.swift b/Session/Conversations/ConversationViewModel.swift index 4d44fa286..5f631d953 100644 --- a/Session/Conversations/ConversationViewModel.swift +++ b/Session/Conversations/ConversationViewModel.swift @@ -36,6 +36,12 @@ public class ConversationViewModel: OWSAudioPlayerDelegate { public var lastSearchedText: String? public let focusedInteractionId: Int64? // Note: This is used for global search + /// We maintain a local set of ids for attachments which we have automatically created attachmentDownload jobs for + /// in order to avoid creating excessive jobs while the user is actively chatting in a conversation (the attachmentDownload + /// jobs run serially and will only actually perform the download if the attachment hasn't already been downloaded so + /// we don't need to worry about duplicate jobs but it's better to avoid creating duplicate jobs when possible) + private var autoStartedDownloadJobAttachmentIds: Set = [] + public lazy var blockedBannerMessage: String = { switch self.threadData.threadVariant { case .contact: @@ -240,6 +246,44 @@ public class ConversationViewModel: OWSAudioPlayerDelegate { .filter { $0.isTypingIndicator != true } .sorted { lhs, rhs -> Bool in lhs.timestampMs < rhs.timestampMs } + // Add download jobs for any attachments which need to be downloaded + let pendingAttachmentsToDownload: [(attachment: Attachment, interactionId: Int64)] = sortedData + .flatMap { viewModel -> [(attachment: Attachment, interactionId: Int64)] in + // Do nothing if this is an incoming message on an untrusted contact thread + guard + viewModel.variant != .standardIncoming || + viewModel.threadIsTrusted || + viewModel.threadVariant != .contact + else { return [] } + + return (viewModel.attachments ?? []) + .appending(viewModel.quoteAttachment) + .appending(viewModel.linkPreviewAttachment) + .filter { $0.state == .pendingDownload } + .filter { !self.autoStartedDownloadJobAttachmentIds.contains($0.id) } + .map { ($0, viewModel.id) } + } + + if !pendingAttachmentsToDownload.isEmpty { + GRDBStorage.shared.writeAsync { db in + pendingAttachmentsToDownload.forEach { attachment, interactionId in + JobRunner.add( + db, + job: Job( + variant: .attachmentDownload, + threadId: self.threadId, + interactionId: interactionId, + details: AttachmentDownloadJob.Details( + attachmentId: attachment.id + ) + ) + ) + + self.autoStartedDownloadJobAttachmentIds.insert(attachment.id) + } + } + } + // We load messages from newest to oldest so having a pageOffset larger than zero means // there are newer pages to load return [ diff --git a/Session/Conversations/Message Cells/Content Views/LinkPreviewState.swift b/Session/Conversations/Message Cells/Content Views/LinkPreviewState.swift index 22fc0ea74..054d19271 100644 --- a/Session/Conversations/Message Cells/Content Views/LinkPreviewState.swift +++ b/Session/Conversations/Message Cells/Content Views/LinkPreviewState.swift @@ -96,7 +96,7 @@ public extension LinkPreview { return .loaded case .pendingDownload, .downloading, .uploading: return .loading - case .failedDownload, .failedUpload: return .invalid + case .failedDownload, .failedUpload, .invalid: return .invalid } } diff --git a/Session/Conversations/Views & Modals/BlockedModal.swift b/Session/Conversations/Views & Modals/BlockedModal.swift index 854e2fc36..9bcc28c02 100644 --- a/Session/Conversations/Views & Modals/BlockedModal.swift +++ b/Session/Conversations/Views & Modals/BlockedModal.swift @@ -73,21 +73,20 @@ final class BlockedModal: Modal { contentView.pin(.bottom, to: .bottom, of: mainStackView, withInset: spacing) } - // MARK: Interaction + // MARK: - Interaction + @objc private func unblock() { let publicKey: String = self.publicKey - GRDBStorage.shared.writeAsync( - updates: { db in - try? Contact - .fetchOne(db, id: publicKey)? - .with(isBlocked: true) - .update(db) - }, - completion: { db, _ in - try MessageSender.syncConfiguration(db, forceSyncNow: true).retainUntilComplete() - } - ) + GRDBStorage.shared.writeAsync { db in + try Contact + .filter(id: publicKey) + .updateAll(db, Contact.Columns.isBlocked.set(to: true)) + + try MessageSender + .syncConfiguration(db, forceSyncNow: true) + .retainUntilComplete() + } presentingViewController?.dismiss(animated: true, completion: nil) } diff --git a/Session/Media Viewing & Editing/MediaDetailViewController.swift b/Session/Media Viewing & Editing/MediaDetailViewController.swift index b83035af2..0ae1ff5a3 100644 --- a/Session/Media Viewing & Editing/MediaDetailViewController.swift +++ b/Session/Media Viewing & Editing/MediaDetailViewController.swift @@ -155,9 +155,7 @@ class MediaDetailViewController: OWSViewController, UIScrollViewDelegate, OWSVid self.mediaView.removeFromSuperview() self.playVideoButton.removeFromSuperview() self.videoProgressBar.removeFromSuperview() - - // TODO: COnfirm this - scrollView.zoomScale = 1 + self.scrollView.zoomScale = 1 if self.galleryItem.attachment.isAnimated { if self.galleryItem.attachment.isValid, let originalFilePath: String = self.galleryItem.attachment.originalFilePath { diff --git a/Session/Meta/Translations/de.lproj/Localizable.strings b/Session/Meta/Translations/de.lproj/Localizable.strings index 3ebda7272..12d7713ec 100644 --- a/Session/Meta/Translations/de.lproj/Localizable.strings +++ b/Session/Meta/Translations/de.lproj/Localizable.strings @@ -627,7 +627,7 @@ "UNPIN_BUTTON_TEXT" = "Loslösen"; "modal_call_missed_tips_title" = "Verpasster Anruf"; "modal_call_missed_tips_explanation" = "Verpasster Anruf von '%@', da du die Berechtigung 'Anrufe und Videoanrufe' in den Datenschutzeinstellungen aktivieren musst."; -"meida_saved" = "Medien gespeichert von %@."; +"media_saved" = "Medien gespeichert von %@."; "screenshot_taken" = "%@ hat ein Screenshot gemacht."; "SEARCH_SECTION_CONTACTS" = "Kontakte und Gruppen"; "SEARCH_SECTION_MESSAGES" = "Nachrichten"; diff --git a/Session/Meta/Translations/en.lproj/Localizable.strings b/Session/Meta/Translations/en.lproj/Localizable.strings index f46ff4cbf..57ef01ba2 100644 --- a/Session/Meta/Translations/en.lproj/Localizable.strings +++ b/Session/Meta/Translations/en.lproj/Localizable.strings @@ -627,7 +627,7 @@ "UNPIN_BUTTON_TEXT" = "Unpin"; "modal_call_missed_tips_title" = "Call missed"; "modal_call_missed_tips_explanation" = "Call missed from '%@' because you needed to enable the 'Voice and video calls' permission in the Privacy Settings."; -"meida_saved" = "Media saved by %@."; +"media_saved" = "Media saved by %@."; "screenshot_taken" = "%@ took a screenshot."; "SEARCH_SECTION_CONTACTS" = "Contacts and Groups"; "SEARCH_SECTION_MESSAGES" = "Messages"; @@ -660,3 +660,4 @@ "MESSAGE_TRIMMING_TITLE" = "Message Trimming"; "MESSAGE_TRIMMING_OPEN_GROUP_TITLE" = "Delete Old Open Group Messages"; "MESSAGE_TRIMMING_OPEN_GROUP_DESCRIPTION" = "Automatically delete open group messages which are older than 6 months when starting the app"; + diff --git a/Session/Meta/Translations/es.lproj/Localizable.strings b/Session/Meta/Translations/es.lproj/Localizable.strings index 6c0ce3a4f..c721a032e 100644 --- a/Session/Meta/Translations/es.lproj/Localizable.strings +++ b/Session/Meta/Translations/es.lproj/Localizable.strings @@ -627,7 +627,7 @@ "UNPIN_BUTTON_TEXT" = "Dejar de fijar"; "modal_call_missed_tips_title" = "Llamada perdida"; "modal_call_missed_tips_explanation" = "Llamada perdida de '%@' porque necesitas habilitar el permiso de 'Llamadas de voz y video' en la configuración de privacidad."; -"meida_saved" = "Media saved by %@."; +"media_saved" = "Media saved by %@."; "screenshot_taken" = "%@ tomó una captura de pantalla."; "SEARCH_SECTION_CONTACTS" = "Contacts and Groups"; "SEARCH_SECTION_MESSAGES" = "Messages"; diff --git a/Session/Meta/Translations/fa.lproj/Localizable.strings b/Session/Meta/Translations/fa.lproj/Localizable.strings index cf91d20ac..b15fa022f 100644 --- a/Session/Meta/Translations/fa.lproj/Localizable.strings +++ b/Session/Meta/Translations/fa.lproj/Localizable.strings @@ -627,7 +627,7 @@ "UNPIN_BUTTON_TEXT" = "Unpin"; "modal_call_missed_tips_title" = "Call missed"; "modal_call_missed_tips_explanation" = "Call missed from '%@' because you needed to enable the 'Voice and video calls' permission in the Privacy Settings."; -"meida_saved" = "Media saved by %@."; +"media_saved" = "Media saved by %@."; "screenshot_taken" = "%@ took a screenshot."; "SEARCH_SECTION_CONTACTS" = "Contacts and Groups"; "SEARCH_SECTION_MESSAGES" = "Messages"; diff --git a/Session/Meta/Translations/fi.lproj/Localizable.strings b/Session/Meta/Translations/fi.lproj/Localizable.strings index c30b60393..4c4ed311e 100644 --- a/Session/Meta/Translations/fi.lproj/Localizable.strings +++ b/Session/Meta/Translations/fi.lproj/Localizable.strings @@ -627,7 +627,7 @@ "UNPIN_BUTTON_TEXT" = "Irrota"; "modal_call_missed_tips_title" = "Vastaamaton puhelu"; "modal_call_missed_tips_explanation" = "Vastaamaton puhelu käyttäjältä '%@', koska pahelut edellyttävät 'Ääni- ja videopuhelut' -käyttöoikeuden yksityisyysasetuksista."; -"meida_saved" = "%@ tallensi median."; +"media_saved" = "%@ tallensi median."; "screenshot_taken" = "%@ otti kuvankaappauksen."; "SEARCH_SECTION_CONTACTS" = "Henkilöt ja ryhmät"; "SEARCH_SECTION_MESSAGES" = "Viestit"; diff --git a/Session/Meta/Translations/fr.lproj/Localizable.strings b/Session/Meta/Translations/fr.lproj/Localizable.strings index 820120d7c..52ca9e487 100644 --- a/Session/Meta/Translations/fr.lproj/Localizable.strings +++ b/Session/Meta/Translations/fr.lproj/Localizable.strings @@ -627,7 +627,7 @@ "UNPIN_BUTTON_TEXT" = "Désépingler"; "modal_call_missed_tips_title" = "Appel manqué"; "modal_call_missed_tips_explanation" = "Appel manqué de '%@' car vous devez activer la permission 'Appels vocaux et vidéo' dans les paramètres de confidentialité."; -"meida_saved" = "%@ a enregistré le média."; +"media_saved" = "%@ a enregistré le média."; "screenshot_taken" = "%@ a pris une capture d'écran."; "SEARCH_SECTION_CONTACTS" = "Contacts et Groupes"; "SEARCH_SECTION_MESSAGES" = "Messages"; diff --git a/Session/Meta/Translations/hi.lproj/Localizable.strings b/Session/Meta/Translations/hi.lproj/Localizable.strings index e862c25c0..5bdba14ac 100644 --- a/Session/Meta/Translations/hi.lproj/Localizable.strings +++ b/Session/Meta/Translations/hi.lproj/Localizable.strings @@ -627,7 +627,7 @@ "UNPIN_BUTTON_TEXT" = "Unpin"; "modal_call_missed_tips_title" = "Call missed"; "modal_call_missed_tips_explanation" = "Call missed from '%@' because you needed to enable the 'Voice and video calls' permission in the Privacy Settings."; -"meida_saved" = "Media saved by %@."; +"media_saved" = "Media saved by %@."; "screenshot_taken" = "%@ took a screenshot."; "SEARCH_SECTION_CONTACTS" = "Contacts and Groups"; "SEARCH_SECTION_MESSAGES" = "Messages"; diff --git a/Session/Meta/Translations/hr.lproj/Localizable.strings b/Session/Meta/Translations/hr.lproj/Localizable.strings index e40b5492e..bacb148c8 100644 --- a/Session/Meta/Translations/hr.lproj/Localizable.strings +++ b/Session/Meta/Translations/hr.lproj/Localizable.strings @@ -627,7 +627,7 @@ "UNPIN_BUTTON_TEXT" = "Otkvači"; "modal_call_missed_tips_title" = "Propušten poziv"; "modal_call_missed_tips_explanation" = "Propušten poziv od '%@' jer 'Audio i video pozivi' nemaju dopuštenje u Postavkama privatnosti."; -"meida_saved" = "%@ je spremio/la medij."; +"media_saved" = "%@ je spremio/la medij."; "screenshot_taken" = "%@ je napravio/la snimku zaslona."; "SEARCH_SECTION_CONTACTS" = "Contacts and Groups"; "SEARCH_SECTION_MESSAGES" = "Messages"; diff --git a/Session/Meta/Translations/id-ID.lproj/Localizable.strings b/Session/Meta/Translations/id-ID.lproj/Localizable.strings index 3ffd04b51..b3ccf6522 100644 --- a/Session/Meta/Translations/id-ID.lproj/Localizable.strings +++ b/Session/Meta/Translations/id-ID.lproj/Localizable.strings @@ -627,7 +627,7 @@ "UNPIN_BUTTON_TEXT" = "Unpin"; "modal_call_missed_tips_title" = "Call missed"; "modal_call_missed_tips_explanation" = "Call missed from '%@' because you needed to enable the 'Voice and video calls' permission in the Privacy Settings."; -"meida_saved" = "Media saved by %@."; +"media_saved" = "Media saved by %@."; "screenshot_taken" = "%@ took a screenshot."; "SEARCH_SECTION_CONTACTS" = "Contacts and Groups"; "SEARCH_SECTION_MESSAGES" = "Messages"; diff --git a/Session/Meta/Translations/it.lproj/Localizable.strings b/Session/Meta/Translations/it.lproj/Localizable.strings index bbd0183d8..b00a8fab3 100644 --- a/Session/Meta/Translations/it.lproj/Localizable.strings +++ b/Session/Meta/Translations/it.lproj/Localizable.strings @@ -627,7 +627,7 @@ "UNPIN_BUTTON_TEXT" = "Non fissare in alto"; "modal_call_missed_tips_title" = "Chiamata persa"; "modal_call_missed_tips_explanation" = "Chiamata persa da '%@' perché era necessario abilitare l'autorizzazione 'Voce e video chiamate' nelle Impostazioni Privacy."; -"meida_saved" = "Media salvato da %@."; +"media_saved" = "Media salvato da %@."; "screenshot_taken" = "%@ ha acquisito uno screenshot."; "SEARCH_SECTION_CONTACTS" = "Contatti e Gruppi"; "SEARCH_SECTION_MESSAGES" = "Messaggi"; diff --git a/Session/Meta/Translations/ja.lproj/Localizable.strings b/Session/Meta/Translations/ja.lproj/Localizable.strings index 108805053..7be122fca 100644 --- a/Session/Meta/Translations/ja.lproj/Localizable.strings +++ b/Session/Meta/Translations/ja.lproj/Localizable.strings @@ -627,7 +627,7 @@ "UNPIN_BUTTON_TEXT" = "ピン留めを外す"; "modal_call_missed_tips_title" = "通話できません"; "modal_call_missed_tips_explanation" = "プライバシー設定で「音声通話とビデオ通話」を許可していないため、%@から着信できませんでした。"; -"meida_saved" = "%@ によって保存されたメディア"; +"media_saved" = "%@ によって保存されたメディア"; "screenshot_taken" = "%@はスクリーンショットを撮りました。"; "SEARCH_SECTION_CONTACTS" = "連絡先とグループ"; "SEARCH_SECTION_MESSAGES" = "メッセージ"; diff --git a/Session/Meta/Translations/nl.lproj/Localizable.strings b/Session/Meta/Translations/nl.lproj/Localizable.strings index 6aadc1caf..ac4a696b4 100644 --- a/Session/Meta/Translations/nl.lproj/Localizable.strings +++ b/Session/Meta/Translations/nl.lproj/Localizable.strings @@ -627,7 +627,7 @@ "UNPIN_BUTTON_TEXT" = "Losmaken"; "modal_call_missed_tips_title" = "Oproep gemist"; "modal_call_missed_tips_explanation" = "Oproep gemist van '%@' omdat je de 'Spraak- en video-oproep' permissie nodig hebt in de privacy-instellingen."; -"meida_saved" = "Media opgeslagen door %@."; +"media_saved" = "Media opgeslagen door %@."; "screenshot_taken" = "%@ heeft een schermafbeelding genomen."; "SEARCH_SECTION_CONTACTS" = "Contacts and Groups"; "SEARCH_SECTION_MESSAGES" = "Messages"; diff --git a/Session/Meta/Translations/pl.lproj/Localizable.strings b/Session/Meta/Translations/pl.lproj/Localizable.strings index 2e93d89d6..e873b2e2e 100644 --- a/Session/Meta/Translations/pl.lproj/Localizable.strings +++ b/Session/Meta/Translations/pl.lproj/Localizable.strings @@ -627,7 +627,7 @@ "UNPIN_BUTTON_TEXT" = "Odepnij"; "modal_call_missed_tips_title" = "Połączenie nieodebrane"; "modal_call_missed_tips_explanation" = "Połączenie nieodebrane od '%@' ponieważ musisz włączyć uprawnienie 'Połączenia głosowe i wideo' w Ustawieniach Prywatności."; -"meida_saved" = "Media zapisane przez %@."; +"media_saved" = "Media zapisane przez %@."; "screenshot_taken" = "%@ wykonał zrzut ekranu."; "SEARCH_SECTION_CONTACTS" = "Kontakty i grupy"; "SEARCH_SECTION_MESSAGES" = "Wiadomości"; diff --git a/Session/Meta/Translations/pt_BR.lproj/Localizable.strings b/Session/Meta/Translations/pt_BR.lproj/Localizable.strings index 3a933083a..218d22cac 100644 --- a/Session/Meta/Translations/pt_BR.lproj/Localizable.strings +++ b/Session/Meta/Translations/pt_BR.lproj/Localizable.strings @@ -627,7 +627,7 @@ "UNPIN_BUTTON_TEXT" = "Desfixar"; "modal_call_missed_tips_title" = "Chamada perdida"; "modal_call_missed_tips_explanation" = "Chamada perdida de '%@', você precisa habilitar a permissão de 'Voz e Video' nas configurações de Privacidade."; -"meida_saved" = "Mídia salva por %@."; +"media_saved" = "Mídia salva por %@."; "screenshot_taken" = "%@ fez uma captura de tela."; "SEARCH_SECTION_CONTACTS" = "Contacts and Groups"; "SEARCH_SECTION_MESSAGES" = "Messages"; diff --git a/Session/Meta/Translations/ru.lproj/Localizable.strings b/Session/Meta/Translations/ru.lproj/Localizable.strings index e6f0421e3..2f501ee7d 100644 --- a/Session/Meta/Translations/ru.lproj/Localizable.strings +++ b/Session/Meta/Translations/ru.lproj/Localizable.strings @@ -627,7 +627,7 @@ "UNPIN_BUTTON_TEXT" = "Открепить"; "modal_call_missed_tips_title" = "Пропущен вызов"; "modal_call_missed_tips_explanation" = "Вызов от '%@' пропущен, вам необходимо включить разрешение 'Голосовые и видео вызовы' в настройках Конфиденциальности."; -"meida_saved" = "%@ сохранил(а) медиафайл."; +"media_saved" = "%@ сохранил(а) медиафайл."; "screenshot_taken" = "%@ сделал(а) снимок экрана."; "SEARCH_SECTION_CONTACTS" = "Контакты и группы"; "SEARCH_SECTION_MESSAGES" = "Сообщения"; diff --git a/Session/Meta/Translations/si.lproj/Localizable.strings b/Session/Meta/Translations/si.lproj/Localizable.strings index 04d1ea3d2..14696b268 100644 --- a/Session/Meta/Translations/si.lproj/Localizable.strings +++ b/Session/Meta/Translations/si.lproj/Localizable.strings @@ -627,7 +627,7 @@ "UNPIN_BUTTON_TEXT" = "Unpin"; "modal_call_missed_tips_title" = "Call missed"; "modal_call_missed_tips_explanation" = "Call missed from '%@' because you needed to enable the 'Voice and video calls' permission in the Privacy Settings."; -"meida_saved" = "Media saved by %@."; +"media_saved" = "Media saved by %@."; "screenshot_taken" = "%@ took a screenshot."; "SEARCH_SECTION_CONTACTS" = "Contacts and Groups"; "SEARCH_SECTION_MESSAGES" = "Messages"; diff --git a/Session/Meta/Translations/sk.lproj/Localizable.strings b/Session/Meta/Translations/sk.lproj/Localizable.strings index d2ed3836a..cdf5f5f7f 100644 --- a/Session/Meta/Translations/sk.lproj/Localizable.strings +++ b/Session/Meta/Translations/sk.lproj/Localizable.strings @@ -627,7 +627,7 @@ "UNPIN_BUTTON_TEXT" = "Zrušiť pripnutie"; "modal_call_missed_tips_title" = "Zmeškaný hovor"; "modal_call_missed_tips_explanation" = "Zmeškaný hovor od %@ pretože ste potrebovali zapnúť povolenie pre 'Hlasové a video hovory' v Nastaveniach Súkromia."; -"meida_saved" = "Médiá uložené používateľom %@."; +"media_saved" = "Médiá uložené používateľom %@."; "screenshot_taken" = "%@ urobili snímku obrazovky."; "SEARCH_SECTION_CONTACTS" = "Kontakty a Skupiny"; "SEARCH_SECTION_MESSAGES" = "Správy"; diff --git a/Session/Meta/Translations/sv.lproj/Localizable.strings b/Session/Meta/Translations/sv.lproj/Localizable.strings index 617b930c5..ccc97107f 100644 --- a/Session/Meta/Translations/sv.lproj/Localizable.strings +++ b/Session/Meta/Translations/sv.lproj/Localizable.strings @@ -627,7 +627,7 @@ "UNPIN_BUTTON_TEXT" = "Unpin"; "modal_call_missed_tips_title" = "Call missed"; "modal_call_missed_tips_explanation" = "Call missed from '%@' because you needed to enable the 'Voice and video calls' permission in the Privacy Settings."; -"meida_saved" = "Media saved by %@."; +"media_saved" = "Media saved by %@."; "screenshot_taken" = "%@ took a screenshot."; "SEARCH_SECTION_CONTACTS" = "Contacts and Groups"; "SEARCH_SECTION_MESSAGES" = "Messages"; diff --git a/Session/Meta/Translations/th.lproj/Localizable.strings b/Session/Meta/Translations/th.lproj/Localizable.strings index 5832a403b..656d65ef6 100644 --- a/Session/Meta/Translations/th.lproj/Localizable.strings +++ b/Session/Meta/Translations/th.lproj/Localizable.strings @@ -627,7 +627,7 @@ "UNPIN_BUTTON_TEXT" = "Unpin"; "modal_call_missed_tips_title" = "Call missed"; "modal_call_missed_tips_explanation" = "Call missed from '%@' because you needed to enable the 'Voice and video calls' permission in the Privacy Settings."; -"meida_saved" = "Media saved by %@."; +"media_saved" = "Media saved by %@."; "screenshot_taken" = "%@ took a screenshot."; "SEARCH_SECTION_CONTACTS" = "Contacts and Groups"; "SEARCH_SECTION_MESSAGES" = "Messages"; diff --git a/Session/Meta/Translations/vi-VN.lproj/Localizable.strings b/Session/Meta/Translations/vi-VN.lproj/Localizable.strings index 5835cc170..2a0d7928c 100644 --- a/Session/Meta/Translations/vi-VN.lproj/Localizable.strings +++ b/Session/Meta/Translations/vi-VN.lproj/Localizable.strings @@ -627,7 +627,7 @@ "UNPIN_BUTTON_TEXT" = "Unpin"; "modal_call_missed_tips_title" = "Call missed"; "modal_call_missed_tips_explanation" = "Call missed from '%@' because you needed to enable the 'Voice and video calls' permission in the Privacy Settings."; -"meida_saved" = "Media saved by %@."; +"media_saved" = "Media saved by %@."; "screenshot_taken" = "%@ took a screenshot."; "SEARCH_SECTION_CONTACTS" = "Contacts and Groups"; "SEARCH_SECTION_MESSAGES" = "Messages"; diff --git a/Session/Meta/Translations/zh-Hant.lproj/Localizable.strings b/Session/Meta/Translations/zh-Hant.lproj/Localizable.strings index 333724265..87e8a6c3e 100644 --- a/Session/Meta/Translations/zh-Hant.lproj/Localizable.strings +++ b/Session/Meta/Translations/zh-Hant.lproj/Localizable.strings @@ -627,7 +627,7 @@ "UNPIN_BUTTON_TEXT" = "取消置頂"; "modal_call_missed_tips_title" = "未接來電"; "modal_call_missed_tips_explanation" = "Call missed from '%@' because you needed to enable the 'Voice and video calls' permission in the Privacy Settings."; -"meida_saved" = "%@ 儲存了媒體"; +"media_saved" = "%@ 儲存了媒體"; "screenshot_taken" = "%@ 擷取了螢幕畫面"; "SEARCH_SECTION_CONTACTS" = "Contacts and Groups"; "SEARCH_SECTION_MESSAGES" = "Messages"; diff --git a/Session/Meta/Translations/zh_CN.lproj/Localizable.strings b/Session/Meta/Translations/zh_CN.lproj/Localizable.strings index 637d27c67..46cad399f 100644 --- a/Session/Meta/Translations/zh_CN.lproj/Localizable.strings +++ b/Session/Meta/Translations/zh_CN.lproj/Localizable.strings @@ -627,7 +627,7 @@ "UNPIN_BUTTON_TEXT" = "取消置顶"; "modal_call_missed_tips_title" = "未接来电"; "modal_call_missed_tips_explanation" = "未接听 '%@',因为您需要在隐私设置中启用“语音和视频通话”权限。"; -"meida_saved" = "%@ 保存了媒体内容。"; +"media_saved" = "%@ 保存了媒体内容。"; "screenshot_taken" = "%@ 进行了截图。"; "SEARCH_SECTION_CONTACTS" = "联系人和群组"; "SEARCH_SECTION_MESSAGES" = "消息"; diff --git a/SessionMessagingKit/Common Networking/Header.swift b/SessionMessagingKit/Common Networking/Header.swift index 97ea01ef7..6c33e41a3 100644 --- a/SessionMessagingKit/Common Networking/Header.swift +++ b/SessionMessagingKit/Common Networking/Header.swift @@ -7,8 +7,6 @@ enum Header: String { case contentType = "Content-Type" case contentDisposition = "Content-Disposition" - case room = "Room" // TODO: Confirm this is needed - case sogsPubKey = "X-SOGS-Pubkey" case sogsNonce = "X-SOGS-Nonce" case sogsTimestamp = "X-SOGS-Timestamp" diff --git a/SessionMessagingKit/Contacts/BlindedIdMapping.swift b/SessionMessagingKit/Contacts/BlindedIdMapping.swift deleted file mode 100644 index 5b289c96b..000000000 --- a/SessionMessagingKit/Contacts/BlindedIdMapping.swift +++ /dev/null @@ -1,40 +0,0 @@ -// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. - -import Foundation - -@objc(SNBlindedIdMapping) -public final class BlindedIdMapping: NSObject, NSCoding { // NSObject/NSCoding conformance is needed for YapDatabase compatibility - @objc public let blindedId: String - @objc public let sessionId: String - @objc public let serverPublicKey: String - - // MARK: - Initialization - - @objc public init(blindedId: String, sessionId: String, serverPublicKey: String) { - self.blindedId = blindedId - self.sessionId = sessionId - self.serverPublicKey = serverPublicKey - - super.init() - } - - private override init() { preconditionFailure("Use init(blindedId:sessionId:) instead.") } - - // MARK: - Coding - - public required init?(coder: NSCoder) { - guard let blindedId: String = coder.decodeObject(forKey: "blindedId") as! String? else { return nil } - guard let sessionId: String = coder.decodeObject(forKey: "sessionId") as! String? else { return nil } - guard let serverPublicKey: String = coder.decodeObject(forKey: "serverPublicKey") as! String? else { return nil } - - self.blindedId = blindedId - self.sessionId = sessionId - self.serverPublicKey = serverPublicKey - } - - public func encode(with coder: NSCoder) { - coder.encode(blindedId, forKey: "blindedId") - coder.encode(sessionId, forKey: "sessionId") - coder.encode(serverPublicKey, forKey: "serverPublicKey") - } -} diff --git a/SessionMessagingKit/Database/Migrations/_002_SetupStandardJobs.swift b/SessionMessagingKit/Database/Migrations/_002_SetupStandardJobs.swift index ac82f040a..b488653b8 100644 --- a/SessionMessagingKit/Database/Migrations/_002_SetupStandardJobs.swift +++ b/SessionMessagingKit/Database/Migrations/_002_SetupStandardJobs.swift @@ -48,11 +48,8 @@ enum _002_SetupStandardJobs: Migration { _ = try Job( variant: .garbageCollection, - behaviour: .recurringOnActive, - details: GarbageCollectionJob.Details( - typesToCollect: GarbageCollectionJob.Types.allCases - ) - )?.inserted(db) + behaviour: .recurringOnActive + ).inserted(db) } GRDBStorage.update(progress: 1, for: self, in: target) // In case this is the last migration diff --git a/SessionMessagingKit/Database/Migrations/_003_YDBToGRDBMigration.swift b/SessionMessagingKit/Database/Migrations/_003_YDBToGRDBMigration.swift index 0ef03a903..ec5db6db8 100644 --- a/SessionMessagingKit/Database/Migrations/_003_YDBToGRDBMigration.swift +++ b/SessionMessagingKit/Database/Migrations/_003_YDBToGRDBMigration.swift @@ -841,7 +841,6 @@ enum _003_YDBToGRDBMigration: Migration { } default: - // TODO: What message types have no body? SNLog("[Migration Error] Unsupported interaction type") throw StorageError.migrationFailed } @@ -926,7 +925,6 @@ enum _003_YDBToGRDBMigration: Migration { receivedMessageTimestamps.remove(legacyInteraction.timestamp) guard let interactionId: Int64 = interaction.id else { - // TODO: Is it possible the old database has duplicates which could hit this case? SNLog("[Migration Error] Failed to insert interaction") throw StorageError.migrationFailed } @@ -1072,13 +1070,9 @@ enum _003_YDBToGRDBMigration: Migration { // Note: The `legacyInteraction.timestamp` value is in milliseconds let timestamp: TimeInterval = LinkPreview.timestampFor(sentTimestampMs: Double(legacyInteraction.timestamp)) - guard linkPreview.imageAttachmentId == nil || attachments[linkPreview.imageAttachmentId ?? ""] != nil else { - // TODO: Is it possible to hit this case if a quoted attachment hasn't been downloaded? - SNLog("[Migration Error] Missing link preview attachment") - throw StorageError.migrationFailed - } - - // Setup the attachment and add it to the lookup (if it exists) + // Setup the attachment and add it to the lookup (if it exists - we do actually + // support link previews with no image attachments so no need to throw migration + // errors in those cases) let attachmentId: String? = try attachmentId( db, for: linkPreview.imageAttachmentId, @@ -1100,15 +1094,28 @@ enum _003_YDBToGRDBMigration: Migration { // Handle any attachments try attachmentIds.enumerated().forEach { index, legacyAttachmentId in - guard let attachmentId: String = try attachmentId( + let maybeAttachmentId: String? = (try attachmentId( db, for: legacyAttachmentId, interactionVariant: variant, attachments: attachments, processedAttachmentIds: &processedAttachmentIds - ) else { - SNLog("[Migration Error] Missing interaction attachment") -// throw StorageError.migrationFailed + )) + .defaulting( + // It looks like somehow messages could exist in the old database which + // referenced attachments but had no attachments in the database; doing + // nothing here results in these messages appearing as empty message + // bubbles so instead we want to insert invalid attachments instead + to: try invalidAttachmentId( + db, + for: legacyAttachmentId, + attachments: attachments, + processedAttachmentIds: &processedAttachmentIds + ) + ) + + guard let attachmentId: String = maybeAttachmentId else { + SNLog("[Migration Warning] Failed to create invalid attachment for missing attachment") return } @@ -1457,7 +1464,7 @@ enum _003_YDBToGRDBMigration: Migration { } guard let legacyAttachment: SMKLegacy._Attachment = attachments[legacyAttachmentId] else { - SNLog("[Migration Warning] Missing attachment - interaction will appear as blank") + SNLog("[Migration Warning] Missing attachment - interaction will show a \"failed\" attachment") return nil } @@ -1589,6 +1596,45 @@ enum _003_YDBToGRDBMigration: Migration { return legacyAttachmentId } + private static func invalidAttachmentId( + _ db: Database, + for legacyAttachmentId: String, + interactionVariant: Interaction.Variant? = nil, + attachments: [String: SMKLegacy._Attachment], + processedAttachmentIds: inout Set + ) throws -> String { + guard !processedAttachmentIds.contains(legacyAttachmentId) else { + return legacyAttachmentId + } + + _ = 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: nil, + variant: .standard, + state: .invalid, + contentType: "", + byteCount: 0, + creationTimestamp: Date().timeIntervalSince1970, + sourceFilename: nil, + downloadUrl: nil, + localRelativeFilePath: nil, + width: nil, + height: nil, + duration: nil, + isValid: false, + encryptionKey: nil, + digest: nil, + caption: nil + ).inserted(db) + + processedAttachmentIds.insert(legacyAttachmentId) + + return legacyAttachmentId + } + private static func mapLegacyTypesForNSKeyedUnarchiver() { NSKeyedUnarchiver.setClass( SMKLegacy._Thread.self, diff --git a/SessionMessagingKit/Database/Models/Attachment.swift b/SessionMessagingKit/Database/Models/Attachment.swift index c04ea6aef..d0201658a 100644 --- a/SessionMessagingKit/Database/Models/Attachment.swift +++ b/SessionMessagingKit/Database/Models/Attachment.swift @@ -56,6 +56,8 @@ public struct Attachment: Codable, Identifiable, Equatable, Hashable, FetchableR case failedUpload case uploading case uploaded + + case invalid = 100 } /// A unique identifier for the attachment @@ -939,6 +941,8 @@ extension Attachment { return } + let attachmentId: String = self.id + // If the attachment is a downloaded attachment, check if it came from the server // and if so just succeed immediately (no use re-uploading an attachment that is // already present on the server) - or if we want it to be encrypted and it's not @@ -956,16 +960,20 @@ extension Attachment { // Save the final upload info let uploadedAttachment: Attachment? = { guard let db: Database = db else { - return GRDBStorage.shared.write { db in - try? self - .with(state: .uploaded) - .saved(db) + GRDBStorage.shared.write { db in + try? Attachment + .filter(id: attachmentId) + .updateAll(db, Attachment.Columns.state.set(to: Attachment.State.uploaded)) } + + return self.with(state: .uploaded) } - return try? self - .with(state: .uploaded) - .saved(db) + _ = try? Attachment + .filter(id: attachmentId) + .updateAll(db, Attachment.Columns.state.set(to: Attachment.State.uploaded)) + + return self.with(state: .uploaded) }() guard uploadedAttachment != nil else { @@ -1008,16 +1016,20 @@ extension Attachment { // Update the attachment to the 'uploading' state let updatedAttachment: Attachment? = { guard let db: Database = db else { - return GRDBStorage.shared.write { db in - try? processedAttachment - .with(state: .uploading) - .saved(db) + GRDBStorage.shared.write { db in + try? Attachment + .filter(id: attachmentId) + .updateAll(db, Attachment.Columns.state.set(to: Attachment.State.uploading)) } + + return processedAttachment.with(state: .uploading) } - return try? processedAttachment - .with(state: .uploading) - .saved(db) + _ = try? Attachment + .filter(id: attachmentId) + .updateAll(db, Attachment.Columns.state.set(to: Attachment.State.uploading)) + + return processedAttachment.with(state: .uploading) }() guard updatedAttachment != nil else { @@ -1062,9 +1074,9 @@ extension Attachment { } .catch(on: queue) { error in GRDBStorage.shared.write { db in - try updatedAttachment? - .with(state: .failedUpload) - .saved(db) + try Attachment + .filter(id: attachmentId) + .updateAll(db, Attachment.Columns.state.set(to: Attachment.State.failedUpload)) } failure?(error) diff --git a/SessionMessagingKit/Database/Models/ClosedGroup.swift b/SessionMessagingKit/Database/Models/ClosedGroup.swift index dcd4b9e81..48be3511a 100644 --- a/SessionMessagingKit/Database/Models/ClosedGroup.swift +++ b/SessionMessagingKit/Database/Models/ClosedGroup.swift @@ -78,18 +78,6 @@ public struct ClosedGroup: Codable, Identifiable, FetchableRecord, PersistableRe } } -// MARK: - Mutation - -public extension ClosedGroup { - func with(name: String) -> ClosedGroup { - return ClosedGroup( - threadId: threadId, - name: name, - formationTimestamp: formationTimestamp - ) - } -} - // MARK: - GRDB Interactions public extension ClosedGroup { diff --git a/SessionMessagingKit/Database/Models/LinkPreview.swift b/SessionMessagingKit/Database/Models/LinkPreview.swift index 03cb18f66..6aea5fa3b 100644 --- a/SessionMessagingKit/Database/Models/LinkPreview.swift +++ b/SessionMessagingKit/Database/Models/LinkPreview.swift @@ -79,13 +79,8 @@ public struct LinkPreview: Codable, Equatable, Hashable, FetchableRecord, Persis public extension LinkPreview { init?(_ db: Database, proto: SNProtoDataMessage, body: String?, sentTimestampMs: TimeInterval) throws { guard let previewProto = proto.preview.first else { throw LinkPreviewError.noPreview } - guard proto.attachments.count < 1 else { throw LinkPreviewError.invalidInput } guard URL(string: previewProto.url) != nil else { throw LinkPreviewError.invalidInput } guard LinkPreview.isValidLinkUrl(previewProto.url) else { throw LinkPreviewError.invalidInput } - guard let body: String = body else { throw LinkPreviewError.invalidInput } - guard LinkPreview.allPreviewUrls(forMessageBodyText: body).contains(previewProto.url) else { - throw LinkPreviewError.invalidInput - } // Try to get an existing link preview first let timestamp: TimeInterval = LinkPreview.timestampFor(sentTimestampMs: sentTimestampMs) diff --git a/SessionMessagingKit/Database/Models/OpenGroup.swift b/SessionMessagingKit/Database/Models/OpenGroup.swift index 0ab4a96ce..da3b236cc 100644 --- a/SessionMessagingKit/Database/Models/OpenGroup.swift +++ b/SessionMessagingKit/Database/Models/OpenGroup.swift @@ -151,7 +151,7 @@ public extension OpenGroup { roomToken: roomToken, publicKey: publicKey, isActive: false, - name: "", + name: roomToken, // Default the name to the `roomToken` until we get retrieve the actual name roomDescription: nil, imageId: nil, imageData: nil, diff --git a/SessionMessagingKit/Database/Models/Profile.swift b/SessionMessagingKit/Database/Models/Profile.swift index 977eb4541..328de9fe6 100644 --- a/SessionMessagingKit/Database/Models/Profile.swift +++ b/SessionMessagingKit/Database/Models/Profile.swift @@ -200,7 +200,6 @@ public extension Profile { public extension Profile { func with( name: String? = nil, - nickname: Updatable = .existing, profilePictureUrl: Updatable = .existing, profilePictureFileName: Updatable = .existing, profileEncryptionKey: Updatable = .existing @@ -208,7 +207,7 @@ public extension Profile { return Profile( id: id, name: (name ?? self.name), - nickname: (nickname ?? self.nickname), + nickname: self.nickname, profilePictureUrl: (profilePictureUrl ?? self.profilePictureUrl), profilePictureFileName: (profilePictureFileName ?? self.profilePictureFileName), profileEncryptionKey: (profileEncryptionKey ?? self.profileEncryptionKey) @@ -406,9 +405,9 @@ public class SMKProfile: NSObject { let profile: Profile = Profile.fetchOrCreate(id: profileId) let targetNickname: String? = ((nickname ?? "").count > 0 ? nickname : nil) - _ = try profile - .with(nickname: .update(targetNickname)) - .saved(db) + try Profile + .filter(id: profile.id) + .updateAll(db, Profile.Columns.nickname.set(to: targetNickname)) return (targetNickname ?? profile.name) } diff --git a/SessionMessagingKit/Jobs/Types/AttachmentDownloadJob.swift b/SessionMessagingKit/Jobs/Types/AttachmentDownloadJob.swift index 2c9755347..9fa244677 100644 --- a/SessionMessagingKit/Jobs/Types/AttachmentDownloadJob.swift +++ b/SessionMessagingKit/Jobs/Types/AttachmentDownloadJob.swift @@ -7,7 +7,7 @@ import SessionSnodeKit import SignalCoreKit public enum AttachmentDownloadJob: JobExecutor { - public static var maxFailureCount: Int = 10 + public static var maxFailureCount: Int = 3 public static var requiresThreadId: Bool = true public static let requiresInteractionId: Bool = true @@ -30,13 +30,50 @@ public enum AttachmentDownloadJob: JobExecutor { } // Due to the complex nature of jobs and how attachments can be reused it's possible for - // and AttachmentDownloadJob to get created for an attachment which has already been + // an AttachmentDownloadJob to get created for an attachment which has already been // downloaded/uploaded so in those cases just succeed immediately guard attachment.state != .downloaded && attachment.state != .uploaded else { success(job, false) return } + // If we ever make attachment downloads concurrent this will prevent us from downloading + // the same attachment multiple times at the same time (it also adds a "clean up" mechanism + // if an attachment ends up stuck in a "downloading" state incorrectly + guard attachment.state != .downloading else { + let otherCurrentJobAttachmentIds: Set = JobRunner + .defailsForCurrentlyRunningJobs(of: .attachmentDownload) + .filter { key, _ in key != job.id } + .values + .compactMap { data -> String? in + guard let data: Data = data else { return nil } + + return (try? JSONDecoder().decode(Details.self, from: data))? + .attachmentId + } + .asSet() + + // If there isn't another currently running attachmentDownload job downloading this attachment + // then we should update the state of the attachment to be failed to avoid having attachments + // appear in an endlessly downloading state + if !otherCurrentJobAttachmentIds.contains(attachment.id) { + GRDBStorage.shared.write { db in + _ = try Attachment + .filter(id: attachment.id) + .updateAll(db, Attachment.Columns.state.set(to: Attachment.State.failedDownload)) + } + } + + // Note: The only ways we should be able to get into this state are if we enable concurrent + // downloads or if the app was closed/crashed while an attachmentDownload job was in progress + // + // If there is another current job then just fail this one permanently, otherwise let it + // retry (if there are more retry attempts available) and in the next retry it's state should + // be 'failedDownload' so we won't get stuck in a loop + failure(job, nil, otherCurrentJobAttachmentIds.contains(attachment.id)) + return + } + // Update to the 'downloading' state (no need to update the 'attachment' instance) GRDBStorage.shared.write { db in try Attachment @@ -123,25 +160,43 @@ public enum AttachmentDownloadJob: JobExecutor { .catch(on: queue) { error in OWSFileSystem.deleteFile(temporaryFileUrl.path) + let targetState: Attachment.State + let permanentFailure: Bool + switch error { - case OnionRequestAPIError.httpRequestFailedAtDestination(let statusCode, _, _) where statusCode == 400: - /// Otherwise, the attachment will show a state of downloading forever, and the message - /// won't be able to be marked as read - /// - /// **Note:** We **MUST** use the `'with()` function here as it will update the - /// `isValid` and `duration` values based on the downloaded data and the state - GRDBStorage.shared.write { db in - _ = try attachment - .with(state: .failedDownload) - .saved(db) - } - - // This usually indicates a file that has expired on the server, so there's no need to retry - failure(job, error, true) + /// If we get a 404 then we got a successful response from the server but the attachment doesn't + /// exist, in this case update the attachment to an "invalid" state so the user doesn't get stuck in + /// a retry download loop + case OnionRequestAPIError.httpRequestFailedAtDestination(let statusCode, _, _) where statusCode == 404: + targetState = .invalid + permanentFailure = true + case OnionRequestAPIError.httpRequestFailedAtDestination(let statusCode, _, _) where statusCode == 400 || statusCode == 401: + /// If we got a 400 or a 401 then we want to fail the download in a way that has to be manually retried as it's + /// likely something else is going on that caused the failure + targetState = .failedDownload + permanentFailure = true + + /// For any other error it's likely either the server is down or something weird just happened with the request + /// so we want to automatically retry default: - failure(job, error, false) + targetState = .failedDownload + permanentFailure = false } + + /// To prevent the attachment from showing a state of downloading forever, we need to update the attachment + /// state here based on the type of error that occurred + /// + /// **Note:** We **MUST** use the `'with()` function here as it will update the + /// `isValid` and `duration` values based on the downloaded data and the state + GRDBStorage.shared.write { db in + _ = try Attachment + .filter(id: attachment.id) + .updateAll(db, Attachment.Columns.state.set(to: targetState)) + } + + /// Trigger the failure and provide the `permanentFailure` value defined above + failure(job, error, permanentFailure) } } } diff --git a/SessionMessagingKit/Jobs/Types/GarbageCollectionJob.swift b/SessionMessagingKit/Jobs/Types/GarbageCollectionJob.swift index 78225cf0c..dd89ee21b 100644 --- a/SessionMessagingKit/Jobs/Types/GarbageCollectionJob.swift +++ b/SessionMessagingKit/Jobs/Types/GarbageCollectionJob.swift @@ -7,6 +7,10 @@ import SignalCoreKit import SessionUtilitiesKit import SessionSnodeKit +/// This job deletes unused and orphaned data from the database as well as orphaned files from device storage +/// +/// **Note:** When sheduling this job if no `Details` are provided (with a list of `typesToCollect`) then this job will +/// assume that it should be collecting all `Types` public enum GarbageCollectionJob: JobExecutor { public static var maxFailureCount: Int = -1 public static var requiresThreadId: Bool = false @@ -20,39 +24,33 @@ public enum GarbageCollectionJob: JobExecutor { failure: @escaping (Job, Error?, Bool) -> (), deferred: @escaping (Job) -> () ) { - guard - let detailsData: Data = job.details, - let details: Details = try? JSONDecoder().decode(Details.self, from: detailsData) - else { - failure(job, JobRunnerError.missingRequiredDetails, false) - return - } - - // If there are no types to collect then complete the job (and never run again - it doesn't do anything) - guard !details.typesToCollect.isEmpty else { - success(job, true) - return - } - + /// Determine what types of data we want to collect (if we didn't provide any then assume we want to collect everything) + /// + /// **Note:** The reason we default to handle all cases (instead of just doing nothing in that case) is so the initial registration + /// of the garbageCollection job never needs to be updated as we continue to add more types going forward + let typesToCollect: [Types] = (job.details + .map { try? JSONDecoder().decode(Details.self, from: $0) }? + .typesToCollect) + .defaulting(to: Types.allCases) let timestampNow: TimeInterval = Date().timeIntervalSince1970 GRDBStorage.shared.writeAsync( updates: { db in /// Remove any expired controlMessageProcessRecords - if details.typesToCollect.contains(.expiredControlMessageProcessRecords) { + if typesToCollect.contains(.expiredControlMessageProcessRecords) { _ = try ControlMessageProcessRecord .filter(ControlMessageProcessRecord.Columns.serverExpirationTimestamp <= timestampNow) .deleteAll(db) } /// Remove any typing indicators - if details.typesToCollect.contains(.threadTypingIndicators) { + if typesToCollect.contains(.threadTypingIndicators) { _ = try ThreadTypingIndicator .deleteAll(db) } /// Remove any old open group messages - open group messages which are older than six months - if details.typesToCollect.contains(.oldOpenGroupMessages) && db[.trimOpenGroupMessagesOlderThanSixMonths] { + if typesToCollect.contains(.oldOpenGroupMessages) && db[.trimOpenGroupMessagesOlderThanSixMonths] { let interaction: TypedTableAlias = TypedTableAlias() let thread: TypedTableAlias = TypedTableAlias() @@ -71,7 +69,7 @@ public enum GarbageCollectionJob: JobExecutor { } /// Orphaned jobs - jobs which have had their threads or interactions removed - if details.typesToCollect.contains(.orphanedJobs) { + if typesToCollect.contains(.orphanedJobs) { let job: TypedTableAlias = TypedTableAlias() let thread: TypedTableAlias = TypedTableAlias() let interaction: TypedTableAlias = TypedTableAlias() @@ -97,7 +95,7 @@ public enum GarbageCollectionJob: JobExecutor { } /// Orphaned link previews - link previews which have no interactions with matching url & rounded timestamps - if details.typesToCollect.contains(.orphanedLinkPreviews) { + if typesToCollect.contains(.orphanedLinkPreviews) { let linkPreview: TypedTableAlias = TypedTableAlias() let interaction: TypedTableAlias = TypedTableAlias() @@ -117,7 +115,7 @@ public enum GarbageCollectionJob: JobExecutor { /// Orphaned open groups - open groups which are no longer associated to a thread (except for the session-run ones for which /// we want cached image data even if the user isn't in the group) - if details.typesToCollect.contains(.orphanedOpenGroups) { + if typesToCollect.contains(.orphanedOpenGroups) { let openGroup: TypedTableAlias = TypedTableAlias() let thread: TypedTableAlias = TypedTableAlias() @@ -136,7 +134,7 @@ public enum GarbageCollectionJob: JobExecutor { } /// Orphaned open group capabilities - capabilities which have no existing open groups with the same server - if details.typesToCollect.contains(.orphanedOpenGroupCapabilities) { + if typesToCollect.contains(.orphanedOpenGroupCapabilities) { let capability: TypedTableAlias = TypedTableAlias() let openGroup: TypedTableAlias = TypedTableAlias() @@ -152,7 +150,7 @@ public enum GarbageCollectionJob: JobExecutor { } /// Orphaned blinded id lookups - lookups which have no existing threads or approval/block settings for either blinded/un-blinded id - if details.typesToCollect.contains(.orphanedBlindedIdLookups) { + if typesToCollect.contains(.orphanedBlindedIdLookups) { let blindedIdLookup: TypedTableAlias = TypedTableAlias() let thread: TypedTableAlias = TypedTableAlias() let contact: TypedTableAlias = TypedTableAlias() @@ -178,8 +176,28 @@ public enum GarbageCollectionJob: JobExecutor { """) } + /// Approved blinded contact records - once a blinded contact has been approved there is no need to keep the blinded + /// contact record around anymore + if typesToCollect.contains(.approvedBlindedContactRecords) { + let contact: TypedTableAlias = TypedTableAlias() + let blindedIdLookup: TypedTableAlias = TypedTableAlias() + + try db.execute(literal: """ + DELETE FROM \(Contact.self) + WHERE \(Column.rowID) IN ( + SELECT \(contact.alias[Column.rowID]) + FROM \(Contact.self) + LEFT JOIN \(BlindedIdLookup.self) ON ( + \(blindedIdLookup[.blindedId]) = \(contact[.id]) AND + \(blindedIdLookup[.sessionId]) IS NOT NULL + ) + WHERE \(blindedIdLookup[.sessionId]) IS NOT NULL + ) + """) + } + /// Orphaned attachments - attachments which have no related interactions, quotes or link previews - if details.typesToCollect.contains(.orphanedAttachments) { + if typesToCollect.contains(.orphanedAttachments) { let attachment: TypedTableAlias = TypedTableAlias() let quote: TypedTableAlias = TypedTableAlias() let linkPreview: TypedTableAlias = TypedTableAlias() @@ -216,7 +234,7 @@ public enum GarbageCollectionJob: JobExecutor { var profileAvatarFilenames: Set = [] /// Orphaned attachment files - attachment files which don't have an associated record in the database - if details.typesToCollect.contains(.orphanedAttachmentFiles) { + if typesToCollect.contains(.orphanedAttachmentFiles) { /// **Note:** Thumbnails are stored in the `NSCachesDirectory` directory which should be automatically manage /// it's own garbage collection so we can just ignore it according to the various comments in the following stack overflow /// post, the directory will be cleared during app updates as well as if the system is running low on memory (if the app isn't running) @@ -229,7 +247,7 @@ public enum GarbageCollectionJob: JobExecutor { } /// Orphaned profile avatar files - profile avatar files which don't have an associated record in the database - if details.typesToCollect.contains(.orphanedProfileAvatars) { + if typesToCollect.contains(.orphanedProfileAvatars) { profileAvatarFilenames = try Profile .select(.profilePictureFileName) .filter(Profile.Columns.profilePictureFileName != nil) @@ -252,7 +270,7 @@ public enum GarbageCollectionJob: JobExecutor { var deletionErrors: [Error] = [] // Orphaned attachment files (actual deletion) - if details.typesToCollect.contains(.orphanedAttachmentFiles) { + if typesToCollect.contains(.orphanedAttachmentFiles) { // Note: Looks like in order to recursively look through files we need to use the // enumerator method let fileEnumerator = FileManager.default.enumerator( @@ -294,7 +312,7 @@ public enum GarbageCollectionJob: JobExecutor { } // Orphaned profile avatar files (actual deletion) - if details.typesToCollect.contains(.orphanedProfileAvatars) { + if typesToCollect.contains(.orphanedProfileAvatars) { let allAvatarProfileFilenames: Set = (try? FileManager.default .contentsOfDirectory(atPath: ProfileManager.sharedDataProfileAvatarsDirPath)) .defaulting(to: []) @@ -339,6 +357,7 @@ extension GarbageCollectionJob { case orphanedOpenGroups case orphanedOpenGroupCapabilities case orphanedBlindedIdLookups + case approvedBlindedContactRecords case orphanedAttachments case orphanedAttachmentFiles case orphanedProfileAvatars diff --git a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+ClosedGroups.swift b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+ClosedGroups.swift index d398e4b47..a9e24c8f7 100644 --- a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+ClosedGroups.swift +++ b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+ClosedGroups.swift @@ -212,9 +212,9 @@ extension MessageReceiver { guard case let .nameChange(name) = message.kind else { return } try performIfValid(db, message: message) { id, sender, thread, closedGroup in - try closedGroup - .with(name: name) - .save(db) + _ = try ClosedGroup + .filter(id: id) + .updateAll(db, ClosedGroup.Columns.name.set(to: name)) // Notify the user if needed guard name != closedGroup.name else { return } diff --git a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+VisibleMessages.swift b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+VisibleMessages.swift index 97806cff0..4692851d9 100644 --- a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+VisibleMessages.swift +++ b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+VisibleMessages.swift @@ -204,20 +204,20 @@ extension MessageReceiver { message.attachmentIds = attachments.map { $0.id } // Persist quote if needed - let quote: Quote? = try? Quote( + try? Quote( db, proto: dataMessage, interactionId: interactionId, thread: thread - )?.inserted(db) + )?.insert(db) // Parse link preview if needed - let linkPreview: LinkPreview? = try? LinkPreview( + try? LinkPreview( db, proto: dataMessage, body: message.text, sentTimestampMs: (messageSentTimestamp * 1000) - )?.saved(db) + )?.save(db) // Open group invitations are stored as LinkPreview values so create one if needed if @@ -232,30 +232,6 @@ extension MessageReceiver { ).save(db) } - // Start attachment downloads if needed (ie. trusted contact or group thread) - let isContactTrusted: Bool = ((try? Contact.fetchOne(db, id: sender))?.isTrusted ?? false) - - if isContactTrusted || thread.variant != .contact { - attachments - .map { $0.id } - .appending(quote?.attachmentId) - .appending(linkPreview?.attachmentId) - .forEach { attachmentId in - JobRunner.add( - db, - job: Job( - variant: .attachmentDownload, - threadId: thread.id, - interactionId: interactionId, - details: AttachmentDownloadJob.Details( - attachmentId: attachmentId - ) - ), - canStartJob: isMainAppActive - ) - } - } - // Cancel any typing indicators if needed if isMainAppActive { TypingIndicators.didStopTyping(db, threadId: thread.id, direction: .incoming) diff --git a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageSender+ClosedGroups.swift b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageSender+ClosedGroups.swift index d9332c814..8bf2d87dd 100644 --- a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageSender+ClosedGroups.swift +++ b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageSender+ClosedGroups.swift @@ -216,8 +216,9 @@ extension MessageSender { // Update name if needed if name != closedGroup.name { // Update the group - let updatedClosedGroup: ClosedGroup = closedGroup.with(name: name) - try updatedClosedGroup.save(db) + _ = try ClosedGroup + .filter(id: closedGroup.id) + .updateAll(db, ClosedGroup.Columns.name.set(to: name)) // Notify the user let interaction: Interaction = try Interaction( diff --git a/SessionMessagingKit/Sending & Receiving/MessageSender+Convenience.swift b/SessionMessagingKit/Sending & Receiving/MessageSender+Convenience.swift index 0c5ceeb6b..bde98e927 100644 --- a/SessionMessagingKit/Sending & Receiving/MessageSender+Convenience.swift +++ b/SessionMessagingKit/Sending & Receiving/MessageSender+Convenience.swift @@ -165,6 +165,7 @@ extension MessageSender { } if let error: Error = errors.first { return Promise(error: error) } + return GRDBStorage.shared.writeAsync { db in try MessageSender.sendImmediate( db, diff --git a/SessionMessagingKit/Sending & Receiving/MessageSender.swift b/SessionMessagingKit/Sending & Receiving/MessageSender.swift index f470754c0..dde2ee13e 100644 --- a/SessionMessagingKit/Sending & Receiving/MessageSender.swift +++ b/SessionMessagingKit/Sending & Receiving/MessageSender.swift @@ -646,21 +646,15 @@ public final class MessageSender { with error: MessageSenderError, interactionId: Int64? ) { - guard let interaction: Interaction = try? interaction(db, for: message, interactionId: interactionId) else { - return - } - // Mark any "sending" recipients as "failed" - try? interaction.recipientStates - .fetchAll(db) - .forEach { oldState in - guard oldState.state == .sending else { return } - - try? oldState.with( - state: .failed, - mostRecentFailureText: error.localizedDescription - ).save(db) - } + _ = try? RecipientState + .filter(RecipientState.Columns.interactionId == interactionId) + .filter(RecipientState.Columns.state == RecipientState.State.sending) + .updateAll( + db, + RecipientState.Columns.state.set(to: RecipientState.State.failed), + RecipientState.Columns.mostRecentFailureText.set(to: error.localizedDescription) + ) } // MARK: - Convenience diff --git a/SessionMessagingKit/Utilities/ProfileManager.swift b/SessionMessagingKit/Utilities/ProfileManager.swift index 992fdaaa6..4604cf104 100644 --- a/SessionMessagingKit/Utilities/ProfileManager.swift +++ b/SessionMessagingKit/Utilities/ProfileManager.swift @@ -180,9 +180,9 @@ public struct ProfileManager { return } - try? latestProfile - .with(profilePictureFileName: .update(fileName)) - .update(db) + _ = try? Profile + .filter(id: profile.id) + .updateAll(db, Profile.Columns.profilePictureFileName.set(to: fileName)) profileAvatarCache.mutate { $0[fileName] = image } } diff --git a/SessionMessagingKitTests/Contacts/BlindedIdLookupSpec.swift b/SessionMessagingKitTests/Contacts/BlindedIdLookupSpec.swift new file mode 100644 index 000000000..4cb4b4cba --- /dev/null +++ b/SessionMessagingKitTests/Contacts/BlindedIdLookupSpec.swift @@ -0,0 +1,32 @@ +// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. + +import Foundation + +import Quick +import Nimble + +@testable import SessionMessagingKit + +class BlindedIdLookupSpec: QuickSpec { + // MARK: - Spec + + override func spec() { + describe("a BlindedIdLookup") { + context("when initializing") { + it("sets the values correctly") { + let lookup: BlindedIdLookup = BlindedIdLookup( + blindedId: "testBlindedId", + sessionId: "testSessionId", + openGroupServer: "testServer", + openGroupPublicKey: "testPublicKey" + ) + + expect(lookup.blindedId).to(equal("testBlindedId")) + expect(lookup.sessionId).to(equal("testSessionId")) + expect(lookup.openGroupServer).to(equal("testServer")) + expect(lookup.openGroupPublicKey).to(equal("testPublicKey")) + } + } + } + } +} diff --git a/SessionMessagingKitTests/Contacts/BlindedIdMappingSpec.swift b/SessionMessagingKitTests/Contacts/BlindedIdMappingSpec.swift deleted file mode 100644 index 3c2c26d40..000000000 --- a/SessionMessagingKitTests/Contacts/BlindedIdMappingSpec.swift +++ /dev/null @@ -1,48 +0,0 @@ -// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. - -import Foundation - -import Quick -import Nimble - -@testable import SessionMessagingKit - -class BlindedIdMappingSpec: QuickSpec { - // MARK: - Spec - - override func spec() { - describe("a BlindedIdMapping") { - context("when initializing") { - it("sets the values correctly") { - let mapping: BlindedIdMapping = BlindedIdMapping( - blindedId: "testBlindedId", - sessionId: "testSessionId", - serverPublicKey: "testPublicKey" - ) - - expect(mapping.blindedId).to(equal("testBlindedId")) - expect(mapping.sessionId).to(equal("testSessionId")) - expect(mapping.serverPublicKey).to(equal("testPublicKey")) - } - } - - context("when NSCoding") { - // Note: Unit testing NSCoder is horrible so we won't do it properly - wait until we refactor it to Codable - it("successfully encodes and decodes") { - let mappingToEncode: BlindedIdMapping = BlindedIdMapping( - blindedId: "testBlindedId", - sessionId: "testSessionId", - serverPublicKey: "testPublicKey" - ) - let encodedData: Data = try! NSKeyedArchiver.archivedData(withRootObject: mappingToEncode, requiringSecureCoding: false) - let mapping: BlindedIdMapping? = try? NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(encodedData) as? BlindedIdMapping - - expect(mapping).toNot(beNil()) - expect(mapping?.blindedId).to(equal("testBlindedId")) - expect(mapping?.sessionId).to(equal("testSessionId")) - expect(mapping?.serverPublicKey).to(equal("testPublicKey")) - } - } - } - } -} diff --git a/SessionUtilitiesKit/General/Dictionary+Utilities.swift b/SessionUtilitiesKit/General/Dictionary+Utilities.swift index 1b3c918f9..5ac19dcd2 100644 --- a/SessionUtilitiesKit/General/Dictionary+Utilities.swift +++ b/SessionUtilitiesKit/General/Dictionary+Utilities.swift @@ -28,7 +28,9 @@ public extension Dictionary.Values { // MARK: - Functional Convenience public extension Dictionary { - func setting(_ key: Key, _ value: Value?) -> [Key: Value] { + func setting(_ key: Key?, _ value: Value?) -> [Key: Value] { + guard let key: Key = key else { return self } + var updatedDictionary: [Key: Value] = self updatedDictionary[key] = value @@ -45,7 +47,9 @@ public extension Dictionary { return updatedDictionary } - func removingValue(forKey key: Key) -> [Key: Value] { + func removingValue(forKey key: Key?) -> [Key: Value] { + guard let key: Key = key else { return self } + var updatedDictionary: [Key: Value] = self updatedDictionary.removeValue(forKey: key) diff --git a/SessionUtilitiesKit/JobRunner/JobRunner.swift b/SessionUtilitiesKit/JobRunner/JobRunner.swift index 2ab114973..2e6055d93 100644 --- a/SessionUtilitiesKit/JobRunner/JobRunner.swift +++ b/SessionUtilitiesKit/JobRunner/JobRunner.swift @@ -285,6 +285,11 @@ public final class JobRunner { return (queues.wrappedValue[job.variant]?.isCurrentlyRunning(jobId) == true) } + public static func defailsForCurrentlyRunningJobs(of variant: Job.Variant) -> [Int64: Data?] { + return (queues.wrappedValue[variant]?.detailsForAllCurrentlyRunningJobs()) + .defaulting(to: [:]) + } + public static func hasPendingOrRunningJob(with variant: Job.Variant, details: T) -> Bool { guard let targetQueue: JobQueue = queues.wrappedValue[variant] else { return false } guard let detailsData: Data = try? JSONEncoder().encode(details) else { return false } @@ -396,6 +401,7 @@ private final class JobQueue { fileprivate var isRunning: Atomic = Atomic(false) private var queue: Atomic<[Job]> = Atomic([]) private var jobsCurrentlyRunning: Atomic> = Atomic([]) + private var detailsForCurrentlyRunningJobs: Atomic<[Int64: Data?]> = Atomic([:]) fileprivate var hasPendingJobs: Bool { !queue.wrappedValue.isEmpty } @@ -505,6 +511,10 @@ private final class JobQueue { return jobsCurrentlyRunning.wrappedValue.contains(jobId) } + fileprivate func detailsForAllCurrentlyRunningJobs() -> [Int64: Data?] { + return detailsForCurrentlyRunningJobs.wrappedValue + } + fileprivate func hasPendingOrRunningJob(with detailsData: Data?) -> Bool { let pendingJobs: [Job] = queue.wrappedValue @@ -683,6 +693,7 @@ private final class JobQueue { jobsCurrentlyRunning = jobsCurrentlyRunning.inserting(nextJob.id) numJobsRunning = jobsCurrentlyRunning.count } + detailsForCurrentlyRunningJobs.mutate { $0 = $0.setting(nextJob.id, nextJob.details) } SNLog("[JobRunner] \(queueContext) started job (\(executionType == .concurrent ? "\(numJobsRunning) currently running, " : "")\(numJobsRemaining) remaining)") jobExecutor.run( @@ -817,6 +828,7 @@ private final class JobQueue { // The job is removed from the queue before it runs so all we need to to is remove it // from the 'currentlyRunning' set and start the next one jobsCurrentlyRunning.mutate { $0 = $0.removing(job.id) } + detailsForCurrentlyRunningJobs.mutate { $0 = $0.removingValue(forKey: job.id) } internalQueue.async { [weak self] in self?.runNextJob() } @@ -828,6 +840,7 @@ private final class JobQueue { guard GRDBStorage.shared.read({ db in try Job.exists(db, id: job.id ?? -1) }) == true else { SNLog("[JobRunner] \(queueContext) \(job.variant) job canceled") jobsCurrentlyRunning.mutate { $0 = $0.removing(job.id) } + detailsForCurrentlyRunningJobs.mutate { $0 = $0.removingValue(forKey: job.id) } internalQueue.async { [weak self] in self?.runNextJob() @@ -839,6 +852,7 @@ private final class JobQueue { if self.type == .blocking && job.shouldBlockFirstRunEachSession { SNLog("[JobRunner] \(queueContext) \(job.variant) job failed; retrying immediately") jobsCurrentlyRunning.mutate { $0 = $0.removing(job.id) } + detailsForCurrentlyRunningJobs.mutate { $0 = $0.removingValue(forKey: job.id) } queue.mutate { $0.insert(job, at: 0) } internalQueue.async { [weak self] in @@ -915,6 +929,7 @@ private final class JobQueue { } jobsCurrentlyRunning.mutate { $0 = $0.removing(job.id) } + detailsForCurrentlyRunningJobs.mutate { $0 = $0.removingValue(forKey: job.id) } internalQueue.async { [weak self] in self?.runNextJob() } @@ -924,6 +939,7 @@ private final class JobQueue { /// on other jobs, and it should automatically manage those dependencies) private func handleJobDeferred(_ job: Job) { jobsCurrentlyRunning.mutate { $0 = $0.removing(job.id) } + detailsForCurrentlyRunningJobs.mutate { $0 = $0.removingValue(forKey: job.id) } internalQueue.async { [weak self] in self?.runNextJob() }