Don’t inherit from NSObject in SignalAttachment

This commit is contained in:
Max Radermacher 2025-11-25 09:33:49 -06:00 committed by GitHub
parent 0977510e1f
commit 9fe427d1fa
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 209 additions and 328 deletions

View File

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

View File

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

View File

@ -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<String>()
// 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<AttachmentApprovalItem> {
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<AttachmentApprovalItem>
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<AttachmentApprovalItem>
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<AttachmentApprovalItem>
init(result: PHPickerResult, attachmentApprovalItemPromise: Promise<AttachmentApprovalItem>) {
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)
}

View File

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

View File

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

View File

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

View File

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

View File

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