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 - - private func update() { - AssertIsOnMainThread() - - guard let thread = threadViewModel?.thread else { return } - - accentLineView.alpha = (thread.isBlocked() ? 1 : 0) - profilePictureView.update(for: thread) - displayNameLabel.text = getDisplayName() - } + // MARK: - Updating - 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;