//
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//

import UIKit
import Foundation
import SignalUI

class SharingThreadPickerViewController: ConversationPickerViewController {

    weak var shareViewDelegate: ShareViewDelegate?

    private var sendProgressSheet: SharingThreadPickerProgressSheet?

    /// It can take a while to fully process attachments, and until we do we aren't
    /// fully sure if the attachments are stories-compatible. To speed things up,
    /// we do some fast pre-checks and store the result here.
    /// True if these pre-checks determine all attachments are stories-compatible.
    /// Once this is set, we show stories forever, even if the attachments end up being
    /// incompatible, because it would be weird to have the stories destinations disappear.
    /// Instead, we show an error when actually sending if stories are selected.
    public var areAttachmentStoriesCompatPrecheck: Bool? {
        didSet {
            // If we've already processed attachments, ignore the setting.
            guard attachments == nil else {
                areAttachmentStoriesCompatPrecheck = nil
                return
            }
            updateStoriesState()
            updateApprovalMode()
        }
    }

    var attachments: [SignalAttachment]? {
        didSet {
            updateStoriesState()
            updateApprovalMode()
        }
    }

    var isTextMessage: Bool {
        guard let attachments = attachments, attachments.count == 1, let attachment = attachments.first else { return false }
        return attachment.isConvertibleToTextMessage && attachment.dataLength <= kOversizeTextMessageSizeThreshold
    }

    var isContactShare: Bool {
        guard let attachments = attachments, attachments.count == 1, let attachment = attachments.first else { return false }
        return attachment.isConvertibleToContactShare
    }

    var approvedAttachments: [SignalAttachment]?
    var approvedContactShare: ContactShareDraft?
    var approvalMessageBody: MessageBody?
    var approvalLinkPreviewDraft: OWSLinkPreviewDraft?

    var mentionCandidates: [SignalServiceAddress] = []

    var selectedConversations: [ConversationItem] { selection.conversations }

    public init(shareViewDelegate: ShareViewDelegate) {
        self.shareViewDelegate = shareViewDelegate

        super.init(selection: ConversationPickerSelection())

        shouldBatchUpdateIdentityKeys = true
        pickerDelegate = self
    }

    public func presentActionSheetOnNavigationController(_ alert: ActionSheetController) {
        if let navigationController = shareViewDelegate?.shareViewNavigationController {
            navigationController.presentActionSheet(alert)
        } else {
            super.presentActionSheet(alert)
        }
    }

    private func updateMentionCandidates() {
        AssertIsOnMainThread()

        guard selectedConversations.count == 1,
              case .group(let groupThreadId) = selectedConversations.first?.messageRecipient else {
            mentionCandidates = []
            return
        }

        let groupThread = databaseStorage.read { readTx in
            TSGroupThread.anyFetchGroupThread(uniqueId: groupThreadId, transaction: readTx)
        }

        owsAssertDebug(groupThread != nil)
        if let groupThread = groupThread, groupThread.allowsMentionSend {
            mentionCandidates = groupThread.recipientAddressesWithSneakyTransaction
        } else {
            mentionCandidates = []
        }
    }

    private func updateStoriesState() {
        if areAttachmentStoriesCompatPrecheck == true {
            sectionOptions.insert(.stories)
        } else if let attachments = attachments, attachments.allSatisfy({ $0.isValidImage || $0.isValidVideo }) {
            sectionOptions.insert(.stories)
        } else if isTextMessage {
            sectionOptions.insert(.stories)
        } else {
            sectionOptions.remove(.stories)
        }
    }
}

// MARK: - Approval

extension SharingThreadPickerViewController {

    func approve() {
        do {
            let vc = try buildApprovalViewController(withCancelButton: false)
            navigationController?.pushViewController(vc, animated: true)
        } catch {
            shareViewDelegate?.shareViewFailed(error: error)
        }
    }

    func buildApprovalViewController(for thread: TSThread) throws -> UIViewController {
        AssertIsOnMainThread()
        loadViewIfNeeded()
        guard let conversationItem = conversation(for: thread) else {
            throw OWSAssertionError("Unexpectedly missing conversation for selected thread")
        }
        selection.add(conversationItem)
        return try buildApprovalViewController(withCancelButton: true)
    }

    func buildApprovalViewController(withCancelButton: Bool) throws -> UIViewController {
        guard let attachments = attachments, let firstAttachment = attachments.first else {
            throw OWSAssertionError("Unexpectedly missing attachments")
        }

        let approvalVC: UIViewController

        if isTextMessage {
            guard let messageText = String(data: firstAttachment.data, encoding: .utf8)?.filterForDisplay else {
                throw OWSAssertionError("Missing or invalid message text for text attachment")
            }
            let approvalView = TextApprovalViewController(messageBody: MessageBody(text: messageText, ranges: .empty))
            approvalVC = approvalView
            approvalView.delegate = self

        } else if isContactShare {
            let cnContact = try SystemContact.parseVCardData(firstAttachment.data)

            let contactShareDraft = databaseStorage.read { tx in
                return ContactShareDraft.load(
                    cnContact: cnContact,
                    signalContact: SystemContact(cnContact: cnContact),
                    contactManager: Self.contactsManager,
                    phoneNumberUtil: Self.phoneNumberUtil,
                    profileManager: Self.profileManager,
                    recipientManager: DependenciesBridge.shared.recipientManager,
                    tsAccountManager: DependenciesBridge.shared.tsAccountManager,
                    tx: tx
                )
            }

            let approvalView = ContactShareViewController(contactShareDraft: contactShareDraft)
            approvalVC = approvalView
            approvalView.shareDelegate = self

        } else {
            let approvalItems = attachments.map { AttachmentApprovalItem(attachment: $0, canSave: false) }
            var approvalVCOptions: AttachmentApprovalViewControllerOptions = withCancelButton ? [ .hasCancel ] : []
            if self.selection.conversations.contains(where: \.isStory) {
                approvalVCOptions.insert(.disallowViewOnce)
            }
            let approvalView = AttachmentApprovalViewController(options: approvalVCOptions, attachmentApprovalItems: approvalItems)
            approvalVC = approvalView
            approvalView.approvalDelegate = self
            approvalView.approvalDataSource = self
        }

        return approvalVC
    }
}

// MARK: - Sending

extension SharingThreadPickerViewController {

    func send() {
        // Start presenting empty; the attachments will get set later.
        self.presentOrUpdateSendProgressSheet(attachmentIds: [])

        Task {
            switch await tryToSend(
                selectedConversations: selectedConversations,
                isTextMessage: isTextMessage,
                isContactShare: isContactShare,
                messageBody: approvalMessageBody,
                attachments: approvedAttachments,
                linkPreviewDraft: approvalLinkPreviewDraft,
                contactShareDraft: approvedContactShare
            ) {
            case .success:
                self.dismissSendProgressSheet {}
                self.shareViewDelegate?.shareViewWasCompleted()
            case .failure(let error):
                self.dismissSendProgressSheet { self.showSendFailure(error: error) }
            }
        }
    }

    private struct SendError: Error {
        let outgoingMessages: [PreparedOutgoingMessage]
        let error: Error
    }

    private nonisolated func tryToSend(
        selectedConversations: [ConversationItem],
        isTextMessage: Bool,
        isContactShare: Bool,
        messageBody: MessageBody?,
        attachments: [SignalAttachment]?,
        linkPreviewDraft: OWSLinkPreviewDraft?,
        contactShareDraft: ContactShareDraft?
    ) async -> Result<Void, SendError> {
        if isTextMessage {
            guard let messageBody, !messageBody.text.isEmpty else {
                return .failure(.init(outgoingMessages: [], error: OWSAssertionError("Missing body.")))
            }

            let linkPreviewDataSource: LinkPreviewTSResourceDataSource?
            if let linkPreviewDraft {
                linkPreviewDataSource = try? DependenciesBridge.shared.linkPreviewManager.buildDataSource(
                    from: linkPreviewDraft
                )
            } else {
                linkPreviewDataSource = nil
            }

            return await self.sendToOutgoingMessageThreads(
                selectedConversations: selectedConversations,
                messageBody: messageBody,
                messageBlock: { destination, tx in
                    let unpreparedMessage = UnpreparedOutgoingMessage.build(
                        thread: destination.thread,
                        messageBody: destination.messageBody,
                        quotedReplyDraft: nil,
                        linkPreviewDataSource: linkPreviewDataSource,
                        transaction: tx
                    )
                    return try unpreparedMessage.prepare(tx: tx)
                },
                storySendBlock: { storyConversations in
                    // Send the text message to any selected story recipients
                    // as a text story with default styling.
                    StorySharing.sendTextStory(
                        with: messageBody,
                        linkPreviewDraft: linkPreviewDraft,
                        to: storyConversations
                    )
                }
            )
        } else if isContactShare {
            guard let contactShareDraft else {
                return .failure(.init(outgoingMessages: [], error: OWSAssertionError("Missing contactShare.")))
            }
            let contactShareForSending: ContactShareDraft.ForSending
            do {
                contactShareForSending = try DependenciesBridge.shared.contactShareManager.validateAndPrepare(
                    draft: contactShareDraft
                )
            } catch {
                return .failure(.init(outgoingMessages: [], error: error))
            }
            return await self.sendToOutgoingMessageThreads(
                selectedConversations: selectedConversations,
                messageBody: nil,
                messageBlock: { destination, tx in
                    let builder = TSOutgoingMessageBuilder(thread: destination.thread)
                    let dmConfigurationStore = DependenciesBridge.shared.disappearingMessagesConfigurationStore
                    builder.expiresInSeconds = dmConfigurationStore.durationSeconds(
                        for: destination.thread,
                        tx: tx.asV2Read
                    )
                    let message = builder.build(transaction: tx)
                    let unpreparedMessage = UnpreparedOutgoingMessage.forMessage(
                        message,
                        contactShareDraft: contactShareForSending
                    )
                    return try unpreparedMessage.prepare(tx: tx)
                },
                // We don't send contact shares to stories
                storySendBlock: nil
            )
        } else {
            guard let attachments else {
                return .failure(.init(outgoingMessages: [], error: OWSAssertionError("Missing approvedAttachments.")))
            }

            // This method will also add threads to the profile whitelist.
            let sendResult = TSResourceMultisend.sendApprovedMedia(
                conversations: selectedConversations,
                approvalMessageBody: messageBody,
                approvedAttachments: attachments
            )

            let preparedMessages: [PreparedOutgoingMessage]
            do {
                preparedMessages = try await sendResult.preparedPromise.awaitable()
            } catch let error {
                return .failure(.init(outgoingMessages: [], error: error))
            }
            await MainActor.run {
                self.presentOrUpdateSendProgressSheet(outgoingMessages: preparedMessages)
            }

            do {
                _ = try await sendResult.sentPromise.awaitable()
            } catch let error {
                return .failure(.init(outgoingMessages: preparedMessages, error: error))
            }
            return .success(())
        }
    }

    private func presentOrUpdateSendProgressSheet(outgoingMessages: [PreparedOutgoingMessage]) {
        let attachmentIds = databaseStorage.read { tx in
            return outgoingMessages.attachmentIdsForUpload(tx: tx)
        }
        presentOrUpdateSendProgressSheet(attachmentIds: attachmentIds)
    }

    private func presentOrUpdateSendProgressSheet(attachmentIds: [TSResourceId]) {
        AssertIsOnMainThread()

        if let sendProgressSheet {
            // Update the existing sheet.
            sendProgressSheet.updateSendingAttachmentIds(attachmentIds)
            return
        } else {
            let actionSheet = SharingThreadPickerProgressSheet(
                attachmentIds: attachmentIds,
                delegate: self.shareViewDelegate
            )
            presentActionSheetOnNavigationController(actionSheet)
            self.sendProgressSheet = actionSheet
        }
    }

    private func dismissSendProgressSheet(_ completion: (() -> Void)?) {
        if let sendProgressSheet {
            sendProgressSheet.dismiss(animated: true, completion: completion)
            self.sendProgressSheet = nil
        } else {
            completion?()
        }
    }

    private nonisolated func sendToOutgoingMessageThreads(
        selectedConversations: [ConversationItem],
        messageBody: MessageBody?,
        messageBlock: @escaping (AttachmentMultisend.Destination, SDSAnyWriteTransaction) throws -> PreparedOutgoingMessage,
        storySendBlock: (([ConversationItem]) -> AttachmentMultisend.Result?)?
    ) async -> Result<Void, SendError> {
        let conversations = selectedConversations.filter { $0.outgoingMessageType == .message }

        let preparedNonStoryMessages: [PreparedOutgoingMessage]
        let nonStorySendPromises: [Promise<Void>]

        do {
            let destinations = try AttachmentMultisend.prepareForSending(
                messageBody,
                to: conversations,
                db: self.databaseStorage,
                attachmentValidator: DependenciesBridge.shared.tsResourceContentValidator
            )

            (preparedNonStoryMessages, nonStorySendPromises) = try await self.databaseStorage.awaitableWrite { tx in
                let preparedMessages = try destinations.map { destination in
                    return try messageBlock(destination, tx)
                }

                // We're sending a message to this thread, approve any pending message request
                destinations.forEach { destination in
                    ThreadUtil.addThreadToProfileWhitelistIfEmptyOrPendingRequest(
                        destination.thread,
                        setDefaultTimerIfNecessary: true,
                        tx: tx
                    )
                }

                let sendPromises = preparedMessages.map {
                    ThreadUtil.enqueueMessagePromise(
                        message: $0,
                        transaction: tx
                    )
                }
                return (preparedMessages, sendPromises)
            }
        } catch let error {
            return .failure(.init(outgoingMessages: [], error: error))
        }

        let storyConversations = selectedConversations.filter { $0.outgoingMessageType == .storyMessage }
        let storySendResult = storySendBlock?(storyConversations)

        let preparedStoryMessages: [PreparedOutgoingMessage]
        do {
            preparedStoryMessages = try await storySendResult?.preparedPromise.awaitable() ?? []
        } catch let error {
            return .failure(.init(outgoingMessages: [], error: error))
        }

        let preparedMessages = preparedNonStoryMessages + preparedStoryMessages
        await MainActor.run {
            self.presentOrUpdateSendProgressSheet(outgoingMessages: preparedMessages)
        }

        do {
            try await withThrowingTaskGroup(of: Void.self) { taskGroup in
                nonStorySendPromises.forEach { promise in
                    taskGroup.addTask(operation: {
                        try await promise.awaitable()
                    })
                }
                taskGroup.addTask(operation: {
                    try await _ = storySendResult?.sentPromise.awaitable()
                })
                try await taskGroup.waitForAll()
            }
            return .success(())
        } catch let error {
            return .failure(.init(outgoingMessages: preparedMessages, error: error))
        }
    }

    private nonisolated func threads(for conversationItems: [ConversationItem], tx: SDSAnyWriteTransaction) -> [TSThread] {
        return conversationItems.compactMap { conversation in
            guard let thread = conversation.getOrCreateThread(transaction: tx) else {
                owsFailDebug("Missing thread for conversation")
                return nil
            }
            return thread
        }
    }

    private func showSendFailure(error: SendError) {
        AssertIsOnMainThread()

        owsFailDebug("Error: \(error.error)")

        let cancelAction = ActionSheetAction(
            title: CommonStrings.cancelButton,
            style: .cancel
        ) { [weak self] _ in
            guard let self = self else { return }
            self.databaseStorage.write { transaction in
                for message in error.outgoingMessages {
                    // If we sent the message to anyone, mark it as failed
                    message.updateWithAllSendingRecipientsMarkedAsFailed(tx: transaction)
                }
            }
            self.shareViewDelegate?.shareViewWasCancelled()
        }

        let failureTitle = OWSLocalizedString("SHARE_EXTENSION_SENDING_FAILURE_TITLE", comment: "Alert title")

        if let untrustedIdentityError = error as? UntrustedIdentityError {
            let untrustedServiceId = untrustedIdentityError.serviceId
            let failureFormat = OWSLocalizedString(
                "SHARE_EXTENSION_FAILED_SENDING_BECAUSE_UNTRUSTED_IDENTITY_FORMAT",
                comment: "alert body when sharing file failed because of untrusted/changed identity keys"
            )
            let displayName = databaseStorage.read { tx in
                return contactsManager.displayName(for: SignalServiceAddress(untrustedServiceId), tx: tx).resolvedValue()
            }
            let failureMessage = String(format: failureFormat, displayName)

            let actionSheet = ActionSheetController(title: failureTitle, message: failureMessage)
            actionSheet.addAction(cancelAction)

            // Capture the identity key before showing the prompt about it.
            let identityKey = databaseStorage.read { tx in
                let identityManager = DependenciesBridge.shared.identityManager
                return identityManager.identityKey(for: SignalServiceAddress(untrustedServiceId), tx: tx.asV2Read)
            }

            let confirmAction = ActionSheetAction(
                title: SafetyNumberStrings.confirmSendButton,
                style: .default
            ) { [weak self] _ in
                guard let self = self else { return }

                // Confirm Identity
                self.databaseStorage.write { transaction in
                    let identityManager = DependenciesBridge.shared.identityManager
                    let verificationState = identityManager.verificationState(
                        for: SignalServiceAddress(untrustedServiceId),
                        tx: transaction.asV2Write
                    )
                    switch verificationState {
                    case .verified:
                        owsFailDebug("Unexpected state")
                    case .noLongerVerified, .implicit(isAcknowledged: _):
                        Logger.info("marked recipient: \(untrustedServiceId) as default verification status.")
                        guard let identityKey else {
                            owsFailDebug("Can't be untrusted unless there's already an identity key.")
                            return
                        }
                        _ = identityManager.setVerificationState(
                            .implicit(isAcknowledged: true),
                            of: identityKey,
                            for: SignalServiceAddress(untrustedServiceId),
                            isUserInitiatedChange: true,
                            tx: transaction.asV2Write
                        )
                    }
                }

                // Resend
                self.resendMessages(error.outgoingMessages)
            }
            actionSheet.addAction(confirmAction)

            presentActionSheetOnNavigationController(actionSheet)
        } else {
            let actionSheet = ActionSheetController(title: failureTitle)
            actionSheet.addAction(cancelAction)

            let retryAction = ActionSheetAction(title: CommonStrings.retryButton, style: .default) { [weak self] _ in
                self?.resendMessages(error.outgoingMessages)
            }
            actionSheet.addAction(retryAction)

            presentActionSheetOnNavigationController(actionSheet)
        }
    }

    func resendMessages(_ outgoingMessages: [PreparedOutgoingMessage]) {
        AssertIsOnMainThread()
        owsAssertDebug(outgoingMessages.count > 0)

        var promises = [Promise<Void>]()
        databaseStorage.write { transaction in
            for message in outgoingMessages {
                promises.append(SSKEnvironment.shared.messageSenderJobQueueRef.add(
                    .promise,
                    message: message,
                    transaction: transaction
                ))
            }
        }

        self.presentOrUpdateSendProgressSheet(outgoingMessages: outgoingMessages)
        Promise.when(fulfilled: promises).done {
            self.dismissSendProgressSheet {}
            self.shareViewDelegate?.shareViewWasCompleted()
        }.catch { error in
            self.dismissSendProgressSheet {
                self.showSendFailure(error: .init(outgoingMessages: outgoingMessages, error: error))
            }
        }
    }
}

// MARK: -

extension SharingThreadPickerViewController: ConversationPickerDelegate {
    func conversationPickerSelectionDidChange(_ conversationPickerViewController: ConversationPickerViewController) {
        updateMentionCandidates()
    }

    func conversationPickerDidCompleteSelection(_ conversationPickerViewController: ConversationPickerViewController) {
        // Check if the attachments are compatible with sending to stories.
        let storySelections = selection.conversations.compactMap({ $0 as? StoryConversationItem })
        if !storySelections.isEmpty, let attachments = attachments {
            let areImagesOrVideos = attachments.allSatisfy({ $0.isValidImage || $0.isValidVideo })
            let isTextMessage = attachments.count == 1 && attachments.first.map {
                $0.isConvertibleToTextMessage && $0.dataLength <= kOversizeTextMessageSizeThreshold
            } ?? false
            if !areImagesOrVideos && !isTextMessage {
                // Can't send to stories!
                storySelections.forEach { self.selection.remove($0) }
                self.updateUIForCurrentSelection(animated: false)
                self.tableView.reloadData()
                let vc = ConversationPickerFailedRecipientsSheet(
                    failedAttachments: attachments,
                    failedStoryConversationItems: storySelections,
                    remainingConversationItems: self.selection.conversations,
                    onApprove: { [weak self] in
                        guard
                            let strongSelf = self,
                            strongSelf.selection.conversations.isEmpty.negated
                        else {
                            return
                        }
                        strongSelf.conversationPickerDidCompleteSelection(strongSelf)
                    })
                self.present(vc, animated: true)
                return
            }
        }

        approve()
    }

    func conversationPickerCanCancel(_ conversationPickerViewController: ConversationPickerViewController) -> Bool {
        return true
    }

    func conversationPickerDidCancel(_ conversationPickerViewController: ConversationPickerViewController) {
        shareViewDelegate?.shareViewWasCancelled()
    }

    func approvalMode(_ conversationPickerViewController: ConversationPickerViewController) -> ApprovalMode {
        return attachments?.isEmpty != false ? .loading : .next
    }

    func conversationPickerDidBeginEditingText() {}

    func conversationPickerSearchBarActiveDidChange(_ conversationPickerViewController: ConversationPickerViewController) {}
}

// MARK: -

extension SharingThreadPickerViewController: TextApprovalViewControllerDelegate {
    func textApproval(_ textApproval: TextApprovalViewController, didApproveMessage messageBody: MessageBody?, linkPreviewDraft: OWSLinkPreviewDraft?) {
        assert(messageBody?.text.nilIfEmpty != nil)

        approvalMessageBody = messageBody
        approvalLinkPreviewDraft = linkPreviewDraft

        send()
    }

    func textApprovalDidCancel(_ textApproval: TextApprovalViewController) {
        shareViewDelegate?.shareViewWasCancelled()
    }

    func textApprovalCustomTitle(_ textApproval: TextApprovalViewController) -> String? {
        return nil
    }

    func textApprovalRecipientsDescription(_ textApproval: TextApprovalViewController) -> String? {
        let conversations = selectedConversations
        guard conversations.count > 0 else {
            return nil
        }
        return conversations.map { $0.titleWithSneakyTransaction }.joined(separator: ", ")
    }

    func textApprovalMode(_ textApproval: TextApprovalViewController) -> ApprovalMode {
        return .send
    }
}

// MARK: -

extension SharingThreadPickerViewController: ContactShareViewControllerDelegate {

    func contactShareViewController(_ viewController: ContactShareViewController, didApproveContactShare contactShare: ContactShareDraft) {
        approvedContactShare = contactShare
        send()
    }

    func contactShareViewControllerDidCancel(_ viewController: ContactShareViewController) {
        shareViewDelegate?.shareViewWasCancelled()
    }

    func titleForContactShareViewController(_ viewController: ContactShareViewController) -> String? {
        return nil
    }

    func recipientsDescriptionForContactShareViewController(_ viewController: ContactShareViewController) -> String? {
        let conversations = selectedConversations
        guard conversations.count > 0 else {
            return nil
        }
        return conversations.map { $0.titleWithSneakyTransaction }.joined(separator: ", ")
    }

    func approvalModeForContactShareViewController(_ viewController: ContactShareViewController) -> SignalUI.ApprovalMode {
        return .send
    }
}

// MARK: -

extension SharingThreadPickerViewController: AttachmentApprovalViewControllerDelegate {

    func attachmentApproval(_ attachmentApproval: AttachmentApprovalViewController, didChangeMessageBody newMessageBody: MessageBody?) {
        self.approvalMessageBody = newMessageBody
    }

    func attachmentApproval(_ attachmentApproval: AttachmentApprovalViewController, didChangeViewOnceState isViewOnce: Bool) {
        // We can ignore this event.
    }

    func attachmentApproval(_ attachmentApproval: AttachmentApprovalViewController, didRemoveAttachment attachment: SignalAttachment) {
        // We can ignore this event.
    }

    func attachmentApproval(_ attachmentApproval: AttachmentApprovalViewController, didApproveAttachments attachments: [SignalAttachment], messageBody: MessageBody?) {
        self.approvedAttachments = attachments
        self.approvalMessageBody = messageBody

        send()
    }

    func attachmentApprovalDidCancel(_ attachmentApproval: AttachmentApprovalViewController) {
        shareViewDelegate?.shareViewWasCancelled()
    }

    func attachmentApprovalDidTapAddMore(_ attachmentApproval: AttachmentApprovalViewController) {
        owsFailDebug("Cannot add more to message forwards.")
    }
}

// MARK: -

extension SharingThreadPickerViewController: AttachmentApprovalViewControllerDataSource {

    var attachmentApprovalTextInputContextIdentifier: String? {
        return nil
    }

    var attachmentApprovalRecipientNames: [String] {
        selectedConversations.map { $0.titleWithSneakyTransaction }
    }

    func attachmentApprovalMentionableAddresses(tx: DBReadTransaction) -> [SignalServiceAddress] {
        mentionCandidates
    }

    func attachmentApprovalMentionCacheInvalidationKey() -> String {
        return "\(mentionCandidates.hashValue)"
    }
}
