diff --git a/WireMessaging/Sources/WireMessagingAssembly/WireCellsFactory.swift b/WireMessaging/Sources/WireMessagingAssembly/WireCellsFactory.swift index 3da25d9ac04..2cbc5aad0dc 100644 --- a/WireMessaging/Sources/WireMessagingAssembly/WireCellsFactory.swift +++ b/WireMessaging/Sources/WireMessagingAssembly/WireCellsFactory.swift @@ -116,6 +116,14 @@ public struct WireCellsFactory { ) } + public func makeDeleteNodesUseCase() -> any WireCellsDeleteNodesUseCaseProtocol { + WireCellsDeleteNodesUseCase( + repository: nodesAPI, + fileCache: fileCache, + localAssetStore: localAssetStore + ) + } + } public extension WireCellsFactory { diff --git a/WireMessaging/Sources/WireMessagingDomain/WireCells/Protocols/WireCellsDeleteNodesUseCaseProtocol.swift b/WireMessaging/Sources/WireMessagingDomain/WireCells/Protocols/WireCellsDeleteNodesUseCaseProtocol.swift new file mode 100644 index 00000000000..11067fd535e --- /dev/null +++ b/WireMessaging/Sources/WireMessagingDomain/WireCells/Protocols/WireCellsDeleteNodesUseCaseProtocol.swift @@ -0,0 +1,25 @@ +// +// Wire +// Copyright (C) 2025 Wire Swiss GmbH +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see http://www.gnu.org/licenses/. +// + +public import Foundation + +public protocol WireCellsDeleteNodesUseCaseProtocol: Sendable { + + func invoke(nodeIDs: [UUID]) async throws + +} diff --git a/WireMessaging/Sources/WireMessagingDomain/WireCells/UseCases/WireCellsDeleteNodesUseCase.swift b/WireMessaging/Sources/WireMessagingDomain/WireCells/UseCases/WireCellsDeleteNodesUseCase.swift index c38f94d2811..047f1c71757 100644 --- a/WireMessaging/Sources/WireMessagingDomain/WireCells/UseCases/WireCellsDeleteNodesUseCase.swift +++ b/WireMessaging/Sources/WireMessagingDomain/WireCells/UseCases/WireCellsDeleteNodesUseCase.swift @@ -23,7 +23,7 @@ package enum WireCellsDeleteNodesError: Error { } /// Deletes `WireCellNodes`s from both the server and locally cached data. -package struct WireCellsDeleteNodesUseCase: Sendable { +package struct WireCellsDeleteNodesUseCase: WireCellsDeleteNodesUseCaseProtocol { private let repository: any WireCellsNodesRepositoryProtocol private let fileCache: any FileCache diff --git a/wire-ios/Tests/Sourcery/generated/AutoMockable.generated.swift b/wire-ios/Tests/Sourcery/generated/AutoMockable.generated.swift index 2663387d408..46faa7bce4a 100644 --- a/wire-ios/Tests/Sourcery/generated/AutoMockable.generated.swift +++ b/wire-ios/Tests/Sourcery/generated/AutoMockable.generated.swift @@ -1782,6 +1782,24 @@ class MockWireCellsFactoryProtocol: WireCellsFactoryProtocol { } } + // MARK: - makeDeleteNodesUseCase + + var makeDeleteNodesUseCase_Invocations: [Void] = [] + var makeDeleteNodesUseCase_MockMethod: (() -> WireCellsDeleteNodesUseCaseProtocol)? + var makeDeleteNodesUseCase_MockValue: WireCellsDeleteNodesUseCaseProtocol? + + func makeDeleteNodesUseCase() -> WireCellsDeleteNodesUseCaseProtocol { + makeDeleteNodesUseCase_Invocations.append(()) + + if let mock = makeDeleteNodesUseCase_MockMethod { + return mock() + } else if let mock = makeDeleteNodesUseCase_MockValue { + return mock + } else { + fatalError("no mock for `makeDeleteNodesUseCase`") + } + } + // MARK: - makeFilesView var makeFilesViewCellNameIsCellsStatePendingNodeIDs_Invocations: [(cellName: String, isCellsStatePending: Bool, nodeIDs: [UUID])] = [] diff --git a/wire-ios/Wire-iOS Tests/ConversationContentViewControllerTests.swift b/wire-ios/Wire-iOS Tests/ConversationContentViewControllerTests.swift index 82a62f2566e..f47306045b8 100644 --- a/wire-ios/Wire-iOS Tests/ConversationContentViewControllerTests.swift +++ b/wire-ios/Wire-iOS Tests/ConversationContentViewControllerTests.swift @@ -79,6 +79,6 @@ final class ConversationContentViewControllerTests: XCTestCase, CoreDataFixtureT message: mockMessage, sourceView: view, userSession: userSession - ) { _ in }) + ) { _, _ in }) } } diff --git a/wire-ios/Wire-iOS/Sources/UserInterface/Collections/CollectionsViewController.swift b/wire-ios/Wire-iOS/Sources/UserInterface/Collections/CollectionsViewController.swift index 2a2db8571f7..f57c4e14f8c 100644 --- a/wire-ios/Wire-iOS/Sources/UserInterface/Collections/CollectionsViewController.swift +++ b/wire-ios/Wire-iOS/Sources/UserInterface/Collections/CollectionsViewController.swift @@ -797,7 +797,7 @@ extension CollectionsViewController: CollectionCellDelegate { forMessage: message, source: source, userSession: userSession - ) { [weak self] deleted in + ) { [weak self] deleted, _ in guard deleted else { return } _ = self?.navigationController?.popViewController(animated: true) self?.refetchCollection() diff --git a/wire-ios/Wire-iOS/Sources/UserInterface/Conversation/Content/Cells/ConfigurationMessageCell/Content/Rich Content/ConversationMultipartMessageCell.swift b/wire-ios/Wire-iOS/Sources/UserInterface/Conversation/Content/Cells/ConfigurationMessageCell/Content/Rich Content/ConversationMultipartMessageCell.swift index b35ee0b7c4a..c7084b80f08 100644 --- a/wire-ios/Wire-iOS/Sources/UserInterface/Conversation/Content/Cells/ConfigurationMessageCell/Content/Rich Content/ConversationMultipartMessageCell.swift +++ b/wire-ios/Wire-iOS/Sources/UserInterface/Conversation/Content/Cells/ConfigurationMessageCell/Content/Rich Content/ConversationMultipartMessageCell.swift @@ -34,7 +34,6 @@ final class ConversationMultipartMessageCell: UIView, ConversationMessageCell { weak var delegate: ConversationMessageCellDelegate? weak var message: ZMConversationMessage? weak var actionController: ConversationMessageActionController? - var isSelected: Bool = false override init(frame: CGRect) { @@ -78,6 +77,7 @@ final class ConversationMultipartMessageCell: UIView, ConversationMessageCell { with object: Configuration, animated: Bool ) { + let attachments = object.attachments.map { WireCellsMessageAttachment( nodeID: $0.nodeID, @@ -134,6 +134,7 @@ final class ConversationMultipartMessageCellDescription: ConversationMessageCell let accessibilityIdentifier: String? = nil let accessibilityLabel: String? = nil + var supportsActions: Bool = true init( multipartMessage: MultipartMessageData, diff --git a/wire-ios/Wire-iOS/Sources/UserInterface/Conversation/Content/Cells/ConfigurationMessageCell/Content/Text/ConversationLinkAttachmentCell.swift b/wire-ios/Wire-iOS/Sources/UserInterface/Conversation/Content/Cells/ConfigurationMessageCell/Content/Text/ConversationLinkAttachmentCell.swift index 323eb8c4421..edbfdad42be 100644 --- a/wire-ios/Wire-iOS/Sources/UserInterface/Conversation/Content/Cells/ConfigurationMessageCell/Content/Text/ConversationLinkAttachmentCell.swift +++ b/wire-ios/Wire-iOS/Sources/UserInterface/Conversation/Content/Cells/ConfigurationMessageCell/Content/Text/ConversationLinkAttachmentCell.swift @@ -186,8 +186,14 @@ final class ConversationLinkAttachmentCellDescription: ConversationMessageCellDe let accessibilityIdentifier: String? = nil let accessibilityLabel: String? = nil - init(attachment: LinkAttachment, thumbnailResource: WireImageResource?) { - self.configuration = View.Configuration(attachment: attachment, thumbnailResource: thumbnailResource) + init( + attachment: LinkAttachment, + thumbnailResource: WireImageResource? + ) { + self.configuration = View.Configuration( + attachment: attachment, + thumbnailResource: thumbnailResource + ) self.actionController = nil } } diff --git a/wire-ios/Wire-iOS/Sources/UserInterface/Conversation/Content/Cells/ConfigurationMessageCell/Content/Text/ConversationTextMessageCell.swift b/wire-ios/Wire-iOS/Sources/UserInterface/Conversation/Content/Cells/ConfigurationMessageCell/Content/Text/ConversationTextMessageCell.swift index c88fcec0387..4f01f00e1de 100644 --- a/wire-ios/Wire-iOS/Sources/UserInterface/Conversation/Content/Cells/ConfigurationMessageCell/Content/Text/ConversationTextMessageCell.swift +++ b/wire-ios/Wire-iOS/Sources/UserInterface/Conversation/Content/Cells/ConfigurationMessageCell/Content/Text/ConversationTextMessageCell.swift @@ -28,7 +28,11 @@ final class ConversationTextMessageCell: UIView, ConversationMessageCell, TextVi let isObfuscated: Bool let userSession: UserSession? - init(attributedText: NSAttributedString, isObfuscated: Bool, userSession: UserSession? = nil) { + init( + attributedText: NSAttributedString, + isObfuscated: Bool, + userSession: UserSession? = nil + ) { self.attributedText = attributedText self.isObfuscated = isObfuscated self.userSession = userSession @@ -263,11 +267,15 @@ final class ConversationTextMessageCellDescription: ConversationMessageCellDescr let accessibilityIdentifier: String? = nil let accessibilityLabel: String? = nil - init(attributedString: NSAttributedString, isObfuscated: Bool, userSession: UserSession?) { + init( + attributedString: NSAttributedString, + isObfuscated: Bool, + userSession: UserSession? + ) { self.configuration = View.Configuration( attributedText: attributedString, isObfuscated: isObfuscated, - userSession: userSession + userSession: userSession, ) } } @@ -366,7 +374,10 @@ extension ConversationTextMessageCellDescription { cells.append(AnyConversationMessageCellDescription(attachmentCell)) } else if textMessageData.linkPreview != nil { // Link Preview - let linkPreviewCell = ConversationLinkPreviewArticleCellDescription(message: message, data: textMessageData) + let linkPreviewCell = ConversationLinkPreviewArticleCellDescription( + message: message, + data: textMessageData + ) cells.append(AnyConversationMessageCellDescription(linkPreviewCell)) } diff --git a/wire-ios/Wire-iOS/Sources/UserInterface/Conversation/Content/Cells/ConfigurationMessageCell/ConversationMessageSectionController.swift b/wire-ios/Wire-iOS/Sources/UserInterface/Conversation/Content/Cells/ConfigurationMessageCell/ConversationMessageSectionController.swift index f85f42f660c..11bd16eb4f3 100644 --- a/wire-ios/Wire-iOS/Sources/UserInterface/Conversation/Content/Cells/ConfigurationMessageCell/ConversationMessageSectionController.swift +++ b/wire-ios/Wire-iOS/Sources/UserInterface/Conversation/Content/Cells/ConfigurationMessageCell/ConversationMessageSectionController.swift @@ -258,11 +258,11 @@ final class ConversationMessageSectionController: NSObject, ZMMessageObserver { return addCollapsedCell() } + let attachments = message.multipartMessageData?.attachments ?? [] let multipartMessageCellDescription = ConversationMultipartMessageCellDescription( multipartMessage: message.multipartMessageData!, wireCellsFactory: wireCellsFactory ) - return [AnyConversationMessageCellDescription(multipartMessageCellDescription)] } @@ -319,6 +319,9 @@ final class ConversationMessageSectionController: NSObject, ZMMessageObserver { if shouldCollapseCell() { return addCollapsedCell() } + + let attachments = message.multipartMessageData?.attachments ?? [] + return ConversationTextMessageCellDescription .cells( for: message, @@ -385,6 +388,7 @@ final class ConversationMessageSectionController: NSObject, ZMMessageObserver { var cells: [AnyConversationMessageCellDescription] = [] + let attachments = message.multipartMessageData?.attachments ?? [] compositeMessage.compositeMessageData?.items.forEach { item in switch item { diff --git a/wire-ios/Wire-iOS/Sources/UserInterface/Conversation/Content/ConversationContentViewController+MessageAction.swift b/wire-ios/Wire-iOS/Sources/UserInterface/Conversation/Content/ConversationContentViewController+MessageAction.swift index 829087313ce..0ceda253fab 100644 --- a/wire-ios/Wire-iOS/Sources/UserInterface/Conversation/Content/ConversationContentViewController+MessageAction.swift +++ b/wire-ios/Wire-iOS/Sources/UserInterface/Conversation/Content/ConversationContentViewController+MessageAction.swift @@ -93,15 +93,26 @@ extension ConversationContentViewController { } case .delete: assert(message.canBeDeleted) + let attachments = message.multipartMessageData?.attachments deletionDialogPresenter = DeletionDialogPresenter(sourceViewController: presentedViewController ?? self) deletionDialogPresenter?.presentDeletionAlertController( forMessage: message, source: view, userSession: userSession - ) { deleted in + ) { [weak self] deleted, deletionType in + guard let self else { return } + if deleted { - self.presentedViewController?.dismiss(animated: true) + presentedViewController?.dismiss(animated: true) + if let attachments, let deletionType { + delegate?.conversationContentViewController( + self, + didDeleteMultipartMessage: message, + withAttachments: attachments, + deletionType: deletionType + ) + } } } case .present: diff --git a/wire-ios/Wire-iOS/Sources/UserInterface/Conversation/Content/ConversationContentViewController+MessageDeletion.swift b/wire-ios/Wire-iOS/Sources/UserInterface/Conversation/Content/ConversationContentViewController+MessageDeletion.swift index a9787550903..e19dafee1cd 100644 --- a/wire-ios/Wire-iOS/Sources/UserInterface/Conversation/Content/ConversationContentViewController+MessageDeletion.swift +++ b/wire-ios/Wire-iOS/Sources/UserInterface/Conversation/Content/ConversationContentViewController+MessageDeletion.swift @@ -63,7 +63,7 @@ final class DeletionDialogPresenter: NSObject { message: ZMConversationMessage, sourceView: UIView, userSession: UserSession, - completion: @escaping (_ succeeded: Bool) -> Void + completion: @escaping (_ succeeded: Bool, DeletionType?) -> Void ) -> UIAlertController { let alert = UIAlertController.forMessageDeletion(with: message.deletionConfiguration) { action, _ in @@ -78,10 +78,10 @@ final class DeletionDialogPresenter: NSObject { ZMMessage.deleteForEveryone(message) } } completionHandler: { - completion(true) + completion(true, type) } } else { - completion(false) + completion(false, nil) } } @@ -116,7 +116,7 @@ final class DeletionDialogPresenter: NSObject { forMessage message: ZMConversationMessage, source: UIView, userSession: UserSession, - completion: @escaping (_ succeeded: Bool) -> Void + completion: @escaping (_ succeeded: Bool, DeletionType?) -> Void ) { guard !message.hasBeenDeleted else { return } @@ -130,14 +130,15 @@ final class DeletionDialogPresenter: NSObject { } } -private enum AlertAction { +enum DeletionType { + case local + case everywhere +} - enum DeletionType { - case local - case everywhere - } +private enum AlertAction { - case delete(DeletionType), cancel + case delete(DeletionType) + case cancel } // Used to enforce only valid configurations can be shown. diff --git a/wire-ios/Wire-iOS/Sources/UserInterface/Conversation/Content/ConversationContentViewControllerDelegate.swift b/wire-ios/Wire-iOS/Sources/UserInterface/Conversation/Content/ConversationContentViewControllerDelegate.swift index c6009e4d09b..777b01c203d 100644 --- a/wire-ios/Wire-iOS/Sources/UserInterface/Conversation/Content/ConversationContentViewControllerDelegate.swift +++ b/wire-ios/Wire-iOS/Sources/UserInterface/Conversation/Content/ConversationContentViewControllerDelegate.swift @@ -65,4 +65,11 @@ protocol ConversationContentViewControllerDelegate: AnyObject { actionController: ConversationMessageActionController, popoverPresentationInfo: (sourceView: UIView, frame: CGRect)? ) + + func conversationContentViewController( + _ controller: ConversationContentViewController, + didDeleteMultipartMessage message: ZMConversationMessage, + withAttachments attachments: [MultipartMessageData.Attachment], + deletionType: DeletionType + ) } diff --git a/wire-ios/Wire-iOS/Sources/UserInterface/Conversation/ConversationViewController+ConversationContentViewControllerDelegate.swift b/wire-ios/Wire-iOS/Sources/UserInterface/Conversation/ConversationViewController+ConversationContentViewControllerDelegate.swift index e729e8601a4..7eac11f0db8 100644 --- a/wire-ios/Wire-iOS/Sources/UserInterface/Conversation/ConversationViewController+ConversationContentViewControllerDelegate.swift +++ b/wire-ios/Wire-iOS/Sources/UserInterface/Conversation/ConversationViewController+ConversationContentViewControllerDelegate.swift @@ -176,6 +176,36 @@ extension ConversationViewController: ConversationContentViewControllerDelegate } } + func conversationContentViewController( + _ controller: ConversationContentViewController, + didDeleteMultipartMessage message: any ZMConversationMessage, + withAttachments attachments: [MultipartMessageData.Attachment], + deletionType: DeletionType + ) { + switch deletionType { + case .everywhere: + Task { + let deleteNodesUseCase = wireCellsFactory.makeDeleteNodesUseCase() + do { + try await deleteNodesUseCase.invoke(nodeIDs: attachments.map(\.nodeID)) + WireLogger.conversation.info( + "Deleted files for message", + attributes: [.nonce: message.nonce?.uuidString] + ) + } catch { + WireLogger.conversation + .error( + "Unable to delete files: \(String(describing: error))", + attributes: [.nonce: message.nonce?.uuidString], .safePublic + ) + } + } + case .local: + // no op, related files will still show up for self user (as aligned other clients) + break + } + } + } extension ConversationViewController { diff --git a/wire-ios/Wire-iOS/Sources/UserInterface/Conversation/WireCellsFactoryProtocol.swift b/wire-ios/Wire-iOS/Sources/UserInterface/Conversation/WireCellsFactoryProtocol.swift index 9d54dc2113b..3b65ebb31c3 100644 --- a/wire-ios/Wire-iOS/Sources/UserInterface/Conversation/WireCellsFactoryProtocol.swift +++ b/wire-ios/Wire-iOS/Sources/UserInterface/Conversation/WireCellsFactoryProtocol.swift @@ -31,6 +31,7 @@ protocol WireCellsFactoryProtocol { func makeClearPublishedDraftsUseCase(cellName: String) -> WireCellsClearPublishedDraftsUseCaseProtocol func makeDeleteDraftUseCase(cellName: String) -> WireCellsDeleteDraftUseCaseProtocol func makeRetryUploadDraftUseCase(cellName: String) -> WireCellsRetryUploadDraftUseCaseProtocol + func makeDeleteNodesUseCase() -> WireCellsDeleteNodesUseCaseProtocol @MainActor func makeFilesView(cellName: String, isCellsStatePending: Bool, nodeIDs: [UUID]) -> UIViewController @MainActor