Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
signalapp
GitHub Repository: signalapp/Signal-iOS
Path: blob/main/Signal/src/ViewControllers/EditHistoryTableSheetViewController.swift
1 views
//
// Copyright 2023 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//

import LibSignalClient
import SignalServiceKit
import SignalUI

protocol MessageEditHistoryViewDelegate: AnyObject {
    func editHistoryMessageWasDeleted()
}

class EditHistoryTableSheetViewController: OWSTableSheetViewController {

    enum Constants {
        static let cellSpacing: CGFloat = 12.0
    }

    weak var delegate: MessageEditHistoryViewDelegate?

    var parentRenderItems: [CVRenderItem]?
    var renderItems = [CVRenderItem]()
    let threadViewModel: ThreadViewModel
    let spoilerState: SpoilerRenderState
    private var message: TSMessage
    private let database: SDSDatabaseStorage
    private let editManager: EditManager

    init(
        message: TSMessage,
        threadViewModel: ThreadViewModel,
        spoilerState: SpoilerRenderState,
        editManager: EditManager,
        database: SDSDatabaseStorage,
        databaseChangeObserver: DatabaseChangeObserver,
    ) {
        self.threadViewModel = threadViewModel
        self.spoilerState = spoilerState
        self.message = message
        self.database = database
        self.editManager = editManager
        super.init()

        databaseChangeObserver.appendDatabaseChangeDelegate(self)
    }

    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        do {
            try self.database.write { tx in

                guard
                    let thread = TSThread.fetchViaCache(
                        uniqueId: message.uniqueThreadId,
                        transaction: tx,
                    ) else { return }

                try self.editManager.markEditRevisionsAsRead(
                    for: self.message,
                    thread: thread,
                    tx: tx,
                )
            }
        } catch {
            owsFailDebug("Failed to update edit read state")
        }
    }

    // MARK: - Table Update

    private func loadEditHistory() throws {
        let messageStillExists = try database.read { tx in
            guard let newMessage = TSInteraction.fetchViaCache(uniqueId: message.uniqueId, transaction: tx) as? TSMessage else {
                return false
            }
            message = newMessage

            let edits: [TSMessage] = try DependenciesBridge.shared.editMessageStore.findEditHistory(
                forMostRecentRevision: message,
                tx: tx,
            ).compactMap { $0.message }

            guard
                let thread = TSThread.fetchViaCache(
                    uniqueId: message.uniqueThreadId,
                    transaction: tx,
                )
            else {
                owsFailDebug("Missing thread.")
                return false
            }

            let threadAssociatedData = ThreadAssociatedData.fetchOrDefault(
                for: thread,
                transaction: tx,
            )

            parentRenderItems = buildRenderItem(
                thread: thread,
                threadAssociatedData: threadAssociatedData,
                message: message,
                forceDateHeader: true,
                tx: tx,
            )

            var renderItems = [CVRenderItem]()
            for edit in edits {
                let items = buildRenderItem(
                    thread: thread,
                    threadAssociatedData: threadAssociatedData,
                    message: edit,
                    tx: tx,
                )
                renderItems.append(contentsOf: items)
            }
            self.renderItems = renderItems

            return true
        }

        if !messageStillExists {
            delegate?.editHistoryMessageWasDeleted()
        }
    }

    override func tableContents() -> OWSTableContents {
        do {
            try loadEditHistory()
        } catch {
            owsFailDebug("Error reading edit history: \(error)")
        }

        let contents = OWSTableContents()

        guard let parentItems = parentRenderItems else {
            return contents
        }

        let topSection = OWSTableSection()
        topSection.add(createMessageListTableItem(items: parentItems))
        contents.add(topSection)

        let header = OWSLocalizedString(
            "EDIT_HISTORY_LABEL",
            comment: "Label for Edit History modal",
        )

        let section = OWSTableSection()
        section.headerTitle = header
        section.hasBackground = true
        section.hasSeparators = false
        section.add(createMessageListTableItem(items: renderItems))
        contents.add(section)

        return contents
    }

    // MARK: - Utility Methods

    private func createMessageListTableItem(items: [CVRenderItem]) -> OWSTableItem {
        return OWSTableItem { [weak self] in
            guard let self else { return UITableViewCell() }

            let views = items.enumerated().map { index, item in
                let cellView = CVCellView()
                cellView.configure(renderItem: item, componentDelegate: self)
                cellView.isCellVisible = true
                cellView.autoSetDimension(.height, toSize: item.cellSize.height)

                // Its not 100% ideal to use an alternate mechanism to handle taps, but
                // hooking up full cell tap handling is a larger effort and for now
                // we just want to handle long text taps on this view.
                cellView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.didTapCell)))
                cellView.tag = index

                return cellView
            }

            let stack = UIStackView(arrangedSubviews: views)
            stack.spacing = Constants.cellSpacing
            stack.axis = .vertical
            stack.alignment = .fill

            let cell = OWSTableItem.newCell()
            cell.selectionStyle = .none
            cell.contentView.addSubview(stack)
            stack.autoPinEdgesToSuperviewMargins()

            return cell
        }
    }

    @objc
    func didTapCell(_ recognizer: UITapGestureRecognizer) {
        guard
            let view = recognizer.view as? CVCellView,
            let item = view.renderItem,
            item.itemModel.componentState.displayableBodyText?.isTextTruncated == true
        else {
            return
        }
        let itemViewModel = CVItemViewModelImpl(renderItem: item)
        let longTextVC = LongTextViewController(
            itemViewModel: itemViewModel,
            threadViewModel: threadViewModel,
            spoilerState: spoilerState,
        )
        longTextVC.delegate = self
        let navVc = OWSNavigationController(rootViewController: longTextVC)
        self.present(navVc, animated: true)
    }

    var currentDaysBefore: Int = -1
    private func buildRenderItem(
        thread: TSThread,
        threadAssociatedData: ThreadAssociatedData,
        message interaction: TSMessage,
        forceDateHeader: Bool = false,
        tx: DBReadTransaction,
    ) -> [CVRenderItem] {
        var results = [CVRenderItem]()
        let cellInsets = tableViewController.cellOuterInsets
        let viewWidth = tableViewController.view.frame.inset(by: cellInsets).width
        let conversationStyle = ConversationStyle(
            type: .messageDetails,
            thread: thread,
            viewWidth: viewWidth,
            hasWallpaper: false,
            shouldDimWallpaperInDarkMode: false,
            isWallpaperPhoto: false,
            chatColor: DependenciesBridge.shared.chatColorSettingStore.resolvedChatColor(
                for: thread,
                tx: tx,
            ),
            isStandaloneRenderItem: true,
        )

        let groupNameColors = GroupNameColors.forThread(thread)

        let itemDate = Date(millisecondsSince1970: interaction.timestamp)
        let daysPrior = DateUtil.daysFrom(firstDate: itemDate, toSecondDate: Date())
        if forceDateHeader || daysPrior > currentDaysBefore {
            currentDaysBefore = daysPrior

            let dateInteraction = DateHeaderInteraction(thread: thread, timestamp: interaction.timestamp)
            if
                let dateItem = CVLoader.buildStandaloneRenderItem(
                    interaction: dateInteraction,
                    thread: thread,
                    threadAssociatedData: threadAssociatedData,
                    conversationStyle: conversationStyle,
                    spoilerState: self.spoilerState,
                    groupNameColors: groupNameColors,
                    transaction: tx,
                )
            {
                results.append(dateItem)
            }
        }

        if
            let item = CVLoader.buildStandaloneRenderItem(
                interaction: interaction,
                thread: thread,
                threadAssociatedData: threadAssociatedData,
                conversationStyle: conversationStyle,
                spoilerState: self.spoilerState,
                groupNameColors: groupNameColors,
                transaction: tx,
            )
        {
            results.append(item)
        }
        return results
    }
}

// MARK: - DatabaseChangeDelegate

extension EditHistoryTableSheetViewController: DatabaseChangeDelegate {
    func databaseChangesDidUpdate(databaseChanges: SignalServiceKit.DatabaseChanges) {
        guard databaseChanges.didUpdate(interaction: self.message) else {
            return
        }

        updateTableContents()
    }

    func databaseChangesDidUpdateExternally() {
        updateTableContents()
    }

    func databaseChangesDidReset() {
        updateTableContents()
    }
}

// MARK: - CVComponentDelegate

extension EditHistoryTableSheetViewController: CVComponentDelegate {

    func enqueueReload() {}

    func enqueueReloadWithoutCaches() {}

    func didTapBodyTextItem(_ item: CVTextLabel.Item) {}

    func didLongPressBodyTextItem(_ item: CVTextLabel.Item) {}

    func didTapSystemMessageItem(_ item: CVTextLabel.Item) {}

    func didTapCollapseSet(collapseSetId: String) {}

    func didDoubleTapTextViewItem(_ itemViewModel: CVItemViewModelImpl) {}

    func didLongPressTextViewItem(
        _ cell: CVCell,
        itemViewModel: CVItemViewModelImpl,
        shouldAllowReply: Bool,
    ) {}

    func didLongPressMediaViewItem(
        _ cell: CVCell,
        itemViewModel: CVItemViewModelImpl,
        shouldAllowReply: Bool,
    ) {}

    func didLongPressQuote(
        _ cell: CVCell,
        itemViewModel: CVItemViewModelImpl,
        shouldAllowReply: Bool,
    ) {}

    func didLongPressSystemMessage(
        _ cell: CVCell,
        itemViewModel: CVItemViewModelImpl,
    ) {}

    func didLongPressSticker(
        _ cell: CVCell,
        itemViewModel: CVItemViewModelImpl,
        shouldAllowReply: Bool,
    ) {}

    func didLongPressPaymentMessage(
        _ cell: CVCell,
        itemViewModel: CVItemViewModelImpl,
        shouldAllowReply: Bool,
    ) {}

    func didLongPressPoll(
        _ cell: CVCell,
        itemViewModel: CVItemViewModelImpl,
        shouldAllowReply: Bool,
    ) {}

    func didTapPayment(_ payment: PaymentsHistoryItem) {}

    func didChangeLongPress(_ itemViewModel: CVItemViewModelImpl) {}

    func didEndLongPress(_ itemViewModel: CVItemViewModelImpl) {}

    func didCancelLongPress(_ itemViewModel: CVItemViewModelImpl) {}

    // MARK: -

    func willBecomeVisibleWithSkippedDownloads(_ message: TSMessage) {}

    func didTapSkippedDownloads(_ message: TSMessage) {}

    func didCancelDownload(_ message: TSMessage, attachmentId: Attachment.IDType) {}

    // MARK: -

    func didTapReplyToItem(_ itemViewModel: CVItemViewModelImpl) {}

    func didTapSenderAvatar(_ interaction: TSInteraction) {}

    func shouldAllowReplyForItem(_ itemViewModel: CVItemViewModelImpl) -> Bool { false }

    func didTapReactions(
        reactionState: InteractionReactionState,
        message: TSMessage,
    ) {}

    func didTapTruncatedTextMessage(_ itemViewModel: CVItemViewModelImpl) {}

    func didTapShowEditHistory(_ itemViewModel: CVItemViewModelImpl) {}

    var hasPendingMessageRequest: Bool { false }

    func didTapUndownloadableMedia() {}

    func didTapUndownloadableGenericFile() {}

    func didTapUndownloadableOversizeText() {}

    func didTapUndownloadableAudio() {}

    func didTapUndownloadableSticker() {}

    func didTapBrokenVideo() {}

    func didTapBodyMedia(
        itemViewModel: CVItemViewModelImpl,
        attachmentStream: ReferencedAttachmentStream,
        imageView: UIView,
    ) {}

    func didTapGenericAttachment(
        _ attachment: CVComponentGenericAttachment,
    ) -> CVAttachmentTapAction { .default }

    func didTapQuotedReply(_ quotedReply: QuotedReplyModel) {}

    func didTapLinkPreview(url: URL) {}

    func didTapContactShare(_ contactShare: ContactShareViewModel) {}

    func didTapSendMessage(to phoneNumbers: [String]) {}

    func didTapSendInvite(toContactShare contactShare: ContactShareViewModel) {}

    func didTapAddToContacts(contactShare: ContactShareViewModel) {}

    func didTapStickerPack(_ stickerPackInfo: StickerPackInfo) {}

    func didTapGroupInviteLink(url: URL) {}

    func didTapProxyLink(url: URL) {}

    func didTapShowMessageDetail(_ itemViewModel: CVItemViewModelImpl) {}

    func willWrapGift(_ messageUniqueId: String) -> Bool { false }

    func willShakeGift(_ messageUniqueId: String) -> Bool { false }

    func willUnwrapGift(_ itemViewModel: CVItemViewModelImpl) {}

    func didTapGiftBadge(
        _ itemViewModel: CVItemViewModelImpl,
        profileBadge: ProfileBadge,
        isExpired: Bool,
        isRedeemed: Bool,
    ) {}

    func prepareMessageDetailForInteractivePresentation(_ itemViewModel: CVItemViewModelImpl) {}

    func beginCellAnimation(maximumDuration: TimeInterval) -> EndCellAnimation {
        return {}
    }

    var wallpaperBlurProvider: WallpaperBlurProvider? { nil }

    var selectionState: CVSelectionState { CVSelectionState() }

    func didTapPreviouslyVerifiedIdentityChange(_ address: SignalServiceAddress) {}

    func didTapUnverifiedIdentityChange(_ address: SignalServiceAddress) {}

    func didTapCorruptedMessage(_ message: TSErrorMessage) {}

    func didTapSessionRefreshMessage(_ message: TSErrorMessage) {}

    func didTapResendGroupUpdateForErrorMessage(_ errorMessage: TSErrorMessage) {}

    func didTapShowFingerprint(_ address: SignalServiceAddress) {}

    func didTapIndividualCall(_ call: TSCall) {}

    func didTapLearnMoreMissedCallFromBlockedContact(_ call: TSCall) {}

    func didTapGroupCall() {}

    func didTapPendingOutgoingMessage(_ message: TSOutgoingMessage) {}

    func didTapFailedMessage(_ message: TSMessage) {}

    func didTapGroupMigrationLearnMore() {}

    func didTapGroupInviteLinkPromotion(groupModel: TSGroupModel) {}

    func didTapViewGroupDescription(newGroupDescription: String) {}

    func didTapNameEducation(type: SafetyTipsType) {}

    func didTapShowConversationSettings() {}

    func didTapShowConversationSettingsAndShowMemberRequests() {}

    func didTapBlockRequest(
        groupModel: TSGroupModelV2,
        requesterName: String,
        requesterAci: Aci,
    ) {}

    func didTapShowUpgradeAppUI() {}

    func didTapUpdateSystemContact(
        _ address: SignalServiceAddress,
        newNameComponents: PersonNameComponents,
    ) {}

    func didTapPhoneNumberChange(aci: Aci, phoneNumberOld: String, phoneNumberNew: String) {}

    func didTapViewOnceAttachment(_ interaction: TSInteraction) {}

    func didTapViewOnceExpired(_ interaction: TSInteraction) {}

    func didTapContactName(thread: TSContactThread) {}

    func didTapUnknownThreadWarningGroup() {}
    func didTapUnknownThreadWarningContact() {}
    func didTapDeliveryIssueWarning(_ message: TSErrorMessage) {}

    func didTapActivatePayments() {}
    func didTapSendPayment() {}

    func didTapThreadMergeLearnMore(phoneNumber: String) {}

    func didTapReportSpamLearnMore() {}

    func didTapMessageRequestAcceptedOptions() {}

    func didTapJoinCallLinkCall(callLink: CallLink) {}

    func didTapViewVotes(poll: OWSPoll) {}

    func didTapViewPoll(pollInteractionUniqueId: String) {}

    func didTapVoteOnPoll(poll: OWSPoll, optionIndex: UInt32, isUnvote: Bool) {}

    func didTapViewPinnedMessage(pinnedMessageUniqueId: String) {}

    func didTapSafetyTips() {}
}

extension EditHistoryTableSheetViewController: LongTextViewDelegate {
    func longTextViewMessageWasDeleted(_ longTextViewController: LongTextViewController) {
        self.dismiss(animated: true)
    }
}