From 5bcc124388181fa5bcbeeb3eb722426ff8b9507a Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Sun, 15 May 2022 14:39:21 +1000 Subject: [PATCH] Updated the SessionShareExtension to work with GRDB Updated to the latest version of GRDB Fixed an issue with db reentrant behaviour with the Attachment upload function Finished up the updated 'sendNonDurability' functions --- Podfile | 1 + Podfile.lock | 6 +- Session.xcodeproj/project.pbxproj | 4 + .../ConversationVC+Interaction.swift | 25 +- Session/Home/HomeVC.swift | 10 +- Session/Notifications/AppNotifications.swift | 6 +- .../Database/Models/Attachment.swift | 19 +- .../Database/Models/Interaction.swift | 57 ++++ .../Models/InteractionAttachment.swift | 10 + .../Jobs/Types/AttachmentUploadJob.swift | 27 +- .../ClosedGroupControlMessage.swift | 8 +- .../MessageSender+ClosedGroups.swift | 6 +- .../MessageSender+Convenience.swift | 153 ++++++----- SessionShareExtension/ShareVC.swift | 7 + .../SimplifiedConversationCell.swift | 54 ++-- SessionShareExtension/ThreadPickerVC.swift | 253 +++++++++++------- .../ThreadPickerViewModel.swift | 192 +++++++++++++ SessionUtilitiesKit/JobRunner/JobRunner.swift | 6 +- .../AttachmentApprovalViewController.swift | 10 +- ...ModalActivityIndicatorViewController.swift | 9 +- SignalUtilitiesKit/Utilities/AppSetup.m | 4 +- .../Utilities/UIViewController+OWS.m | 35 +-- 22 files changed, 619 insertions(+), 283 deletions(-) create mode 100644 SessionShareExtension/ThreadPickerViewModel.swift diff --git a/Podfile b/Podfile index 72e34dc1c..8e9c238cc 100644 --- a/Podfile +++ b/Podfile @@ -42,6 +42,7 @@ abstract_target 'GlobalDependencies' do target 'SessionShareExtension' do pod 'NVActivityIndicatorView' + pod 'DifferenceKit' end target 'SignalUtilitiesKit' do diff --git a/Podfile.lock b/Podfile.lock index 81d738229..aa728eb01 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -27,7 +27,7 @@ PODS: - DifferenceKit/Core (1.2.0) - DifferenceKit/UIKitExtension (1.2.0): - DifferenceKit/Core - - GRDB.swift/SQLCipher (5.23.0): + - GRDB.swift/SQLCipher (5.24.0): - SQLCipher (>= 3.4.0) - Mantle (2.1.0): - Mantle/extobjc (= 2.1.0) @@ -203,7 +203,7 @@ SPEC CHECKSUMS: CryptoSwift: a532e74ed010f8c95f611d00b8bbae42e9fe7c17 Curve25519Kit: e63f9859ede02438ae3defc5e1a87e09d1ec7ee6 DifferenceKit: 5659c430bb7fe45876fa32ce5cba5d6167f0c805 - GRDB.swift: e4a950fe99d113ea5d24571d49eaae0062303c14 + GRDB.swift: 7ecc8799aaa97cf1fbbcfa9d75821aa920cb713f Mantle: 2fa750afa478cd625a94230fbf1c13462f29395b NVActivityIndicatorView: 1f6c5687f1171810aa27a3296814dc2d7dec3667 OpenSSL-Universal: e7311447fd2419f57420c79524b641537387eff2 @@ -219,6 +219,6 @@ SPEC CHECKSUMS: YYImage: f1ddd15ac032a58b78bbed1e012b50302d318331 ZXingObjC: fdbb269f25dd2032da343e06f10224d62f537bdb -PODFILE CHECKSUM: 50ae96076a7cd581c63b3276679615844c88ac44 +PODFILE CHECKSUM: bd0e75b0b6e37b30d8414efed2a5a98635e1a1a6 COCOAPODS: 1.11.2 diff --git a/Session.xcodeproj/project.pbxproj b/Session.xcodeproj/project.pbxproj index 81cd9273a..7cc89689a 100644 --- a/Session.xcodeproj/project.pbxproj +++ b/Session.xcodeproj/project.pbxproj @@ -752,6 +752,7 @@ FD1C98E4282E3C5B00B76F9E /* UINavigationBar+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD1C98E3282E3C5B00B76F9E /* UINavigationBar+Utilities.swift */; }; FD28A4F227E990E800FF65E7 /* BlockingManagerRemovalMigration.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD28A4F127E990E800FF65E7 /* BlockingManagerRemovalMigration.swift */; }; FD28A4F427EA79F800FF65E7 /* BlockListUIUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD28A4F327EA79F800FF65E7 /* BlockListUIUtils.swift */; }; + FD3AABE928306BBD00E5099A /* ThreadPickerViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD3AABE828306BBD00E5099A /* ThreadPickerViewModel.swift */; }; FD3C907327E8387300CD579F /* OpenGroupServerIdLookupMigration.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD3C907227E8387300CD579F /* OpenGroupServerIdLookupMigration.swift */; }; FD3C907527E83AC200CD579F /* OpenGroupServerIdLookup.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD3C907427E83AC200CD579F /* OpenGroupServerIdLookup.swift */; }; FD5D200F27AA2B6000FEA984 /* MessageRequestResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD5D200E27AA2B6000FEA984 /* MessageRequestResponse.swift */; }; @@ -1806,6 +1807,7 @@ FD28A4F127E990E800FF65E7 /* BlockingManagerRemovalMigration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlockingManagerRemovalMigration.swift; sourceTree = ""; }; FD28A4F327EA79F800FF65E7 /* BlockListUIUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlockListUIUtils.swift; sourceTree = ""; }; FD28A4F527EAD44C00FF65E7 /* GRDBStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GRDBStorage.swift; sourceTree = ""; }; + FD3AABE828306BBD00E5099A /* ThreadPickerViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadPickerViewModel.swift; sourceTree = ""; }; FD3C907227E8387300CD579F /* OpenGroupServerIdLookupMigration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenGroupServerIdLookupMigration.swift; sourceTree = ""; }; FD3C907427E83AC200CD579F /* OpenGroupServerIdLookup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenGroupServerIdLookup.swift; sourceTree = ""; }; FD5D200E27AA2B6000FEA984 /* MessageRequestResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageRequestResponse.swift; sourceTree = ""; }; @@ -2055,6 +2057,7 @@ FD705A8B278CDB5600F16121 /* SAEScreenLockViewController.swift */, FD705A8F278CEBBC00F16121 /* ShareAppExtensionContext.swift */, C3ADC66026426688005F1414 /* ShareVC.swift */, + FD3AABE828306BBD00E5099A /* ThreadPickerViewModel.swift */, B817AD9B26436F73009DF825 /* ThreadPickerVC.swift */, B817AD9926436593009DF825 /* SimplifiedConversationCell.swift */, ); @@ -4546,6 +4549,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + FD3AABE928306BBD00E5099A /* ThreadPickerViewModel.swift in Sources */, B817AD9A26436593009DF825 /* SimplifiedConversationCell.swift in Sources */, C3ADC66126426688005F1414 /* ShareVC.swift in Sources */, FD705A90278CEBBC00F16121 /* ShareAppExtensionContext.swift in Sources */, diff --git a/Session/Conversations/ConversationVC+Interaction.swift b/Session/Conversations/ConversationVC+Interaction.swift index 3d37ebcf7..86147e117 100644 --- a/Session/Conversations/ConversationVC+Interaction.swift +++ b/Session/Conversations/ConversationVC+Interaction.swift @@ -89,7 +89,7 @@ extension ConversationVC: dismiss(animated: true, completion: nil) } - func sendMediaNav(_ sendMediaNavigationController: SendMediaNavigationController, didApproveAttachments attachments: [SignalAttachment], messageText: String?) { + func sendMediaNav(_ sendMediaNavigationController: SendMediaNavigationController, didApproveAttachments attachments: [SignalAttachment], forThreadId threadId: String, messageText: String?) { sendAttachments(attachments, with: messageText ?? "") resetMentions() self.snInputView.text = "" @@ -106,7 +106,7 @@ extension ConversationVC: // MARK: - AttachmentApprovalViewControllerDelegate - func attachmentApproval(_ attachmentApproval: AttachmentApprovalViewController, didApproveAttachments attachments: [SignalAttachment], messageText: String?) { + func attachmentApproval(_ attachmentApproval: AttachmentApprovalViewController, didApproveAttachments attachments: [SignalAttachment], forThreadId threadId: String, messageText: String?) { sendAttachments(attachments, with: messageText ?? "") { [weak self] in self?.dismiss(animated: true, completion: nil) } @@ -146,9 +146,13 @@ extension ConversationVC: } func handleLibraryButtonTapped() { + let threadId: String = self.viewModel.viewData.thread.id + requestLibraryPermissionIfNeeded { [weak self] in DispatchQueue.main.async { - let sendMediaNavController = SendMediaNavigationController.showingMediaLibraryFirst() + let sendMediaNavController = SendMediaNavigationController.showingMediaLibraryFirst( + threadId: threadId + ) sendMediaNavController.sendMediaNavDelegate = self sendMediaNavController.modalPresentationStyle = .fullScreen self?.present(sendMediaNavController, animated: true, completion: nil) @@ -165,7 +169,7 @@ extension ConversationVC: SNLog("Proceeding without microphone access. Any recorded video will be silent.") } - let sendMediaNavController = SendMediaNavigationController.showingCameraFirst() + let sendMediaNavController = SendMediaNavigationController.showingCameraFirst(threadId: self.viewModel.viewData.thread.id) sendMediaNavController.sendMediaNavDelegate = self sendMediaNavController.modalPresentationStyle = .fullScreen @@ -234,7 +238,12 @@ extension ConversationVC: } func showAttachmentApprovalDialog(for attachments: [SignalAttachment]) { - let navController = AttachmentApprovalViewController.wrappedInNavController(attachments: attachments, approvalDelegate: self) + let navController = AttachmentApprovalViewController.wrappedInNavController( + threadId: self.viewModel.viewData.thread.id, + attachments: attachments, + approvalDelegate: self + ) + present(navController, animated: true, completion: nil) } @@ -505,7 +514,11 @@ extension ConversationVC: let dataSource = DataSourceValue.dataSource(with: imageData, utiType: kUTTypeJPEG as String) let attachment = SignalAttachment.attachment(dataSource: dataSource, dataUTI: kUTTypeJPEG as String, imageQuality: .medium) - let approvalVC = AttachmentApprovalViewController.wrappedInNavController(attachments: [ attachment ], approvalDelegate: self) + let approvalVC = AttachmentApprovalViewController.wrappedInNavController( + threadId: self.viewModel.viewData.thread.id, + attachments: [ attachment ], + approvalDelegate: self + ) approvalVC.modalPresentationStyle = .fullScreen self.present(approvalVC, animated: true, completion: nil) diff --git a/Session/Home/HomeVC.swift b/Session/Home/HomeVC.swift index 4e13dec3e..30d51278a 100644 --- a/Session/Home/HomeVC.swift +++ b/Session/Home/HomeVC.swift @@ -15,6 +15,12 @@ final class HomeVC: BaseVC, UITableViewDataSource, UITableViewDelegate, NewConve private var dataChangeObservable: DatabaseCancellable? private var hasLoadedInitialData: Bool = false + // MARK: - Intialization + + deinit { + NotificationCenter.default.removeObserver(self) + } + // MARK: - UI private var tableViewTopConstraint: NSLayoutConstraint! @@ -205,10 +211,6 @@ final class HomeVC: BaseVC, UITableViewDataSource, UITableViewDelegate, NewConve dataChangeObservable?.cancel() } - deinit { - NotificationCenter.default.removeObserver(self) - } - // MARK: - Updating private func startObservingChanges() { diff --git a/Session/Notifications/AppNotifications.swift b/Session/Notifications/AppNotifications.swift index 06ae9bc2e..7bf09b3d7 100644 --- a/Session/Notifications/AppNotifications.swift +++ b/Session/Notifications/AppNotifications.swift @@ -425,7 +425,11 @@ class NotificationActionHandler { trySendReadReceipt: true ) - return MessageSender.sendNonDurably(db, interaction: interaction, in: thread) + return try MessageSender.sendNonDurably( + db, + interaction: interaction, + in: thread + ) } promise.catch { [weak self] error in diff --git a/SessionMessagingKit/Database/Models/Attachment.swift b/SessionMessagingKit/Database/Models/Attachment.swift index 6fa2c67d3..d526532ab 100644 --- a/SessionMessagingKit/Database/Models/Attachment.swift +++ b/SessionMessagingKit/Database/Models/Attachment.swift @@ -563,7 +563,7 @@ public extension Attachment { return attachmentsFolder }() - internal static func originalFilePath(id: String, mimeType: String, sourceFilename: String?) -> String? { + public static func originalFilePath(id: String, mimeType: String, sourceFilename: String?) -> String? { return MIMETypeUtil.filePath( forAttachment: id, ofMIMEType: mimeType, @@ -866,6 +866,7 @@ extension Attachment { extension Attachment { internal func upload( + _ db: Database, using upload: (Data) -> Promise, encrypt: Bool, success: (() -> Void)?, @@ -899,11 +900,9 @@ extension Attachment { digest == nil else { // Save the final upload info - let uploadedAttachment: Attachment? = GRDBStorage.shared.write { db in - try self - .with(state: .uploaded) - .saved(db) - } + let uploadedAttachment: Attachment? = try? self + .with(state: .uploaded) + .saved(db) guard uploadedAttachment != nil else { SNLog("Couldn't update attachmentUpload job.") @@ -943,11 +942,9 @@ extension Attachment { } // Update the attachment to the 'uploading' state - let updatedAttachment: Attachment? = GRDBStorage.shared.write { db in - try processedAttachment - .with(state: .uploading) - .saved(db) - } + let updatedAttachment: Attachment? = try? processedAttachment + .with(state: .uploading) + .saved(db) guard updatedAttachment != nil else { SNLog("Couldn't update attachmentUpload job.") diff --git a/SessionMessagingKit/Database/Models/Interaction.swift b/SessionMessagingKit/Database/Models/Interaction.swift index b3a42eca8..01c2267fd 100644 --- a/SessionMessagingKit/Database/Models/Interaction.swift +++ b/SessionMessagingKit/Database/Models/Interaction.swift @@ -404,6 +404,63 @@ public extension Interaction { // MARK: - GRDB Interactions public extension Interaction { + static func lastInteractionTimestamp(timestampMsKey: String) -> CommonTableExpression { + return CommonTableExpression( + named: "lastInteraction", + request: Interaction + .select( + Interaction.Columns.threadId, + + // 'max()' to get the latest + max(Interaction.Columns.timestampMs).forKey(timestampMsKey) + ) + .joining(required: Interaction.thread) + .group(Interaction.Columns.threadId) // One interaction per thread + ) + } + + static func lastInteraction( + lastInteractionKey: String, + timestampMsKey: String, + threadVariantKey: String, + isOpenGroupInvitationKey: String, + recipientStatesKey: String + ) -> CommonTableExpression { + let thread: TypedTableAlias = TypedTableAlias() + let linkPreview: TypedTableAlias = TypedTableAlias() + + return CommonTableExpression( + named: lastInteractionKey, + request: Interaction + .select( + Interaction.Columns.id, + Interaction.Columns.threadId, + Interaction.Columns.variant, + + // 'max()' to get the latest + max(Interaction.Columns.timestampMs).forKey(timestampMsKey), + + thread[.variant].forKey(threadVariantKey), + Interaction.Columns.body, + Interaction.Columns.authorId, + (linkPreview[.url] != nil).forKey(isOpenGroupInvitationKey) + ) + .joining(required: Interaction.thread.aliased(thread)) + .joining( + optional: Interaction.linkPreview + .filter(literal: Interaction.linkPreviewFilterLiteral) + .filter(LinkPreview.Columns.variant == LinkPreview.Variant.openGroupInvitation) + ) + .including(all: Interaction.attachments) + .including( + all: Interaction.recipientStates + .select(RecipientState.Columns.state) + .forKey(recipientStatesKey) + ) + .group(Interaction.Columns.threadId) // One interaction per thread + ) + } + /// This will update the `wasRead` state the the interaction /// /// - Parameters diff --git a/SessionMessagingKit/Database/Models/InteractionAttachment.swift b/SessionMessagingKit/Database/Models/InteractionAttachment.swift index 50f0dab0c..394df2e07 100644 --- a/SessionMessagingKit/Database/Models/InteractionAttachment.swift +++ b/SessionMessagingKit/Database/Models/InteractionAttachment.swift @@ -30,6 +30,16 @@ public struct InteractionAttachment: Codable, FetchableRecord, PersistableRecord request(for: InteractionAttachment.attachment) } + // MARK: - Initialization + + public init( + interactionId: Int64, + attachmentId: String + ) { + self.interactionId = interactionId + self.attachmentId = attachmentId + } + // MARK: - Custom Database Interaction public func delete(_ db: Database) throws -> Bool { diff --git a/SessionMessagingKit/Jobs/Types/AttachmentUploadJob.swift b/SessionMessagingKit/Jobs/Types/AttachmentUploadJob.swift index 827512b06..f9965ba89 100644 --- a/SessionMessagingKit/Jobs/Types/AttachmentUploadJob.swift +++ b/SessionMessagingKit/Jobs/Types/AttachmentUploadJob.swift @@ -33,18 +33,21 @@ public enum AttachmentUploadJob: JobExecutor { return } - attachment.upload( - using: { data in - if let openGroup: OpenGroup = openGroup { - return OpenGroupAPIV2.upload(data, to: openGroup.room, on: openGroup.server) - } - - return FileServerAPIV2.upload(data) - }, - encrypt: (openGroup == nil), - success: { success(job, false) }, - failure: { error in failure(job, error, false) } - ) + GRDBStorage.shared.writeAsync { db in + attachment.upload( + db, + using: { data in + if let openGroup: OpenGroup = openGroup { + return OpenGroupAPIV2.upload(data, to: openGroup.room, on: openGroup.server) + } + + return FileServerAPIV2.upload(data) + }, + encrypt: (openGroup == nil), + success: { success(job, false) }, + failure: { error in failure(job, error, false) } + ) + } } } diff --git a/SessionMessagingKit/Messages/Control Messages/ClosedGroupControlMessage.swift b/SessionMessagingKit/Messages/Control Messages/ClosedGroupControlMessage.swift index 3bcbc3232..1be74cf07 100644 --- a/SessionMessagingKit/Messages/Control Messages/ClosedGroupControlMessage.swift +++ b/SessionMessagingKit/Messages/Control Messages/ClosedGroupControlMessage.swift @@ -15,8 +15,8 @@ public final class ClosedGroupControlMessage: ControlMessage { public override var ttl: UInt64 { switch kind { - case .encryptionKeyPair: return 14 * 24 * 60 * 60 * 1000 - default: return 14 * 24 * 60 * 60 * 1000 + case .encryptionKeyPair: return 14 * 24 * 60 * 60 * 1000 + default: return 14 * 24 * 60 * 60 * 1000 } } @@ -184,8 +184,8 @@ public final class ClosedGroupControlMessage: ControlMessage { // MARK: - Initialization - internal init(kind: Kind) { - super.init() + internal init(kind: Kind, sentTimestampMs: UInt64? = nil) { + super.init(sentTimestamp: sentTimestampMs) self.kind = kind } diff --git a/SessionMessagingKit/Sending & Receiving/MessageSender+ClosedGroups.swift b/SessionMessagingKit/Sending & Receiving/MessageSender+ClosedGroups.swift index fa42ccc75..3215309a7 100644 --- a/SessionMessagingKit/Sending & Receiving/MessageSender+ClosedGroups.swift +++ b/SessionMessagingKit/Sending & Receiving/MessageSender+ClosedGroups.swift @@ -73,12 +73,10 @@ extension MessageSender { members: membersAsData, admins: adminsAsData, expirationTimer: 0 - ) - ) - .with( + ), // Note: We set this here to ensure the value matches the 'ClosedGroup' // object we created - sentTimestamp: UInt64(floor(formationTimestamp * 1000)) + sentTimestampMs: UInt64(floor(formationTimestamp * 1000)) ), interactionId: nil, in: contactThread diff --git a/SessionMessagingKit/Sending & Receiving/MessageSender+Convenience.swift b/SessionMessagingKit/Sending & Receiving/MessageSender+Convenience.swift index c58632fed..716d74ba5 100644 --- a/SessionMessagingKit/Sending & Receiving/MessageSender+Convenience.swift +++ b/SessionMessagingKit/Sending & Receiving/MessageSender+Convenience.swift @@ -6,8 +6,8 @@ import PromiseKit import SessionUtilitiesKit extension MessageSender { - - // MARK: Durable + + // MARK: - Durable public static func send(_ db: Database, interaction: Interaction, with attachments: [SignalAttachment], in thread: SessionThread) throws { guard let interactionId: Int64 = interaction.id else { throw GRDBStorageError.objectNotSaved } @@ -61,78 +61,111 @@ extension MessageSender { ) } + // MARK: - Non-Durable - public static func sendNonDurably(_ db: Database, interaction: Interaction, in thread: SessionThread) -> Promise { - guard let interactionId: Int64 = interaction.id else { - return Promise(error: GRDBStorageError.objectNotSaved) - } + public static func sendNonDurably(_ db: Database, interaction: Interaction, with attachments: [SignalAttachment], in thread: SessionThread) throws -> Promise { + guard let interactionId: Int64 = interaction.id else { return Promise(error: GRDBStorageError.objectNotSaved) } - let openGroup: OpenGroup? = try? thread.openGroup.fetchOne(db) - let attachmentStateInfo: [Attachment.StateInfo] = (try? Attachment - .stateInfo(interactionId: interactionId, state: .pending) - .fetchAll(db)) - .defaulting(to: []) - let attachmentUploadPromises: [Promise] = (try? Attachment - .filter(ids: attachmentStateInfo.map { $0.attachmentId }) - .fetchAll(db)) - .defaulting(to: []) - .map { attachment -> Promise in - let (promise, seal) = Promise.pending() - - attachment.upload( - using: { data in - if let openGroup: OpenGroup = openGroup { - return OpenGroupAPIV2.upload(data, to: openGroup.room, on: openGroup.server) - } - - return FileServerAPIV2.upload(data) - }, - encrypt: (openGroup == nil), - success: { seal.fulfill(()) }, - failure: { seal.reject($0) } - ) - - return promise - } + try prep(db, signalAttachments: attachments, for: interactionId) - return when(resolved: attachmentUploadPromises) - .then(on: DispatchQueue.global(qos: .userInitiated)) { results -> Promise in - let errors = results - .compactMap { result -> Swift.Error? in - if case .rejected(let error) = result { return error } - - return nil - } - - if let error = errors.first { return Promise(error: error) } - - return sendNonDurably(db, interaction: interaction, in: thread) - } + return sendNonDurably( + db, + message: VisibleMessage.from(db, interaction: interaction), + interactionId: interactionId, + to: try Message.Destination.from(db, thread: thread) + ) } - public static func sendNonDurably(_ db: Database, _ message: VisibleMessage, with attachmentIds: [String], in thread: TSThread) -> Promise { + + public static func sendNonDurably(_ db: Database, interaction: Interaction, in thread: SessionThread) throws -> Promise { + // Only 'VisibleMessage' types can be sent via this method + guard interaction.variant == .standardOutgoing else { throw MessageSenderError.invalidMessage } + guard let interactionId: Int64 = interaction.id else { throw GRDBStorageError.objectNotSaved } + + return sendNonDurably( + db, + message: VisibleMessage.from(db, interaction: interaction), + interactionId: interactionId, + to: try Message.Destination.from(db, thread: thread) + ) } - public static func sendNonDurably(_ db: Database, message: Message, interactionId: Int64?, in thread: SessionThread) throws -> Promise { - return try MessageSender.sendImmediate( + return sendNonDurably( db, message: message, - to: try Message.Destination.from(db, thread: thread), - interactionId: interactionId + interactionId: interactionId, + to: try Message.Destination.from(db, thread: thread) ) } - public static func sendNonDurably(_ message: VisibleMessage, with attachments: [SignalAttachment], in thread: TSThread) -> Promise { - } + public static func sendNonDurably(_ db: Database, message: Message, interactionId: Int64?, to destination: Message.Destination) -> Promise { + var attachmentUploadPromises: [Promise] = [Promise.value(())] + + // If we have an interactionId then check if it has any attachments and process them first + if let interactionId: Int64 = interactionId { + let threadId: String = { + switch destination { + case .contact(let publicKey): return publicKey + case .closedGroup(let groupPublicKey): return groupPublicKey + case .openGroupV2(let room, let server): + return OpenGroup.idFor(room: room, server: server) + + case .openGroup: return "" + } + }() + let openGroup: OpenGroup? = try? OpenGroup.fetchOne(db, id: threadId) + let attachmentStateInfo: [Attachment.StateInfo] = (try? Attachment + .stateInfo(interactionId: interactionId, state: .pending) + .fetchAll(db)) + .defaulting(to: []) + + attachmentUploadPromises = (try? Attachment + .filter(ids: attachmentStateInfo.map { $0.attachmentId }) + .fetchAll(db)) + .defaulting(to: []) + .map { attachment -> Promise in + let (promise, seal) = Promise.pending() - public static func sendNonDurably(_ db: Database, message: Message, interactionId: Int64?, to destination: Message.Destination) throws -> Promise { - return try MessageSender.sendImmediate( - db, - message: message, - to: destination, - interactionId: interactionId - ) + attachment.upload( + db, + using: { data in + if let openGroup: OpenGroup = openGroup { + return OpenGroupAPIV2.upload(data, to: openGroup.room, on: openGroup.server) + } + + return FileServerAPIV2.upload(data) + }, + encrypt: (openGroup == nil), + success: { seal.fulfill(()) }, + failure: { seal.reject($0) } + ) + + return promise + } + } + + // Once the attachments are processed then send the message + return when(resolved: attachmentUploadPromises) + .then { results -> Promise in + let errors: [Error] = results + .compactMap { result -> Error? in + if case .rejected(let error) = result { return error } + + return nil + } + + if let error: Error = errors.first { return Promise(error: error) } + + return GRDBStorage.shared.write { db in + try MessageSender.sendImmediate( + db, + message: message, + to: destination, + interactionId: interactionId + ) + } + } } /// This method requires the `db` value to be passed in because if it's called within a `writeAsync` completion block diff --git a/SessionShareExtension/ShareVC.swift b/SessionShareExtension/ShareVC.swift index 4cd67274f..f637b9418 100644 --- a/SessionShareExtension/ShareVC.swift +++ b/SessionShareExtension/ShareVC.swift @@ -225,6 +225,13 @@ final class ShareVC : UINavigationController, ShareViewDelegate, AppModeManagerD } func shareViewFailed(error: Error) { + guard Thread.isMainThread else { + DispatchQueue.main.async { [weak self] in + self?.shareViewFailed(error: error) + } + return + } + let alert = UIAlertController(title: "Session", message: error.localizedDescription, preferredStyle: .alert) alert.addAction(UIAlertAction(title: "BUTTON_OK".localized(), style: .default, handler: { _ in self.extensionContext!.cancelRequest(withError: error) diff --git a/SessionShareExtension/SimplifiedConversationCell.swift b/SessionShareExtension/SimplifiedConversationCell.swift index 386b2da83..ad0eddfc9 100644 --- a/SessionShareExtension/SimplifiedConversationCell.swift +++ b/SessionShareExtension/SimplifiedConversationCell.swift @@ -4,9 +4,7 @@ import UIKit import SessionUIKit import SessionMessagingKit -final class SimplifiedConversationCell : UITableViewCell { - var threadViewModel: ThreadViewModel! { didSet { update() } } - +final class SimplifiedConversationCell: UITableViewCell { // MARK: - Initialization override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { @@ -80,45 +78,25 @@ final class SimplifiedConversationCell : UITableViewCell { accentLineView.set(.width, to: Values.accentLineThickness) accentLineView.set(.height, to: 68) - let profilePictureViewSize = Values.mediumProfilePictureSize - profilePictureView.set(.width, to: profilePictureViewSize) - profilePictureView.set(.height, to: profilePictureViewSize) - profilePictureView.size = profilePictureViewSize + profilePictureView.set(.width, to: Values.mediumProfilePictureSize) + profilePictureView.set(.height, to: Values.mediumProfilePictureSize) + profilePictureView.size = Values.mediumProfilePictureSize stackView.pin(to: self) } - // MARK: - Content + // MARK: - Updating - private func update() { - AssertIsOnMainThread() - - guard let thread = threadViewModel?.thread else { return } - - accentLineView.alpha = (thread.isBlocked() ? 1 : 0) - profilePictureView.update(for: thread) - displayNameLabel.text = getDisplayName() - } - - private func getDisplayName() -> String { - if threadViewModel.thread.variant == .closedGroup || threadViewModel.thread.variant == .openGroup { - if threadViewModel.name.isEmpty { - // TODO: Localization - return "Unknown Group" - } - - return threadViewModel.name - } - - if threadViewModel.threadRecord.isNoteToSelf() { - return "NOTE_TO_SELF".localized() - } - - guard threadViewModel.thread.variant == .contact else { - // TODO: Localization - return "Unknown" - } - - return Profile.displayName(id: threadViewModel.thread.id) + public func update(with item: ThreadPickerViewModel.Item, currentUserProfile: Profile) { + accentLineView.alpha = (item.isBlocked ? 1 : 0) + profilePictureView.update( + publicKey: item.id, + profile: item.profile(currentUserProfile: currentUserProfile), + additionalProfile: item.additionalProfile, + threadVariant: item.variant, + openGroupProfilePicture: item.openGroupProfilePictureData.map { UIImage(data: $0) }, + useFallbackPicture: (item.variant == .openGroup && item.openGroupProfilePictureData == nil) + ) + displayNameLabel.text = item.displayName(currentUserProfile: currentUserProfile) } } diff --git a/SessionShareExtension/ThreadPickerVC.swift b/SessionShareExtension/ThreadPickerVC.swift index 91a193a7e..f4798cde6 100644 --- a/SessionShareExtension/ThreadPickerVC.swift +++ b/SessionShareExtension/ThreadPickerVC.swift @@ -1,24 +1,25 @@ +// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. + import UIKit -import SignalUtilitiesKit +import GRDB +import PromiseKit +import DifferenceKit import SessionUIKit +import SignalUtilitiesKit import SessionMessagingKit -import SessionUtilitiesKit final class ThreadPickerVC: UIViewController, UITableViewDataSource, UITableViewDelegate, AttachmentApprovalViewControllerDelegate { - private var threads: YapDatabaseViewMappings! - private var threadViewModelCache: [String: ThreadViewModel] = [:] // Thread ID to ThreadViewModel - private var selectedThread: TSThread? + private let viewModel: ThreadPickerViewModel = ThreadPickerViewModel() + private var dataChangeObservable: DatabaseCancellable? + private var hasLoadedInitialData: Bool = false + var shareVC: ShareVC? - private var threadCount: UInt { - threads.numberOfItems(inGroup: TSShareExtensionGroup) - } + // MARK: - Intialization - private lazy var dbConnection: YapDatabaseConnection = { - let result = OWSPrimaryStorage.shared().newDatabaseConnection() - result.objectCacheLimit = 500 - return result - }() + deinit { + NotificationCenter.default.removeObserver(self) + } // MARK: - UI @@ -63,14 +64,6 @@ final class ThreadPickerVC: UIViewController, UITableViewDataSource, UITableView view.backgroundColor = .clear view.setGradient(Gradients.defaultBackground) - // Threads - dbConnection.beginLongLivedReadTransaction() // Freeze the connection for use on the main thread (this gives us a stable data source that doesn't change until we tell it to) - threads = YapDatabaseViewMappings(groups: [ TSShareExtensionGroup ], view: TSThreadShareExtensionDatabaseViewExtensionName) // The extension should be registered at this point - threads.setIsReversed(true, forGroup: TSShareExtensionGroup) - dbConnection.read { transaction in - self.threads.update(with: transaction) // Perform the initial update - } - // Title navigationItem.titleView = titleLabel @@ -80,8 +73,41 @@ final class ThreadPickerVC: UIViewController, UITableViewDataSource, UITableView view.addSubview(fadeView) setupLayout() - // Reload - reload() + + // Notifications + NotificationCenter.default.addObserver( + self, + selector: #selector(applicationDidBecomeActive(_:)), + name: UIApplication.didBecomeActiveNotification, + object: nil + ) + NotificationCenter.default.addObserver( + self, + selector: #selector(applicationDidResignActive(_:)), + name: UIApplication.didEnterBackgroundNotification, object: nil + ) + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + + startObservingChanges() + } + + override func viewWillDisappear(_ animated: Bool) { + super.viewWillDisappear(animated) + + // Stop observing database changes + dataChangeObservable?.cancel() + } + + @objc func applicationDidBecomeActive(_ notification: Notification) { + startObservingChanges() + } + + @objc func applicationDidResignActive(_ notification: Notification) { + // Stop observing database changes + dataChangeObservable?.cancel() } private func setupNavBar() { @@ -112,55 +138,83 @@ final class ThreadPickerVC: UIViewController, UITableViewDataSource, UITableView fadeView.pin(.bottom, to: .bottom, of: view) } - // MARK: Table View Data Source + // MARK: - Updating + + private func startObservingChanges() { + // Start observing for data changes + dataChangeObservable = GRDBStorage.shared.start( + viewModel.observableViewData, + onError: { _ in }, + onChange: { [weak self] viewData in + // The defaul scheduler emits changes on the main thread + self?.handleUpdates(viewData) + } + ) + } + + private func handleUpdates(_ updatedViewData: ThreadPickerViewModel.ViewData) { + // Ensure the first load runs without animations (if we don't do this the cells will animate + // in from a frame of CGRect.zero) + guard hasLoadedInitialData else { + hasLoadedInitialData = true + UIView.performWithoutAnimation { handleUpdates(updatedViewData) } + return + } + + // Reload the table content (animate changes after the first load) + tableView.reload( + using: StagedChangeset(source: viewModel.viewData.items, target: updatedViewData.items), + with: .automatic, + interrupt: { $0.changeCount > 100 } // Prevent too many changes from causing performance issues + ) { [weak self] updatedData in + self?.viewModel.updateData( + ThreadPickerViewModel.ViewData( + currentUserProfile: updatedViewData.currentUserProfile, + items: updatedData + ) + ) + } + } + + // MARK: - UITableViewDataSource + func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - return Int(threadCount) + return self.viewModel.viewData.items.count } func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell: SimplifiedConversationCell = tableView.dequeue(type: SimplifiedConversationCell.self, for: indexPath) - cell.threadViewModel = threadViewModel(at: indexPath.row) + cell.update( + with: self.viewModel.viewData.items[indexPath.row], + currentUserProfile: self.viewModel.viewData.currentUserProfile + ) return cell } - // MARK: - Updating - - private func reload() { - AssertIsOnMainThread() - dbConnection.beginLongLivedReadTransaction() // Jump to the latest commit - dbConnection.read { transaction in - self.threads.update(with: transaction) - } - threadViewModelCache.removeAll() - tableView.reloadData() - } - // MARK: - Interaction func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { tableView.deselectRow(at: indexPath, animated: true) - guard let thread = self.thread(at: indexPath.row), let attachments = ShareVC.attachmentPrepPromise?.value else { - return - } + guard let attachments: [SignalAttachment] = ShareVC.attachmentPrepPromise?.value else { return } - self.selectedThread = thread - - let approvalVC = AttachmentApprovalViewController.wrappedInNavController(attachments: attachments, approvalDelegate: self) - navigationController!.present(approvalVC, animated: true, completion: nil) + let approvalVC: OWSNavigationController = AttachmentApprovalViewController.wrappedInNavController( + threadId: self.viewModel.viewData.items[indexPath.row].id, + attachments: attachments, + approvalDelegate: self + ) + self.navigationController?.present(approvalVC, animated: true, completion: nil) } - func attachmentApproval(_ attachmentApproval: AttachmentApprovalViewController, didApproveAttachments attachments: [SignalAttachment], messageText: String?) { + func attachmentApproval(_ attachmentApproval: AttachmentApprovalViewController, didApproveAttachments attachments: [SignalAttachment], forThreadId threadId: String, messageText: String?) { // Sharing a URL or plain text will populate the 'messageText' field so in those // cases we should ignore the attachments let isSharingUrl: Bool = (attachments.count == 1 && attachments[0].isUrl) let isSharingText: Bool = (attachments.count == 1 && attachments[0].isText) let finalAttachments: [SignalAttachment] = (isSharingUrl || isSharingText ? [] : attachments) - - let message = VisibleMessage() - message.sentTimestamp = NSDate.millisecondTimestamp() - message.text = (isSharingUrl && (messageText?.isEmpty == true || attachments[0].linkPreviewDraft == nil) ? + let body: String? = ( + isSharingUrl && (messageText?.isEmpty == true || attachments[0].linkPreviewDraft == nil) ? ( (messageText?.isEmpty == true || (attachments[0].text() == messageText) ? attachments[0].text() : @@ -169,35 +223,55 @@ final class ThreadPickerVC: UIViewController, UITableViewDataSource, UITableView ) : messageText ) - - let tsMessage = TSOutgoingMessage.from(message, associatedWith: selectedThread!) - Storage.write( - with: { transaction in - if isSharingUrl { - message.linkPreview = VisibleMessage.LinkPreview.from( - attachments[0].linkPreviewDraft, - using: transaction - ) - } - else { - tsMessage.save(with: transaction) - } - }, - completion: { - if isSharingUrl { - tsMessage.linkPreview = OWSLinkPreview.from(message.linkPreview) - - Storage.write { transaction in - tsMessage.save(with: transaction) - } - } - } - ) - shareVC!.dismiss(animated: true, completion: nil) + shareVC?.dismiss(animated: true, completion: nil) ModalActivityIndicatorViewController.present(fromViewController: shareVC!, canCancel: false, message: "vc_share_sending_message".localized()) { activityIndicator in - MessageSender.sendNonDurably(message, with: finalAttachments, in: self.selectedThread!) + GRDBStorage.shared + .write { [weak self] db -> Promise in + guard let thread: SessionThread = try SessionThread.fetchOne(db, id: threadId) else { + activityIndicator.dismiss { } + self?.shareVC?.shareViewFailed(error: MessageSenderError.noThread) + return Promise(error: MessageSenderError.noThread) + } + + // Create the interaction + let userPublicKey: String = getUserHexEncodedPublicKey(db) + let interaction: Interaction = try Interaction( + threadId: threadId, + authorId: getUserHexEncodedPublicKey(db), + variant: .standardOutgoing, + body: body, + timestampMs: Int64(floor(Date().timeIntervalSince1970 * 1000)), + hasMention: (body?.contains("@\(userPublicKey)") == true), + linkPreviewUrl: (isSharingUrl ? attachments.first?.linkPreviewDraft?.urlString : nil) + ).inserted(db) + + // If the user is sharing a Url, there is a LinkPreview and it doesn't match an existing + // one then add it now + if + isSharingUrl, + let linkPreviewDraft: OWSLinkPreviewDraft = attachments.first?.linkPreviewDraft, + (try? interaction.linkPreview.isEmpty(db)) == true + { + try LinkPreview( + url: linkPreviewDraft.urlString, + title: linkPreviewDraft.title, + attachmentId: LinkPreview.saveAttachmentIfPossible( + db, + imageData: linkPreviewDraft.jpegImageData, + mimeType: OWSMimeTypeImageJpeg + ) + ).insert(db) + } + + return try MessageSender.sendNonDurably( + db, + interaction: interaction, + with: finalAttachments, + in: thread + ) + } .done { [weak self] _ in activityIndicator.dismiss { } self?.shareVC?.shareViewWasCompleted() @@ -216,31 +290,4 @@ final class ThreadPickerVC: UIViewController, UITableViewDataSource, UITableView func attachmentApproval(_ attachmentApproval: AttachmentApprovalViewController, didChangeMessageText newMessageText: String?) { // Do nothing } - - // MARK: - Convenience - - private func thread(at index: Int) -> TSThread? { - var thread: TSThread? = nil - dbConnection.read { transaction in - let ext = transaction.ext(TSThreadShareExtensionDatabaseViewExtensionName) as! YapDatabaseViewTransaction - thread = ext.object(atRow: UInt(index), inSection: 0, with: self.threads) as! TSThread? - } - return thread - } - - private func threadViewModel(at index: Int) -> ThreadViewModel? { - guard let thread = thread(at: index) else { return nil } - - if let cachedThreadViewModel = threadViewModelCache[thread.uniqueId!] { - return cachedThreadViewModel - } - else { - var threadViewModel: ThreadViewModel? = nil - dbConnection.read { transaction in - threadViewModel = ThreadViewModel(thread: thread, transaction: transaction) - } - threadViewModelCache[thread.uniqueId!] = threadViewModel - return threadViewModel - } - } } diff --git a/SessionShareExtension/ThreadPickerViewModel.swift b/SessionShareExtension/ThreadPickerViewModel.swift new file mode 100644 index 000000000..6885dcd81 --- /dev/null +++ b/SessionShareExtension/ThreadPickerViewModel.swift @@ -0,0 +1,192 @@ +// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. + +import Foundation +import GRDB +import DifferenceKit +import SignalUtilitiesKit +import SessionMessagingKit + +public class ThreadPickerViewModel { + // MARK: - Initialization + + init() { + viewData = ViewData( + currentUserProfile: Profile.fetchOrCreateCurrentUser(), + items: [] + ) + } + + public struct Item: FetchableRecord, Decodable, Equatable, Differentiable { + public struct GroupMemberInfo: FetchableRecord, Decodable, Equatable { + public let profile: Profile + } + + fileprivate static let closedGroupNameKey = CodingKeys.closedGroupName.stringValue + fileprivate static let openGroupNameKey = CodingKeys.openGroupName.stringValue + fileprivate static let openGroupProfilePictureDataKey = CodingKeys.openGroupProfilePictureData.stringValue + fileprivate static let contactProfileKey = CodingKeys.contactProfile.stringValue + fileprivate static let closedGroupAvatarProfilesKey = CodingKeys.closedGroupAvatarProfiles.stringValue + fileprivate static let contactIsBlockedKey = CodingKeys.contactIsBlocked.stringValue + fileprivate static let isNoteToSelfKey = CodingKeys.isNoteToSelf.stringValue + + public var differenceIdentifier: String { id } + + public let id: String + public let variant: SessionThread.Variant + + public let closedGroupName: String? + public let openGroupName: String? + public let openGroupProfilePictureData: Data? + private let contactProfile: Profile? + private let closedGroupAvatarProfiles: [GroupMemberInfo]? + + /// A flag indicating whether the contact is blocked (will be null for non-contact threads) + private let contactIsBlocked: Bool? + public let isNoteToSelf: Bool + + public func displayName(currentUserProfile: Profile) -> String { + return SessionThread.displayName( + threadId: id, + variant: variant, + closedGroupName: closedGroupName, + openGroupName: openGroupName, + isNoteToSelf: isNoteToSelf, + profile: contactProfile + ) + } + + public func profile(currentUserProfile: Profile) -> Profile? { + switch variant { + case .contact: return contactProfile + case .openGroup: return nil + case .closedGroup: + // If there is only a single user in the group then we want to use the current user + // profile at the back + if closedGroupAvatarProfiles?.count == 1 { + return currentUserProfile + } + + return closedGroupAvatarProfiles?.first?.profile + } + } + + public var additionalProfile: Profile? { + switch variant { + case .closedGroup: return closedGroupAvatarProfiles?.last?.profile + default: return nil + } + } + + /// A flag indicating whether the thread is blocked (only contact threads can be blocked) + public var isBlocked: Bool { + return (contactIsBlocked == true) + } + + // MARK: - Query + + public static func query(userPublicKey: String) -> QueryInterfaceRequest { + let thread: TypedTableAlias = TypedTableAlias() + let contact: TypedTableAlias = TypedTableAlias() + let closedGroup: TypedTableAlias = TypedTableAlias() + let openGroup: TypedTableAlias = TypedTableAlias() + let lastInteraction: TableAlias = TableAlias() + + let lastInteractionTimestampExpression: CommonTableExpression = Interaction.lastInteractionTimestamp( + timestampMsKey: Interaction.Columns.timestampMs.stringValue + ) + // FIXME: Exclude unwritable opengroups + return SessionThread + .select( + thread[.id], + thread[.variant], + thread[.creationDateTimestamp], + + closedGroup[.name].forKey(Item.closedGroupNameKey), + openGroup[.name].forKey(Item.openGroupNameKey), + openGroup[.imageData].forKey(Item.openGroupProfilePictureDataKey), + + contact[.isBlocked].forKey(Item.contactIsBlockedKey), + SessionThread.isNoteToSelf(userPublicKey: userPublicKey).forKey(Item.isNoteToSelfKey) + ) + .filter(SessionThread.Columns.shouldBeVisible == true) + .filter(SessionThread.isNotMessageRequest(userPublicKey: userPublicKey)) + .filter( + // Only show the Note to Self if it has an interaction + SessionThread.Columns.id != userPublicKey || + lastInteraction[Interaction.Columns.timestampMs] != nil + ) + .aliased(thread) + .joining( + optional: SessionThread.contact + .aliased(contact) + .including( + optional: Contact.profile + .forKey(Item.contactProfileKey) + ) + ) + .joining( + optional: SessionThread.closedGroup + .aliased(closedGroup) + .including( + all: ClosedGroup.members + .filter(GroupMember.Columns.role == GroupMember.Role.standard) + .filter(GroupMember.Columns.profileId != userPublicKey) + .order(GroupMember.Columns.profileId) // Sort to provide a level of stability + .limit(2) + .including(required: GroupMember.profile) + .forKey(Item.closedGroupAvatarProfilesKey) + ) + ) + .joining(optional: SessionThread.openGroup.aliased(openGroup)) + .with(lastInteractionTimestampExpression) + .including( + optional: SessionThread + .association( + to: lastInteractionTimestampExpression, + on: { thread, lastInteraction in + thread[SessionThread.Columns.id] == lastInteraction[Interaction.Columns.threadId] + } + ) + .aliased(lastInteraction) + ) + .order( + ( + lastInteraction[Interaction.Columns.timestampMs] ?? + (thread[.creationDateTimestamp] * 1000) + ).desc + ) + .asRequest(of: Item.self) + } + } + + public struct ViewData: Equatable { + let currentUserProfile: Profile + let items: [Item] + } + + /// This value is the current state of the view + public private(set) var viewData: ViewData + + /// This is all the data the screen needs to populate itself, please see the following link for tips to help optimise + /// performance https://github.com/groue/GRDB.swift#valueobservation-performance + /// + /// **Note:** The 'trackingConstantRegion' is optimised in such a way that the request needs to be static + /// otherwise there may be situations where it doesn't get updates, this means we can't have conditional queries + public lazy var observableViewData = ValueObservation + .trackingConstantRegion { db -> ViewData in + let currentUserProfile: Profile = Profile.fetchOrCreateCurrentUser(db) + return ViewData( + currentUserProfile: Profile.fetchOrCreateCurrentUser(db), + items: try Item + .query(userPublicKey: currentUserProfile.id) + .fetchAll(db) + ) + } + .removeDuplicates() + + // MARK: - Functions + + public func updateData(_ updatedData: ViewData) { + self.viewData = updatedData + } +} diff --git a/SessionUtilitiesKit/JobRunner/JobRunner.swift b/SessionUtilitiesKit/JobRunner/JobRunner.swift index c3298d133..df07cc562 100644 --- a/SessionUtilitiesKit/JobRunner/JobRunner.swift +++ b/SessionUtilitiesKit/JobRunner/JobRunner.swift @@ -503,7 +503,7 @@ public final class JobRunner { runNextJob() } return - + // For "blocking once per session" jobs only rerun it immediately if it hasn't already // run this session case .recurringOnLaunchBlockingOncePerSession: @@ -517,7 +517,7 @@ public final class JobRunner { runNextJob() } return - + default: break } @@ -531,6 +531,8 @@ public final class JobRunner { maxFailureCount >= 0 && job.failureCount + 1 < maxFailureCount else { + SNLog("[JobRunner] \(job.variant) failed permanently\(maxFailureCount >= 0 ? "; too many retries" : "")") + // If the job permanently failed or we have performed all of our retry attempts // then delete the job (it'll probably never succeed) _ = try job.delete(db) diff --git a/SignalUtilitiesKit/Media Viewing & Editing/Attachment Approval/AttachmentApprovalViewController.swift b/SignalUtilitiesKit/Media Viewing & Editing/Attachment Approval/AttachmentApprovalViewController.swift index 04ce2726a..027a6f877 100644 --- a/SignalUtilitiesKit/Media Viewing & Editing/Attachment Approval/AttachmentApprovalViewController.swift +++ b/SignalUtilitiesKit/Media Viewing & Editing/Attachment Approval/AttachmentApprovalViewController.swift @@ -14,6 +14,7 @@ public protocol AttachmentApprovalViewControllerDelegate: AnyObject { func attachmentApproval( _ attachmentApproval: AttachmentApprovalViewController, didApproveAttachments attachments: [SignalAttachment], + forThreadId threadId: String, messageText: String? ) @@ -54,6 +55,7 @@ public class AttachmentApprovalViewController: UIPageViewController, UIPageViewC // MARK: - Properties private let mode: Mode + private let threadId: String private let isAddMoreVisible: Bool public weak var approvalDelegate: AttachmentApprovalViewControllerDelegate? @@ -123,10 +125,12 @@ public class AttachmentApprovalViewController: UIPageViewController, UIPageViewC @objc required public init( mode: Mode, + threadId: String, attachments: [SignalAttachment] ) { assert(attachments.count > 0) self.mode = mode + self.threadId = threadId let attachmentItems = attachments.map { SignalAttachmentItem(attachment: $0 )} self.isAddMoreVisible = (mode == .sharedNavigation) @@ -154,12 +158,12 @@ public class AttachmentApprovalViewController: UIPageViewController, UIPageViewC NotificationCenter.default.removeObserver(self) } - @objc public class func wrappedInNavController( + threadId: String, attachments: [SignalAttachment], approvalDelegate: AttachmentApprovalViewControllerDelegate ) -> OWSNavigationController { - let vc = AttachmentApprovalViewController(mode: .modal, attachments: attachments) + let vc = AttachmentApprovalViewController(mode: .modal, threadId: threadId, attachments: attachments) vc.approvalDelegate = approvalDelegate let navController = OWSNavigationController(rootViewController: vc) @@ -760,7 +764,7 @@ extension AttachmentApprovalViewController: AttachmentTextToolbarDelegate { attachmentTextToolbar.isUserInteractionEnabled = false attachmentTextToolbar.isHidden = true - approvalDelegate?.attachmentApproval(self, didApproveAttachments: attachments, messageText: attachmentTextToolbar.messageText) + approvalDelegate?.attachmentApproval(self, didApproveAttachments: attachments, forThreadId: threadId, messageText: attachmentTextToolbar.messageText) } func attachmentTextToolbarDidChange(_ attachmentTextToolbar: AttachmentTextToolbar) { diff --git a/SignalUtilitiesKit/Shared View Controllers/ModalActivityIndicatorViewController.swift b/SignalUtilitiesKit/Shared View Controllers/ModalActivityIndicatorViewController.swift index 868d5745c..4f1c818b0 100644 --- a/SignalUtilitiesKit/Shared View Controllers/ModalActivityIndicatorViewController.swift +++ b/SignalUtilitiesKit/Shared View Controllers/ModalActivityIndicatorViewController.swift @@ -63,8 +63,13 @@ public class ModalActivityIndicatorViewController: OWSViewController { } @objc - public func dismiss(completion : @escaping () -> Void) { - AssertIsOnMainThread() + public func dismiss(completion: @escaping () -> Void) { + guard Thread.isMainThread else { + DispatchQueue.main.async { [weak self] in + self?.dismiss(completion: completion) + } + return + } if !wasDimissed { // Only dismiss once. diff --git a/SignalUtilitiesKit/Utilities/AppSetup.m b/SignalUtilitiesKit/Utilities/AppSetup.m index 9a512565b..129400905 100644 --- a/SignalUtilitiesKit/Utilities/AppSetup.m +++ b/SignalUtilitiesKit/Utilities/AppSetup.m @@ -46,7 +46,6 @@ NS_ASSUME_NONNULL_BEGIN TSAccountManager *tsAccountManager = [[TSAccountManager alloc] initWithPrimaryStorage:primaryStorage]; id reachabilityManager = [SSKReachabilityManagerImpl new]; - id typingIndicators = [[OWSTypingIndicatorsImpl alloc] init]; OWSAudioSession *audioSession = [OWSAudioSession new]; id proximityMonitoringManager = [OWSProximityMonitoringManagerImpl new]; @@ -61,8 +60,7 @@ NS_ASSUME_NONNULL_BEGIN // TODO: Refactor this file to Swift [SSKEnvironment setShared:[[SSKEnvironment alloc] initWithPrimaryStorage:primaryStorage tsAccountManager:tsAccountManager - reachabilityManager:reachabilityManager - typingIndicators:typingIndicators]]; + reachabilityManager:reachabilityManager]]; // [SSKEnvironment setShared:[[SSKEnvironment alloc] initWithPrimaryStorage:primaryStorage // tsAccountManager:tsAccountManager // disappearingMessagesJob:disappearingMessagesJob diff --git a/SignalUtilitiesKit/Utilities/UIViewController+OWS.m b/SignalUtilitiesKit/Utilities/UIViewController+OWS.m index bb20c7a7d..ea3a05715 100644 --- a/SignalUtilitiesKit/Utilities/UIViewController+OWS.m +++ b/SignalUtilitiesKit/Utilities/UIViewController+OWS.m @@ -9,6 +9,7 @@ #import "UIView+OWS.h" #import "UIViewController+OWS.h" #import +#import #import @@ -83,7 +84,7 @@ NS_ASSUME_NONNULL_BEGIN const CGFloat kExtraRightPadding = isRTL ? -0 : +10; // Extra hit area above/below - const CGFloat kExtraHeightPadding = 4; + const CGFloat kExtraHeightPadding = 8; // Matching the default backbutton placement is tricky. // We can't just adjust the imageEdgeInsets on a UIBarButtonItem directly, @@ -91,39 +92,19 @@ NS_ASSUME_NONNULL_BEGIN // in a UIBarButtonItem. [backButton addTarget:target action:selector forControlEvents:UIControlEventTouchUpInside]; - UIImage *backImage = [[UIImage imageNamed:(isRTL ? @"NavBarBackRTL" : @"NavBarBack")] - imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate]; + UIImageConfiguration *config = [UIImageSymbolConfiguration configurationWithPointSize:22 weight:UIImageSymbolWeightMedium]; + UIImage *backImage = [[UIImage systemImageNamed:@"chevron.backward" withConfiguration:config] imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate]; OWSAssertDebug(backImage); [backButton setImage:backImage forState:UIControlStateNormal]; - backButton.tintColor = UIColor.lokiGreen; + backButton.tintColor = LKColors.text; backButton.contentHorizontalAlignment = UIControlContentHorizontalAlignmentLeft; + backButton.imageEdgeInsets = UIEdgeInsetsMake(0, kExtraLeftPadding, 0, 0); - // Default back button is 1.5 pixel lower than our extracted image. - const CGFloat kTopInsetPadding = 1.5; - backButton.imageEdgeInsets = UIEdgeInsetsMake(kTopInsetPadding, kExtraLeftPadding, 0, 0); - - CGRect buttonFrame - = CGRectMake(0, 0, backImage.size.width + kExtraRightPadding, backImage.size.height + kExtraHeightPadding); + CGRect buttonFrame = CGRectMake(0, 0, backImage.size.width + kExtraRightPadding, backImage.size.height + kExtraHeightPadding); backButton.frame = buttonFrame; - // In iOS 11.1 beta, the hot area of custom bar button items is _only_ - // the bounds of the custom view, making them very hard to hit. - // - // TODO: Remove this hack if the bug is fixed in iOS 11.1 by the time - // it goes to production (or in a later release), - // since it has two negative side effects: 1) the layout of the - // back button isn't consistent with the iOS default back buttons - // 2) we can't add the unread count badge to the back button - // with this hack. - return [[UIBarButtonItem alloc] initWithImage:backImage - style:UIBarButtonItemStylePlain - target:target - action:selector]; - - UIBarButtonItem *backItem = - [[UIBarButtonItem alloc] initWithCustomView:backButton - accessibilityIdentifier:ACCESSIBILITY_IDENTIFIER_WITH_NAME(self, @"back")]; + UIBarButtonItem *backItem = [[UIBarButtonItem alloc] initWithCustomView:backButton accessibilityIdentifier:ACCESSIBILITY_IDENTIFIER_WITH_NAME(self, @"back")]; backItem.width = buttonFrame.size.width; return backItem;