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
This commit is contained in:
Morgan Pretty 2022-07-01 12:52:41 +10:00
parent f2bd72b3ae
commit eb0118ac10
49 changed files with 377 additions and 302 deletions

View File

@ -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 = "<group>"; };
FD09C5E9282A1BB2000CE219 /* ThreadTypingIndicator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadTypingIndicator.swift; sourceTree = "<group>"; };
FD09C5EB282B8F17000CE219 /* AttachmentError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttachmentError.swift; sourceTree = "<group>"; };
FD0BA51A27CD88EC00CC6805 /* BlindedIdMapping.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlindedIdMapping.swift; sourceTree = "<group>"; };
FD17D79527F3E04600122BE0 /* _001_InitialSetupMigration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _001_InitialSetupMigration.swift; sourceTree = "<group>"; };
FD17D79827F40AB800122BE0 /* _003_YDBToGRDBMigration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _003_YDBToGRDBMigration.swift; sourceTree = "<group>"; };
FD17D79B27F40B2E00122BE0 /* SMKLegacy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SMKLegacy.swift; sourceTree = "<group>"; };
@ -1694,7 +1692,7 @@
FD3C905F27E410F700CD579F /* FileUploadResponseSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileUploadResponseSpec.swift; sourceTree = "<group>"; };
FD3C906127E411AF00CD579F /* HeaderSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HeaderSpec.swift; sourceTree = "<group>"; };
FD3C906327E4122F00CD579F /* RequestSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RequestSpec.swift; sourceTree = "<group>"; };
FD3C906627E416AF00CD579F /* BlindedIdMappingSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlindedIdMappingSpec.swift; sourceTree = "<group>"; };
FD3C906627E416AF00CD579F /* BlindedIdLookupSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlindedIdLookupSpec.swift; sourceTree = "<group>"; };
FD3C906927E417CE00CD579F /* SodiumUtilitiesSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SodiumUtilitiesSpec.swift; sourceTree = "<group>"; };
FD3C906C27E43C4B00CD579F /* MessageSenderEncryptionSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageSenderEncryptionSpec.swift; sourceTree = "<group>"; };
FD3C906E27E43E8700CD579F /* MockBox.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockBox.swift; sourceTree = "<group>"; };
@ -2424,14 +2422,6 @@
path = General;
sourceTree = "<group>";
};
B8B3201F258B1A540020074B /* Contacts */ = {
isa = PBXGroup;
children = (
FD0BA51A27CD88EC00CC6805 /* BlindedIdMapping.swift */,
);
path = Contacts;
sourceTree = "<group>";
};
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 = "<group>";
@ -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)",

View File

@ -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

View File

@ -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<String> = []
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 [

View File

@ -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
}
}

View File

@ -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)
}

View File

@ -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 {

View File

@ -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";

View File

@ -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";

View File

@ -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";

View File

@ -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";

View File

@ -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";

View File

@ -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";

View File

@ -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";

View File

@ -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";

View File

@ -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";

View File

@ -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";

View File

@ -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" = "メッセージ";

View File

@ -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";

View File

@ -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";

View File

@ -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";

View File

@ -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" = "Сообщения";

View File

@ -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";

View File

@ -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";

View File

@ -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";

View File

@ -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";

View File

@ -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";

View File

@ -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";

View File

@ -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" = "消息";

View File

@ -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"

View File

@ -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")
}
}

View File

@ -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

View File

@ -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<String>
) 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,

View File

@ -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)

View File

@ -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 {

View File

@ -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)

View File

@ -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,

View File

@ -200,7 +200,6 @@ public extension Profile {
public extension Profile {
func with(
name: String? = nil,
nickname: Updatable<String?> = .existing,
profilePictureUrl: Updatable<String?> = .existing,
profilePictureFileName: Updatable<String?> = .existing,
profileEncryptionKey: Updatable<OWSAES256Key> = .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)
}

View File

@ -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<String> = 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)
}
}
}

View File

@ -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<Interaction> = TypedTableAlias()
let thread: TypedTableAlias<SessionThread> = 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<Job> = TypedTableAlias()
let thread: TypedTableAlias<SessionThread> = TypedTableAlias()
let interaction: TypedTableAlias<Interaction> = 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<LinkPreview> = TypedTableAlias()
let interaction: TypedTableAlias<Interaction> = 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<OpenGroup> = TypedTableAlias()
let thread: TypedTableAlias<SessionThread> = 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<Capability> = TypedTableAlias()
let openGroup: TypedTableAlias<OpenGroup> = 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<BlindedIdLookup> = TypedTableAlias()
let thread: TypedTableAlias<SessionThread> = TypedTableAlias()
let contact: TypedTableAlias<Contact> = 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<Contact> = TypedTableAlias()
let blindedIdLookup: TypedTableAlias<BlindedIdLookup> = 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<Attachment> = TypedTableAlias()
let quote: TypedTableAlias<Quote> = TypedTableAlias()
let linkPreview: TypedTableAlias<LinkPreview> = TypedTableAlias()
@ -216,7 +234,7 @@ public enum GarbageCollectionJob: JobExecutor {
var profileAvatarFilenames: Set<String> = []
/// 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<String> = (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

View File

@ -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 }

View File

@ -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)

View File

@ -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(

View File

@ -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,

View File

@ -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

View File

@ -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 }
}

View File

@ -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"))
}
}
}
}
}

View File

@ -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"))
}
}
}
}
}

View File

@ -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)

View File

@ -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<T: Encodable>(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<Bool> = Atomic(false)
private var queue: Atomic<[Job]> = Atomic([])
private var jobsCurrentlyRunning: Atomic<Set<Int64>> = 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()
}