diff --git a/Signal/ConversationView/ConversationViewController+Delegates.swift b/Signal/ConversationView/ConversationViewController+Delegates.swift index 771cd3d9ec..0ed89bcf90 100644 --- a/Signal/ConversationView/ConversationViewController+Delegates.swift +++ b/Signal/ConversationView/ConversationViewController+Delegates.swift @@ -44,7 +44,7 @@ extension ConversationViewController: AttachmentApprovalViewControllerDelegate { inputToolbar.setMessageBody(newMessageBody, animated: false) } - public func attachmentApproval(_ attachmentApproval: AttachmentApprovalViewController, didRemoveAttachment attachment: SignalAttachment) { } + public func attachmentApproval(_ attachmentApproval: AttachmentApprovalViewController, didRemoveAttachment attachmentApprovalItem: AttachmentApprovalItem) { } public func attachmentApprovalDidTapAddMore(_ attachmentApproval: AttachmentApprovalViewController) { } diff --git a/Signal/src/ViewControllers/GifPicker/GifPickerViewController.swift b/Signal/src/ViewControllers/GifPicker/GifPickerViewController.swift index e268843770..3dff6ebf63 100644 --- a/Signal/src/ViewControllers/GifPicker/GifPickerViewController.swift +++ b/Signal/src/ViewControllers/GifPicker/GifPickerViewController.swift @@ -71,7 +71,7 @@ extension GifPickerNavigationViewController: AttachmentApprovalViewControllerDel approvalDelegate?.attachmentApproval(attachmentApproval, didChangeMessageBody: newMessageBody) } - func attachmentApproval(_ attachmentApproval: AttachmentApprovalViewController, didRemoveAttachment attachment: SignalAttachment) { } + func attachmentApproval(_ attachmentApproval: AttachmentApprovalViewController, didRemoveAttachment attachmentApprovalItem: AttachmentApprovalItem) { } func attachmentApprovalDidTapAddMore(_ attachmentApproval: AttachmentApprovalViewController) { } diff --git a/Signal/src/ViewControllers/Photos/SendMediaNavigationController.swift b/Signal/src/ViewControllers/Photos/SendMediaNavigationController.swift index c2fb95a9a5..f34d5a9b3b 100644 --- a/Signal/src/ViewControllers/Photos/SendMediaNavigationController.swift +++ b/Signal/src/ViewControllers/Photos/SendMediaNavigationController.swift @@ -150,12 +150,12 @@ class SendMediaNavigationController: OWSNavigationController { navController.modalPresentationStyle = .overCurrentContext let approvalItem = AttachmentApprovalItem(attachment: attachment, canSave: false) - let libraryMedia = MediaLibraryAttachment(asset: asset, attachmentApprovalItemPromise: .value(approvalItem)) - navController.attachmentDrafts.append(.picker(attachment: libraryMedia)) + navController.pendingAttachments.append(PendingAttachment( + source: .systemLibrary(systemIdentifier: asset.localIdentifier), + approvalItem: approvalItem, + )) - navController.showApprovalViewController( - attachmentApprovalItems: [approvalItem] - ) + navController.showApprovalViewController() return navController } @@ -170,13 +170,7 @@ class SendMediaNavigationController: OWSNavigationController { setViewControllers(viewControllers, animated: false) } - // MARK: - Attachments - - private var attachmentCount: Int { - return attachmentDrafts.count - } - - private var attachmentDrafts: [AttachmentDraft] = [] + private var pendingAttachments: [PendingAttachment] = [] // MARK: - Child View Controllers @@ -191,9 +185,7 @@ class SendMediaNavigationController: OWSNavigationController { (topViewController as? AttachmentApprovalViewController)?.currentPageViewController?.canSaveMedia ?? false } - private func showApprovalViewController( - attachmentApprovalItems: [AttachmentApprovalItem] - ) { + private func showApprovalViewController() { guard let sendMediaNavDataSource = sendMediaNavDataSource else { owsFailDebug("sendMediaNavDataSource was unexpectedly nil") return @@ -214,7 +206,7 @@ class SendMediaNavigationController: OWSNavigationController { if hasQuotedReplyDraft { options.insert(.disallowViewOnce) } - let approvalViewController = AttachmentApprovalViewController(options: options, attachmentApprovalItems: attachmentApprovalItems) + let approvalViewController = AttachmentApprovalViewController(options: options, attachmentApprovalItems: pendingAttachments.map(\.approvalItem)) approvalViewController.approvalDelegate = self approvalViewController.approvalDataSource = self approvalViewController.stickerSheetDelegate = self @@ -233,26 +225,31 @@ class SendMediaNavigationController: OWSNavigationController { } private func didRequestExit(dontAbandonText: String) { - if attachmentDrafts.count == 0 { + if self.pendingAttachments.isEmpty { self.sendMediaNavDelegate?.sendMediaNavDidCancel(self) } else { let alert = ActionSheetController() alert.overrideUserInterfaceStyle = .dark - let confirmAbandonText = OWSLocalizedString("SEND_MEDIA_CONFIRM_ABANDON_ALBUM", - comment: "alert action, confirming the user wants to exit the media flow and abandon any photos they've taken") - let confirmAbandonAction = ActionSheetAction(title: confirmAbandonText, - style: .destructive, - handler: { [weak self] _ in - guard let self = self else { return } - self.sendMediaNavDelegate?.sendMediaNavDidCancel(self) - }) + let confirmAbandonText = OWSLocalizedString( + "SEND_MEDIA_CONFIRM_ABANDON_ALBUM", + comment: "alert action, confirming the user wants to exit the media flow and abandon any photos they've taken", + ) + let confirmAbandonAction = ActionSheetAction( + title: confirmAbandonText, + style: .destructive, + handler: { [weak self] _ in + guard let self = self else { return } + self.sendMediaNavDelegate?.sendMediaNavDidCancel(self) + }, + ) alert.addAction(confirmAbandonAction) - let dontAbandonAction = ActionSheetAction(title: dontAbandonText, - style: .default, - handler: { _ in }) + let dontAbandonAction = ActionSheetAction( + title: dontAbandonText, + style: .default, + handler: { _ in }, + ) alert.addAction(dontAbandonAction) - self.presentActionSheet(alert) } } @@ -275,15 +272,14 @@ extension SendMediaNavigationController { extension SendMediaNavigationController: PhotoCaptureViewControllerDelegate { func photoCaptureViewControllerDidFinish(_ photoCaptureViewController: PhotoCaptureViewController) { - guard attachmentDrafts.count > 0 else { - owsFailDebug("No camera attachments found") - return - } - showApprovalAfterProcessingAnyMediaLibrarySelections() + owsPrecondition(numberOfMediaItems > 0) + showApprovalViewController() } - func photoCaptureViewController(_ photoCaptureViewController: PhotoCaptureViewController, - didFinishWithTextAttachment textAttachment: UnsentTextAttachment) { + func photoCaptureViewController( + _ photoCaptureViewController: PhotoCaptureViewController, + didFinishWithTextAttachment textAttachment: UnsentTextAttachment, + ) { sendMediaNavDelegate?.sendMediaNav(self, didFinishWithTextAttachment: textAttachment) } @@ -293,9 +289,13 @@ extension SendMediaNavigationController: PhotoCaptureViewControllerDelegate { } func photoCaptureViewControllerViewWillAppear(_ photoCaptureViewController: PhotoCaptureViewController) { - if photoCaptureViewController.captureMode == .single, attachmentCount == 1, case .camera = attachmentDrafts.last { + if + photoCaptureViewController.captureMode == .single, + self.pendingAttachments.count == 1, + case .camera = self.pendingAttachments.last?.source + { // User is navigating back to the camera screen, indicating they want to discard the previously captured item. - discardDraft() + self.pendingAttachments = [] } } @@ -304,45 +304,45 @@ extension SendMediaNavigationController: PhotoCaptureViewControllerDelegate { } func photoCaptureViewControllerCanCaptureMoreItems(_ photoCaptureViewController: PhotoCaptureViewController) -> Bool { - return attachmentCount < SignalAttachment.maxAttachmentsAllowed - } - - func discardDraft() { - owsAssertDebug(attachmentDrafts.count <= 1) - if let lastAttachmentDraft = attachmentDrafts.last { - attachmentDrafts.removeAll { $0 == lastAttachmentDraft } - } - owsAssertDebug(attachmentDrafts.count == 0) + return self.pendingAttachments.count < SignalAttachment.maxAttachmentsAllowed } func photoCaptureViewControllerDidRequestPresentPhotoLibrary(_ photoCaptureViewController: PhotoCaptureViewController) { presentAdditionalPhotosPicker() } - func photoCaptureViewController(_ photoCaptureViewController: PhotoCaptureViewController, - didRequestSwitchCaptureModeTo captureMode: PhotoCaptureViewController.CaptureMode, - completion: @escaping (Bool) -> Void) { + func photoCaptureViewController( + _ photoCaptureViewController: PhotoCaptureViewController, + didRequestSwitchCaptureModeTo captureMode: PhotoCaptureViewController.CaptureMode, + completion: @escaping (Bool) -> Void, + ) { // .multi always can be enabled. guard captureMode == .single else { completion(true) return } // Disable immediately if there's no media attachments yet. - guard attachmentCount > 0 else { + guard !self.pendingAttachments.isEmpty else { completion(true) return } // Ask to delete all existing media attachments. - let title = OWSLocalizedString("SEND_MEDIA_TURN_OFF_MM_TITLE", - comment: "In-app camera: title for the prompt to turn off multi-mode that will cause previously taken photos to be discarded.") - let message = OWSLocalizedString("SEND_MEDIA_TURN_OFF_MM_MESSAGE", - comment: "In-app camera: message for the prompt to turn off multi-mode that will cause previously taken photos to be discarded.") - let buttonTitle = OWSLocalizedString("SEND_MEDIA_TURN_OFF_MM_BUTTON", - comment: "In-app camera: confirmation button in the prompt to turn off multi-mode.") + let title = OWSLocalizedString( + "SEND_MEDIA_TURN_OFF_MM_TITLE", + comment: "In-app camera: title for the prompt to turn off multi-mode that will cause previously taken photos to be discarded.", + ) + let message = OWSLocalizedString( + "SEND_MEDIA_TURN_OFF_MM_MESSAGE", + comment: "In-app camera: message for the prompt to turn off multi-mode that will cause previously taken photos to be discarded.", + ) + let buttonTitle = OWSLocalizedString( + "SEND_MEDIA_TURN_OFF_MM_BUTTON", + comment: "In-app camera: confirmation button in the prompt to turn off multi-mode.", + ) let actionSheet = ActionSheetController(title: title, message: message) actionSheet.overrideUserInterfaceStyle = .dark actionSheet.addAction(ActionSheetAction(title: buttonTitle, style: .destructive) { _ in - self.attachmentDrafts.removeAll() + self.pendingAttachments.removeAll() completion(true) }) actionSheet.addAction(ActionSheetAction(title: CommonStrings.cancelButton, style: .cancel) { _ in @@ -359,105 +359,111 @@ extension SendMediaNavigationController: PhotoCaptureViewControllerDelegate { extension SendMediaNavigationController: PhotoCaptureViewControllerDataSource { var numberOfMediaItems: Int { - attachmentCount + return self.pendingAttachments.count } func addMedia(attachment: SignalAttachment) { - let cameraCaptureAttachment = CameraCaptureAttachment(signalAttachment: attachment) - attachmentDrafts.append(.camera(attachment: cameraCaptureAttachment)) + self.pendingAttachments.append(PendingAttachment( + source: .camera, + approvalItem: AttachmentApprovalItem(attachment: attachment, canSave: true), + )) } } extension SendMediaNavigationController: PHPickerViewControllerDelegate { private struct PHPickerResultsLoadResult { - let attachmentDrafts: [AttachmentDraft] + let resolvablePendingAttachments: [() async throws -> PendingAttachment] let didAddAttachments: Bool } - /// Load the `results` in the order they are given. Any other existing - /// `AttachmentDraft`s will be removed from `self.attachmentDrafts`. - private func loadOrderedPHPickerResults( - _ results: [PHPickerResult] - ) -> PHPickerResultsLoadResult { + /// Load the `results` in the order they are given. + private func loadOrderedPHPickerResults(_ results: [PHPickerResult]) -> PHPickerResultsLoadResult { var didAddAttachments = false - let attachmentDraftsByAssetID: [String: AttachmentDraft] = Dictionary( - uniqueKeysWithValues: attachmentDrafts.compactMap { attachmentDraft in - guard let systemID = attachmentDraft.systemIdentifier else { + + let pendingAttachmentByAssetIdentifier: [String: PendingAttachment] = Dictionary( + uniqueKeysWithValues: self.pendingAttachments.compactMap { (pendingAttachment) -> (String, PendingAttachment)? in + switch pendingAttachment.source { + case .camera: return nil + case .systemLibrary(let systemIdentifier): + return (systemIdentifier, pendingAttachment) } - return (systemID, attachmentDraft) } ) - let attachmentDrafts: [AttachmentDraft] = results.map { result in - if - let assetID = result.assetIdentifier, - let existingItem = attachmentDraftsByAssetID[assetID] - { - return existingItem + let resolvablePendingAttachments = results.compactMap { (result) -> (() async throws -> PendingAttachment)? in + guard let assetIdentifier = result.assetIdentifier else { + owsFailDebug("can't select asset without an identifier") + return nil } - - didAddAttachments = true - let attachment = PHPickerAttachment( - result: result, - attachmentApprovalItemPromise: Promise.wrapAsync { - let attachment = try await TypedItemProvider.buildVisualMediaAttachment(forItemProvider: result.itemProvider) - return AttachmentApprovalItem(attachment: attachment, canSave: false) + if let pendingAttachment = pendingAttachmentByAssetIdentifier[assetIdentifier] { + return { + return pendingAttachment } - ) - return .phPicker(attachment: attachment) + } else { + didAddAttachments = true + return { + let attachment = try await TypedItemProvider.buildVisualMediaAttachment(forItemProvider: result.itemProvider) + let approvalItem = AttachmentApprovalItem(attachment: attachment, canSave: false) + return PendingAttachment(source: .systemLibrary(systemIdentifier: assetIdentifier), approvalItem: approvalItem) + } + } } return PHPickerResultsLoadResult( - attachmentDrafts: attachmentDrafts, - didAddAttachments: didAddAttachments + resolvablePendingAttachments: resolvablePendingAttachments, + didAddAttachments: didAddAttachments, ) } - /// Load the `results` on top of the existing `AttachmentDraft`s in - /// `self.attachmentDrafts`, adding new items to the end. - private func loadUnorderedPHPickerResults( - _ results: [PHPickerResult] - ) -> PHPickerResultsLoadResult { - var attachmentDrafts = self.attachmentDrafts - var oldAttachments = Set(attachmentDrafts.attachmentSystemIdentifiers) - var didAddAttachments = false - results.filter { result in - if let assetID = result.assetIdentifier { - let removedItem = oldAttachments.remove(assetID) - let alreadyIncluded = removedItem != nil - if alreadyIncluded { - return false - } + /// Load the `results` on top of the existing `pendingAttachments`. + private func loadUnorderedPHPickerResults(_ results: [PHPickerResult]) -> PHPickerResultsLoadResult { + let selectedAssetIdentifiers = Set(results.compactMap(\.assetIdentifier)) + var existingAssetIdentifiers = Set() + + // Keep any attachments from the camera or that are still selected. + var resolvablePendingAttachments = [() async throws -> PendingAttachment]() + for pendingAttachment in self.pendingAttachments { + let shouldKeep: Bool + switch pendingAttachment.source { + case .camera: + shouldKeep = true + case .systemLibrary(let systemIdentifier): + existingAssetIdentifiers.insert(systemIdentifier) + shouldKeep = selectedAssetIdentifiers.contains(systemIdentifier) + } + if shouldKeep { + resolvablePendingAttachments.append({ return pendingAttachment }) } - didAddAttachments = true - return true - }.map { result in - PHPickerAttachment( - result: result, - attachmentApprovalItemPromise: Promise.wrapAsync { - let attachment = try await TypedItemProvider.buildVisualMediaAttachment(forItemProvider: result.itemProvider) - return AttachmentApprovalItem(attachment: attachment, canSave: false) - } - ) - }.forEach { attachment in - attachmentDrafts.append(.phPicker(attachment: attachment)) } - // Anything left in here was deselected - oldAttachments.forEach { oldAttachment in - attachmentDrafts.remove(itemWithSystemID: oldAttachment) + // Add any newly-selected attachments + var didAddAttachments = false + for result in results { + guard let assetIdentifier = result.assetIdentifier else { + owsFailDebug("can't select asset without an identifier") + continue + } + if existingAssetIdentifiers.contains(assetIdentifier) { + continue + } + didAddAttachments = true + resolvablePendingAttachments.append({ + let attachment = try await TypedItemProvider.buildVisualMediaAttachment(forItemProvider: result.itemProvider) + let approvalItem = AttachmentApprovalItem(attachment: attachment, canSave: false) + return PendingAttachment(source: .systemLibrary(systemIdentifier: assetIdentifier), approvalItem: approvalItem) + }) } return PHPickerResultsLoadResult( - attachmentDrafts: attachmentDrafts, - didAddAttachments: didAddAttachments + resolvablePendingAttachments: resolvablePendingAttachments, + didAddAttachments: didAddAttachments, ) } func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) { - let loadResult = if attachmentDrafts.contains(where: { - if case .camera = $0 { true } else { false } + let loadResult = if self.pendingAttachments.contains(where: { + return if case .camera = $0.source { true } else { false } }) { // When there are camera attachments, there isn't a straightforward // way to handle re-ordering selection, so we just drop the order @@ -466,28 +472,30 @@ extension SendMediaNavigationController: PHPickerViewControllerDelegate { loadOrderedPHPickerResults(results) } - self.attachmentDrafts = loadResult.attachmentDrafts - if !loadResult.didAddAttachments, viewControllers.first is PhotoCaptureViewController { picker.dismiss(animated: true) - if attachmentCount <= 0 { + if self.pendingAttachments.isEmpty { captureViewController.captureMode = .single } captureViewController.updateDoneButtonAppearance() return } - if attachmentCount <= 0 { + if loadResult.resolvablePendingAttachments.isEmpty { + self.pendingAttachments = [] // The user tapped the cancel button or deselected everything self.view.layer.opacity = 0 self.sendMediaNavDelegate?.sendMediaNavDidCancel(self) return } - showApprovalAfterProcessingAnyMediaLibrarySelections(picker: picker) + showApprovalAfterProcessing( + resolvablePendingAttachments: loadResult.resolvablePendingAttachments, + pickerViewController: picker, + ) } } @@ -498,27 +506,28 @@ extension SendMediaNavigationController: UIAdaptivePresentationControllerDelegat } } -extension SendMediaNavigationController { - func showApprovalAfterProcessingAnyMediaLibrarySelections( - picker: PHPickerViewController? = nil +private extension SendMediaNavigationController { + func showApprovalAfterProcessing( + resolvablePendingAttachments: [() async throws -> PendingAttachment], + pickerViewController: PHPickerViewController, ) { ModalActivityIndicatorViewController.present( - fromViewController: picker ?? self, + fromViewController: pickerViewController, canCancel: true, asyncBlock: { modal in - let result = await Result<[AttachmentApprovalItem], any Error> { - var attachmentApprovalItems = [AttachmentApprovalItem]() - for attachmentApprovalItemPromise in self.attachmentDrafts.map(\.attachmentApprovalItemPromise) { + let result = await Result<[PendingAttachment], any Error> { + var pendingAttachments = [PendingAttachment]() + for resolvablePendingAttachment in resolvablePendingAttachments { try Task.checkCancellation() - attachmentApprovalItems.append(try await attachmentApprovalItemPromise.awaitable()) + pendingAttachments.append(try await resolvablePendingAttachment()) } - return attachmentApprovalItems + return pendingAttachments } modal.dismissIfNotCanceled(completionIfNotCanceled: { do { - let attachmentApprovalItems = try result.get() - self.showApprovalViewController(attachmentApprovalItems: attachmentApprovalItems) - picker?.dismiss(animated: true) + self.pendingAttachments = try result.get() + self.showApprovalViewController() + pickerViewController.dismiss(animated: true) } catch SignalAttachmentError.fileSizeTooLarge { OWSActionSheets.showActionSheet( title: OWSLocalizedString( @@ -546,13 +555,8 @@ extension SendMediaNavigationController: AttachmentApprovalViewControllerDelegat sendMediaNavDelegate?.sendMediaNav(self, didChangeViewOnceState: isViewOnce) } - func attachmentApproval(_ attachmentApproval: AttachmentApprovalViewController, didRemoveAttachment attachment: SignalAttachment) { - guard let removedDraft = attachmentDrafts.attachmentDraft(for: attachment) else { - owsFailDebug("removedDraft was unexpectedly nil") - return - } - - attachmentDrafts.removeAll { $0 == removedDraft } + func attachmentApproval(_ attachmentApproval: AttachmentApprovalViewController, didRemoveAttachment attachmentApprovalItem: AttachmentApprovalItem) { + self.pendingAttachments.removeAll(where: { $0.approvalItem.isIdenticalTo(attachmentApprovalItem) }) } func attachmentApproval( @@ -585,9 +589,19 @@ extension SendMediaNavigationController: AttachmentApprovalViewControllerDelegat private func presentAdditionalPhotosPicker() { var config = Self.phPickerConfiguration( - cameraAttachmentCount: attachmentDrafts.cameraAttachmentCount + cameraAttachmentCount: self.pendingAttachments.count(where: { + switch $0.source { + case .camera: true + case .systemLibrary: false + } + }), ) - config.preselectedAssetIdentifiers = attachmentDrafts.attachmentSystemIdentifiers + config.preselectedAssetIdentifiers = self.pendingAttachments.compactMap({ + switch $0.source { + case .camera: nil + case .systemLibrary(let systemIdentifier): systemIdentifier + } + }) let vc = PHPickerViewController(configuration: config) vc.delegate = self @@ -625,133 +639,12 @@ extension SendMediaNavigationController: StickerPickerSheetDelegate { } } -private enum AttachmentDraft: Equatable { - - case camera(attachment: CameraCaptureAttachment) - - case picker(attachment: MediaLibraryAttachment) - - case phPicker(attachment: PHPickerAttachment) +private struct PendingAttachment { + var source: AttachmentSource + var approvalItem: AttachmentApprovalItem } -private extension AttachmentDraft { - var attachmentApprovalItemPromise: Promise { - switch self { - case .camera(let cameraAttachment): - return cameraAttachment.attachmentApprovalItemPromise - case .picker(let pickerAttachment): - return pickerAttachment.attachmentApprovalItemPromise - case .phPicker(let phPickerAttachment): - return phPickerAttachment.attachmentApprovalItemPromise - } - } - - var systemIdentifier: String? { - switch self { - case .picker(let attachment): - attachment.asset.localIdentifier - case .camera: - nil - case .phPicker(let phPickerAttachment): - phPickerAttachment.result.assetIdentifier - } - } -} - -// MARK: - AttachmentDrafts - -private extension Array where Element == AttachmentDraft { - var cameraAttachmentCount: Int { - count { attachmentDraft in - switch attachmentDraft { - case .camera: true - case .picker, .phPicker: false - } - } - } - - var attachmentSystemIdentifiers: [String] { - compactMap { attachmentDraft in - attachmentDraft.systemIdentifier - } - } - - mutating func remove(itemWithSystemID id: String) { - self.removeAll { item in - switch item { - case .camera: - false - case .picker(let attachment): - attachment.asset.localIdentifier == id - case .phPicker(let attachment): - attachment.result.assetIdentifier == id - } - } - } - - func attachmentDraft(for attachment: SignalAttachment) -> AttachmentDraft? { - self.first { attachmentDraft in - guard let attachmentApprovalItem = attachmentDraft.attachmentApprovalItemPromise.value else { - // method should only be used after draft promises have been resolved. - owsFailDebug("attachment was unexpectedly nil") - return false - } - return attachmentApprovalItem.attachment == attachment - } - } -} - -// MARK: - CameraCaptureAttachment - -private struct CameraCaptureAttachment: Hashable, Equatable { - - let signalAttachment: SignalAttachment - let attachmentApprovalItem: AttachmentApprovalItem - let attachmentApprovalItemPromise: Promise - - init(signalAttachment: SignalAttachment) { - self.signalAttachment = signalAttachment - self.attachmentApprovalItem = AttachmentApprovalItem(attachment: signalAttachment, canSave: true) - self.attachmentApprovalItemPromise = Promise.value(attachmentApprovalItem) - } - - func hash(into hasher: inout Hasher) { - hasher.combine(signalAttachment) - } - - static func == (lhs: CameraCaptureAttachment, rhs: CameraCaptureAttachment) -> Bool { - return lhs.signalAttachment == rhs.signalAttachment - } -} - -private struct MediaLibraryAttachment: Hashable, Equatable { - - let asset: PHAsset - let attachmentApprovalItemPromise: Promise - - func hash(into hasher: inout Hasher) { - hasher.combine(asset) - } - - static func == (lhs: MediaLibraryAttachment, rhs: MediaLibraryAttachment) -> Bool { - return lhs.asset == rhs.asset - } -} - -private struct PHPickerAttachment: Hashable { - let result: PHPickerResult - let attachmentApprovalItemPromise: Promise - - init(result: PHPickerResult, attachmentApprovalItemPromise: Promise) { - self.result = result - self.attachmentApprovalItemPromise = attachmentApprovalItemPromise - } - - func hash(into hasher: inout Hasher) { - hasher.combine(result) - } - - static func == (lhs: PHPickerAttachment, rhs: PHPickerAttachment) -> Bool { - return lhs.result == rhs.result - } +private enum AttachmentSource { + case camera + case systemLibrary(systemIdentifier: String) } diff --git a/SignalServiceKit/Attachments/SignalAttachment.swift b/SignalServiceKit/Attachments/SignalAttachment.swift index 1ab10dcff5..7f323eba2e 100644 --- a/SignalServiceKit/Attachments/SignalAttachment.swift +++ b/SignalServiceKit/Attachments/SignalAttachment.swift @@ -63,7 +63,7 @@ extension SignalAttachmentError: LocalizedError, UserErrorDescriptionProvider { // // TODO: Perhaps do conversion off the main thread? -public class SignalAttachment: NSObject { +public class SignalAttachment: CustomDebugStringConvertible { // MARK: Properties @@ -94,7 +94,6 @@ public class SignalAttachment: NSObject { private init(dataSource: DataSource, dataUTI: String) { self.dataSource = dataSource self.dataUTI = dataUTI - super.init() NotificationCenter.default.addObserver( self, @@ -113,7 +112,7 @@ public class SignalAttachment: NSObject { // MARK: Methods - public override var debugDescription: String { + public var debugDescription: String { let fileSize = ByteCountFormatter.string(fromByteCount: Int64(dataSource.dataLength), countStyle: .file) return "[SignalAttachment] mimeType: \(mimeType), fileSize: \(fileSize)" } diff --git a/SignalShareExtension/SharingThreadPickerViewController.swift b/SignalShareExtension/SharingThreadPickerViewController.swift index 87dedbf6cd..63a0442093 100644 --- a/SignalShareExtension/SharingThreadPickerViewController.swift +++ b/SignalShareExtension/SharingThreadPickerViewController.swift @@ -669,7 +669,7 @@ extension SharingThreadPickerViewController: AttachmentApprovalViewControllerDel // We can ignore this event. } - func attachmentApproval(_ attachmentApproval: AttachmentApprovalViewController, didRemoveAttachment attachment: SignalAttachment) { + func attachmentApproval(_ attachmentApproval: AttachmentApprovalViewController, didRemoveAttachment attachmentApprovalItem: AttachmentApprovalItem) { // We can ignore this event. } diff --git a/SignalUI/AttachmentApproval/AttachmentApprovalToolbar.swift b/SignalUI/AttachmentApproval/AttachmentApprovalToolbar.swift index c05da86036..b4cc51a5c8 100644 --- a/SignalUI/AttachmentApproval/AttachmentApprovalToolbar.swift +++ b/SignalUI/AttachmentApproval/AttachmentApprovalToolbar.swift @@ -240,11 +240,9 @@ class AttachmentApprovalToolbar: UIView { // first responder; } - func update(currentAttachmentItem: AttachmentApprovalItem, - configuration: Configuration, - animated: Bool) { + func update(currentAttachmentItem: AttachmentApprovalItem, configuration: Configuration, animated: Bool) { // De-bounce - guard self.currentAttachmentItem != currentAttachmentItem || self.configuration != configuration else { + if currentAttachmentItem.isIdenticalTo(self.currentAttachmentItem as AttachmentApprovalItem?), self.configuration == configuration { updateFirstResponder() return } diff --git a/SignalUI/AttachmentApproval/AttachmentApprovalViewController.swift b/SignalUI/AttachmentApproval/AttachmentApprovalViewController.swift index bd02fb875e..49368da0d2 100644 --- a/SignalUI/AttachmentApproval/AttachmentApprovalViewController.swift +++ b/SignalUI/AttachmentApproval/AttachmentApprovalViewController.swift @@ -53,7 +53,7 @@ public protocol AttachmentApprovalViewControllerDelegate: AnyObject { didChangeViewOnceState isViewOnce: Bool ) - func attachmentApproval(_ attachmentApproval: AttachmentApprovalViewController, didRemoveAttachment attachment: SignalAttachment) + func attachmentApproval(_ attachmentApproval: AttachmentApprovalViewController, didRemoveAttachment attachmentApprovalItem: AttachmentApprovalItem) func attachmentApprovalDidTapAddMore(_ attachmentApproval: AttachmentApprovalViewController) } @@ -451,7 +451,7 @@ public class AttachmentApprovalViewController: UIPageViewController, UIPageViewC // MARK: - View Helpers func remove(attachmentApprovalItem: AttachmentApprovalItem) { - if attachmentApprovalItem == currentItem { + if attachmentApprovalItem.isIdenticalTo(currentItem) { if let nextItem = attachmentApprovalItemCollection.itemAfter(item: attachmentApprovalItem) { setCurrentItem(nextItem, direction: .forward, animated: true) } else if let prevItem = attachmentApprovalItemCollection.itemBefore(item: attachmentApprovalItem) { @@ -465,7 +465,7 @@ public class AttachmentApprovalViewController: UIPageViewController, UIPageViewC } attachmentApprovalItemCollection.remove(item: attachmentApprovalItem) - approvalDelegate?.attachmentApproval(self, didRemoveAttachment: attachmentApprovalItem.attachment) + approvalDelegate?.attachmentApproval(self, didRemoveAttachment: attachmentApprovalItem) // If media rail needs to be hidden, do it immediately. if attachmentApprovalItems.count < 2 { @@ -574,15 +574,12 @@ public class AttachmentApprovalViewController: UIPageViewController, UIPageViewC return currentPageViewController?.attachmentApprovalItem } - private var cachedPages: [AttachmentApprovalItem: AttachmentPrepViewController] = [:] + private var cachedPages: [(key: AttachmentApprovalItem, value: AttachmentPrepViewController)] = [] private func buildPage(item: AttachmentApprovalItem) -> AttachmentPrepViewController? { - - if let cachedPage = cachedPages[item] { - Logger.debug("cache hit.") - return cachedPage + if let cachedPage = cachedPages.first(where: { $0.key.isIdenticalTo(item) }) { + return cachedPage.value } - Logger.debug("cache miss.") guard let viewController = AttachmentPrepViewController.viewController( for: item, stickerSheetDelegate: stickerSheetDelegate @@ -592,15 +589,16 @@ public class AttachmentApprovalViewController: UIPageViewController, UIPageViewC } viewController.prepDelegate = self - cachedPages[item] = viewController + cachedPages.append((item, viewController)) return viewController } - private func setCurrentItem(_ item: AttachmentApprovalItem, - direction: UIPageViewController.NavigationDirection, - animated: Bool) { - + private func setCurrentItem( + _ item: AttachmentApprovalItem, + direction: UIPageViewController.NavigationDirection, + animated: Bool, + ) { guard let page = buildPage(item: item) else { owsFailDebug("unexpectedly unable to build new page") return @@ -769,7 +767,7 @@ public class AttachmentApprovalViewController: UIPageViewController, UIPageViewC } func attachmentApprovalItem(before currentItem: AttachmentApprovalItem) -> AttachmentApprovalItem? { - guard let currentIndex = attachmentApprovalItems.firstIndex(of: currentItem) else { + guard let currentIndex = attachmentApprovalItems.firstIndex(where: { $0.isIdenticalTo(currentItem) }) else { owsFailDebug("currentIndex was unexpectedly nil") return nil } @@ -784,7 +782,7 @@ public class AttachmentApprovalViewController: UIPageViewController, UIPageViewC } func attachmentApprovalItem(after currentItem: AttachmentApprovalItem) -> AttachmentApprovalItem? { - guard let currentIndex = attachmentApprovalItems.firstIndex(of: currentItem) else { + guard let currentIndex = attachmentApprovalItems.firstIndex(where: { $0.isIdenticalTo(currentItem) }) else { owsFailDebug("currentIndex was unexpectedly nil") return nil } @@ -1334,17 +1332,19 @@ extension AttachmentApprovalViewController: AttachmentPrepViewControllerDelegate // MARK: GalleryRail extension AttachmentApprovalItem: GalleryRailItem { - public func buildRailItemView() -> UIView { let imageView = UIImageView() imageView.contentMode = .scaleAspectFill imageView.image = getThumbnailImage() return imageView } + + public func isEqualToGalleryRailItem(_ other: (any GalleryRailItem)?) -> Bool { + return self.isIdenticalTo(other as? Self) + } } -extension AddMoreRailItem: GalleryRailItem { - +class AddMoreRailItem: GalleryRailItem { func buildRailItemView() -> UIView { let button = RoundMediaButton( image: UIImage(imageLiteralResourceName: "plus-square-28"), @@ -1355,6 +1355,10 @@ extension AddMoreRailItem: GalleryRailItem { button.ows_contentEdgeInsets = .zero return button } + + func isEqualToGalleryRailItem(_ other: (any GalleryRailItem)?) -> Bool { + return other is Self + } } // MARK: - @@ -1385,13 +1389,15 @@ extension AttachmentApprovalViewController: GalleryRailViewDelegate { return } - guard let currentItem = currentItem, - let currentIndex = attachmentApprovalItems.firstIndex(of: currentItem) else { + guard + let currentItem = currentItem, + let currentIndex = attachmentApprovalItems.firstIndex(where: { $0.isIdenticalTo(currentItem) }) + else { owsFailDebug("currentIndex was unexpectedly nil") return } - guard let targetIndex = attachmentApprovalItems.firstIndex(of: targetItem) else { + guard let targetIndex = attachmentApprovalItems.firstIndex(where: { $0.isIdenticalTo(targetItem) }) else { owsFailDebug("targetIndex was unexpectedly nil") return } diff --git a/SignalUI/AttachmentApproval/AttachmentItemCollection.swift b/SignalUI/AttachmentApproval/AttachmentItemCollection.swift index b8b3534a3c..4d4eb6aed3 100644 --- a/SignalUI/AttachmentApproval/AttachmentItemCollection.swift +++ b/SignalUI/AttachmentApproval/AttachmentItemCollection.swift @@ -6,14 +6,7 @@ import Foundation public import SignalServiceKit -class AddMoreRailItem: Equatable { - - static func == (lhs: AddMoreRailItem, rhs: AddMoreRailItem) -> Bool { - return true - } -} - -public struct AttachmentApprovalItem: Hashable { +public class AttachmentApprovalItem { enum AttachmentApprovalItemError: Error { case noThumbnail @@ -96,16 +89,8 @@ public struct AttachmentApprovalItem: Hashable { return self.attachment.staticThumbnail() } - // MARK: Hashable - - public func hash(into hasher: inout Hasher) { - return hasher.combine(attachment) - } - - // MARK: Equatable - - public static func == (lhs: AttachmentApprovalItem, rhs: AttachmentApprovalItem) -> Bool { - return lhs.attachment == rhs.attachment + public func isIdenticalTo(_ other: AttachmentApprovalItem?) -> Bool { + return self === other } } @@ -121,7 +106,7 @@ class AttachmentApprovalItemCollection { } func itemAfter(item: AttachmentApprovalItem) -> AttachmentApprovalItem? { - guard let currentIndex = attachmentApprovalItems.firstIndex(of: item) else { + guard let currentIndex = attachmentApprovalItems.firstIndex(where: { $0.isIdenticalTo(item) }) else { owsFailDebug("currentIndex was unexpectedly nil") return nil } @@ -132,7 +117,7 @@ class AttachmentApprovalItemCollection { } func itemBefore(item: AttachmentApprovalItem) -> AttachmentApprovalItem? { - guard let currentIndex = attachmentApprovalItems.firstIndex(of: item) else { + guard let currentIndex = attachmentApprovalItems.firstIndex(where: { $0.isIdenticalTo(item) }) else { owsFailDebug("currentIndex was unexpectedly nil") return nil } @@ -143,7 +128,7 @@ class AttachmentApprovalItemCollection { } func remove(item: AttachmentApprovalItem) { - attachmentApprovalItems = attachmentApprovalItems.filter { $0 != item } + attachmentApprovalItems.removeAll(where: { $0.isIdenticalTo(item) }) } var count: Int {