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

import LibSignalClient
import SignalServiceKit
import SignalUI
import SwiftUI

class MemberLabelViewController: OWSViewController, UITextFieldDelegate {
    private let initialEmoji: String?
    private let initialMemberLabel: String?
    private var updatedMemberLabel: String?
    private var updatedEmoji: String?
    private var addEmojiButton = UIButton(type: .system)
    private var previewContainer: UIStackView?
    private let stackView = UIStackView()
    private let textField = UITextField()
    private var characterCountLabel = UILabel()
    private var clearButton = UIButton(type: .system)
    private var groupNameColors: GroupNameColors
    private let groupMemberLabelsWithoutLocalUser: [SignalServiceAddress: MemberLabelForRendering]
    private let groupModel: TSGroupModelV2
    private let db: DB
    private let contactManager: OWSContactsManager

    weak var updateDelegate: MemberLabelCoordinator?

    private static let maxCharCount = 24
    private static let showCharacterCountMax = 9

    init(
        memberLabel: String? = nil,
        emoji: String? = nil,
        groupNameColors: GroupNameColors,
        groupMemberLabelsWithoutLocalUser: [SignalServiceAddress: MemberLabelForRendering],
        groupModel: TSGroupModelV2,
        db: DB,
        contactManager: OWSContactsManager,
    ) {
        self.initialMemberLabel = memberLabel
        self.initialEmoji = emoji
        self.updatedMemberLabel = memberLabel
        self.updatedEmoji = emoji
        self.groupNameColors = groupNameColors
        self.groupModel = groupModel
        textField.text = memberLabel
        self.groupMemberLabelsWithoutLocalUser = groupMemberLabelsWithoutLocalUser
        self.db = db
        self.contactManager = contactManager

        super.init()

        view.backgroundColor = UIColor.Signal.groupedBackground
        addNavigationTitleView(groupName: groupModel.groupNameOrDefault)
        navigationItem.rightBarButtonItem = .init(barButtonSystemItem: .done, target: self, action: #selector(didTapDone))

        navigationItem.leftBarButtonItem = .cancelButton(
            dismissingFrom: self,
            hasUnsavedChanges: { [weak self] in
                guard let self else { return false }
                return updatedMemberLabel != initialMemberLabel || updatedEmoji != initialEmoji
            },
        )

        navigationItem.rightBarButtonItem?.tintColor = UIColor.Signal.ultramarine
        navigationItem.rightBarButtonItem?.isEnabled = false
    }

    func addNavigationTitleView(groupName: String) {
        let titleLabel = UILabel()
        titleLabel.text = OWSLocalizedString(
            "MEMBER_LABEL_VIEW_TITLE",
            comment: "Title for a view where users can edit and preview their member label.",
        )
        titleLabel.font = .dynamicTypeSubheadline.semibold()
        titleLabel.textColor = UIColor.Signal.label
        titleLabel.textAlignment = .center

        let subtitleLabel = UILabel()
        subtitleLabel.text = groupName
        subtitleLabel.font = .dynamicTypeCaption1.semibold()
        subtitleLabel.textColor = UIColor.Signal.secondaryLabel
        subtitleLabel.textAlignment = .center

        let stackView = UIStackView(arrangedSubviews: [titleLabel, subtitleLabel])
        stackView.axis = .vertical
        stackView.alignment = .center
        stackView.spacing = 0

        navigationItem.titleView = stackView
    }

    override func viewDidLoad() {
        super.viewDidLoad()

        let scrollView = UIScrollView()
        view.addSubview(scrollView)
        scrollView.translatesAutoresizingMaskIntoConstraints = false
        NSLayoutConstraint.activate([
            scrollView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
            scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
            scrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
            scrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
        ])

        createInitialViews()

        scrollView.addSubview(stackView)
        stackView.translatesAutoresizingMaskIntoConstraints = false
        NSLayoutConstraint.activate([
            stackView.topAnchor.constraint(equalTo: scrollView.contentLayoutGuide.topAnchor),
            stackView.leadingAnchor.constraint(equalTo: scrollView.contentLayoutGuide.leadingAnchor, constant: 16),
            stackView.trailingAnchor.constraint(equalTo: scrollView.contentLayoutGuide.trailingAnchor, constant: -16),
            stackView.bottomAnchor.constraint(equalTo: scrollView.contentLayoutGuide.bottomAnchor),
            stackView.widthAnchor.constraint(equalTo: scrollView.frameLayoutGuide.widthAnchor, constant: -32),
        ])
        scrollView.keyboardDismissMode = .onDrag

        buildGroupMembershipSection()

        textField.becomeFirstResponder()

        let tapGesture = UITapGestureRecognizer(target: self, action: #selector(dismissKeyboard))
        view.addGestureRecognizer(tapGesture)
    }

    private func createInitialViews() {
        stackView.axis = .vertical
        stackView.spacing = 20

        let subtitleLabel = UILabel()
        subtitleLabel.text = OWSLocalizedString(
            "MEMBER_LABEL_VIEW_SUBTITLE",
            comment: "Subtitle for a view where users can edit and preview their member label.",
        )
        subtitleLabel.numberOfLines = 0
        subtitleLabel.font = .dynamicTypeCaption1Clamped
        subtitleLabel.textColor = UIColor.Signal.secondaryLabel
        subtitleLabel.textAlignment = .center

        stackView.addArrangedSubview(subtitleLabel)

        let textFieldStack = UIStackView()
        textFieldStack.layer.cornerRadius = 27
        textFieldStack.backgroundColor = UIColor.Signal.tertiaryBackground
        textFieldStack.axis = .horizontal
        textFieldStack.alignment = .center
        textFieldStack.distribution = .fill
        textFieldStack.spacing = 8
        textFieldStack.isLayoutMarginsRelativeArrangement = true
        textFieldStack.layoutMargins = UIEdgeInsets(top: 0, left: 16, bottom: 0, right: 16)

        if let initialEmoji {
            addEmojiButton.setImage(nil, for: .normal)
            addEmojiButton.setTitle(initialEmoji, for: .normal)
            addEmojiButton.titleLabel?.font = .dynamicTypeTitle3Clamped
        } else {
            addEmojiButton.setImage(UIImage(named: "emoji-plus"), for: .normal)
            addEmojiButton.tintColor = UIColor.Signal.secondaryLabel
        }
        addEmojiButton.setContentHuggingPriority(.required, for: .horizontal)
        addEmojiButton.setContentCompressionResistancePriority(.required, for: .horizontal)
        addEmojiButton.addTarget(self, action: #selector(didTapEmojiPicker), for: .touchUpInside)

        textField.placeholder = OWSLocalizedString(
            "MEMBER_LABEL_VIEW_PLACEHOLDER_TEXT",
            comment: "Placeholder text in text field where user can edit their member label.",
        )
        textField.font = .dynamicTypeBodyClamped
        textField.addTarget(self, action: #selector(textDidChange(_:)), for: .editingChanged)
        textField.delegate = self

        characterCountLabel.isHidden = true
        if let count = initialMemberLabel?.count {
            characterCountLabel.text = String(Self.maxCharCount - count)
            characterCountLabel.font = .dynamicTypeBody
            characterCountLabel.textColor = UIColor.Signal.tertiaryLabel.withAlphaComponent(0.3)
            characterCountLabel.isHidden = Self.maxCharCount - count > Self.showCharacterCountMax
            characterCountLabel.setContentHuggingHorizontalHigh()
            characterCountLabel.setCompressionResistanceHigh()
        }

        clearButton.setImage(UIImage(named: "x-circle-fill-compact"), for: .normal)
        clearButton.tintColor = UIColor.Signal.tertiaryLabel
        clearButton.addTarget(self, action: #selector(clearButtonTapped), for: .touchUpInside)
        clearButton.autoSetDimensions(to: .square(16))
        if initialMemberLabel == nil, initialEmoji == nil {
            clearButton.isHidden = true
        }

        textFieldStack.addArrangedSubview(addEmojiButton)
        textFieldStack.addArrangedSubview(textField)
        textFieldStack.addArrangedSubview(characterCountLabel)
        textFieldStack.addArrangedSubview(clearButton)

        clearButton.autoPinEdge(.trailing, to: .trailing, of: textFieldStack, withOffset: -16)
        characterCountLabel.autoPinEdge(.trailing, to: .leading, of: clearButton, withOffset: -8)

        stackView.addArrangedSubview(textFieldStack)
        stackView.setCustomSpacing(34, after: textFieldStack)

        textFieldStack.translatesAutoresizingMaskIntoConstraints = false
        textFieldStack.heightAnchor.constraint(equalToConstant: 52).isActive = true

        guard
            let mockConversationItem = buildMockConversationItem(),
            let previewContainer = messageBubblePreviewContainer(renderItem: mockConversationItem)
        else {
            return
        }

        stackView.addArrangedSubview(previewContainer)
    }

    private func buildMockConversationItem() -> CVRenderItem? {
        let db = DependenciesBridge.shared.db
        let attachmentContentValidator = DependenciesBridge.shared.attachmentContentValidator
        let tsAccountManager = DependenciesBridge.shared.tsAccountManager

        let messageBody = db.write { tx in
            attachmentContentValidator.truncatedMessageBodyForInlining(
                MessageBody(text: OWSLocalizedString(
                    "MEMBER_LABEL_VIEW_MESSAGE_PREVIEW_TEXT",
                    comment: "Text shown in the preview message bubble when a user is editing their member label.",
                ), ranges: .empty),
                tx: tx,
            )
        }

        guard
            let localAci = db.read(block: { tx in
                tsAccountManager.localIdentifiers(tx: tx)?.aci
            }),
            let secretParams = try? GroupSecretParams.generate()
        else {
            return nil
        }

        var groupModelBuilder = TSGroupModelBuilder(secretParams: secretParams)
        var groupMembershipBuilder = groupModelBuilder.groupMembership.asBuilder
        if let updatedMemberLabel {
            groupMembershipBuilder.setMemberLabel(label: MemberLabel(label: updatedMemberLabel, labelEmoji: updatedEmoji), aci: localAci)
        }
        groupModelBuilder.groupMembership = groupMembershipBuilder.build()

        guard let groupModel = try? groupModelBuilder.buildAsV2() else {
            return nil
        }

        let mockGroupThread = MockGroupThread(groupModel: groupModel)
        let mockMessage = MockIncomingMessage(messageBody: messageBody, thread: mockGroupThread, authorAci: localAci)

        let renderItem = db.read { tx in
            let threadAssociatedData = ThreadAssociatedData.fetchOrDefault(for: mockGroupThread, ignoreMissing: true, transaction: tx)

            let conversationStyle = ConversationStyle(
                type: .`default`,
                thread: mockGroupThread,
                viewWidth: view.width - 44, // stack view padding
                hasWallpaper: false,
                shouldDimWallpaperInDarkMode: false,
                isWallpaperPhoto: false,
                chatColor: PaletteChatColor.ultramarine.colorSetting,
            )

            return CVLoader.buildStandaloneRenderItem(
                interaction: mockMessage,
                thread: mockGroupThread,
                threadAssociatedData: threadAssociatedData,
                conversationStyle: conversationStyle,
                spoilerState: SpoilerRenderState(),
                groupNameColors: groupNameColors,
                transaction: tx,
            )
        }
        return renderItem
    }

    func messageBubblePreviewContainer(renderItem: CVRenderItem) -> UIStackView? {
        let previewTitle = UILabel()
        previewTitle.text = OWSLocalizedString(
            "MEMBER_LABEL_PREVIEW_HEADING",
            comment: "Heading shown above the preview of a message bubble with the edited member label.",
        )
        previewTitle.font = .dynamicTypeBodyClamped.semibold()

        let cellView = CVCellView()
        cellView.configure(renderItem: renderItem, componentDelegate: self)
        cellView.isCellVisible = true
        cellView.autoSetDimension(.height, toSize: renderItem.cellMeasurement.cellSize.height)
        cellView.autoSetDimension(.width, toSize: renderItem.cellMeasurement.cellSize.width)

        let cellContainer = UIView()
        cellContainer.layer.cornerRadius = 27
        cellContainer.layer.masksToBounds = true
        cellContainer.backgroundColor = UIColor.Signal.tertiaryBackground

        cellContainer.addSubview(cellView)
        cellView.autoPinEdge(toSuperviewEdge: .top, withInset: 20)
        cellView.autoPinEdge(toSuperviewEdge: .bottom, withInset: 20)

        previewContainer = UIStackView()
        previewContainer?.axis = .vertical
        previewContainer?.spacing = 8
        previewContainer?.addArrangedSubview(previewTitle)
        previewContainer?.addArrangedSubview(cellContainer)

        return previewContainer
    }

    @objc
    private func clearButtonTapped() {
        textField.text = ""
        updatedMemberLabel = nil
        updatedEmoji = nil
        addEmojiButton.setImage(UIImage(named: "emoji-plus"), for: .normal)
        addEmojiButton.setTitle(nil, for: .normal)
        addEmojiButton.tintColor = UIColor.Signal.secondaryLabel

        reloadMessagePreview()
        reloadDoneButtonStatus()
    }

    @objc
    private func didTapDone() {
        var memberLabel: MemberLabel?
        if let updatedMemberLabel {
            memberLabel = MemberLabel(label: updatedMemberLabel, labelEmoji: updatedEmoji)
        }
        dismiss(animated: true, completion: { [weak self] in
            guard let self else { return }
            self.updateDelegate?.updateLabelForLocalUser(memberLabel: memberLabel)
        })
    }

    @objc
    private func didTapEmojiPicker() {
        let picker = EmojiPickerSheet(message: nil, allowReactionConfiguration: false) { [weak self] emoji in
            guard let emojiString = emoji?.rawValue else {
                return
            }
            self?.updatedEmoji = emojiString
            self?.addEmojiButton.setImage(nil, for: .normal)
            self?.addEmojiButton.setTitle(emojiString, for: .normal)
            self?.addEmojiButton.titleLabel?.font = .dynamicTypeTitle3Clamped
            self?.reloadDoneButtonStatus()
            self?.reloadMessagePreview()
        }
        present(picker, animated: true)
    }

    private func reloadMessagePreview() {
        if let previewContainer {
            stackView.removeArrangedSubview(previewContainer)
            previewContainer.removeFromSuperview()
        }
        previewContainer = nil
        if
            let mockRenderItem = buildMockConversationItem(),
            let previewContainer = messageBubblePreviewContainer(renderItem: mockRenderItem)
        {
            stackView.insertArrangedSubview(previewContainer, at: 2)
        }
        let count = textField.text?.count ?? 0
        let charsRemaining = Self.maxCharCount - count
        characterCountLabel.text = String(charsRemaining)
        characterCountLabel.isHidden = charsRemaining > Self.showCharacterCountMax
        characterCountLabel.textColor = charsRemaining > 5 ? UIColor.Signal.tertiaryLabel.withAlphaComponent(0.3) : UIColor.Signal.red

        if updatedMemberLabel == nil, updatedEmoji == nil {
            clearButton.isHidden = true
        } else {
            clearButton.isHidden = false
        }
    }

    private func reloadDoneButtonStatus() {
        // No change, don't allow sending.
        if initialMemberLabel == updatedMemberLabel, initialEmoji == updatedEmoji {
            navigationItem.rightBarButtonItem?.isEnabled = false
            return
        }

        // Clears member label, this is allowed.
        if updatedMemberLabel == nil, updatedEmoji == nil {
            navigationItem.rightBarButtonItem?.isEnabled = true
            return
        }

        // Don't allow emoji-only.
        if updatedMemberLabel == nil {
            navigationItem.rightBarButtonItem?.isEnabled = false
            return
        }

        navigationItem.rightBarButtonItem?.isEnabled = true
    }

    @objc
    func textDidChange(_ textField: UITextField) {
        let filteredText = textField.text?.filterStringForDisplay()
        let collapsedFilteredText = filteredText?.replacingOccurrences(
            of: "\\s+",
            with: " ",
            options: .regularExpression,
        )

        updatedMemberLabel = collapsedFilteredText?.nilIfEmpty
        reloadDoneButtonStatus()
        reloadMessagePreview()
    }

    // MARK: - UITextFieldDelegate

    func textField(
        _ textField: UITextField,
        shouldChangeCharactersIn range: NSRange,
        replacementString string: String,
    ) -> Bool {
        TextFieldHelper.textField(
            textField,
            shouldChangeCharactersInRange: range,
            replacementString: string,
            maxByteCount: 96,
            maxGlyphCount: Self.maxCharCount,
        )
    }

    // MARK: - Group member list

    private func sortedMembers() -> [(key: SignalServiceAddress, value: MemberLabelForRendering)] {
        let allMembersSorted = db.read { tx in contactManager.sortSignalServiceAddresses(groupMemberLabelsWithoutLocalUser.keys, transaction: tx)
        }

        var membersToRender = [SignalServiceAddress]()
        let groupMembership = groupModel.groupMembership
        // Admin users are first.
        let adminMembers = allMembersSorted.filter { groupMembership.isFullMemberAndAdministrator($0) }
        membersToRender += adminMembers
        // Non-admin users are second.
        let nonAdminMembers = allMembersSorted.filter { !groupMembership.isFullMemberAndAdministrator($0) }
        membersToRender += nonAdminMembers

        return membersToRender.map { (key: $0, value: groupMemberLabelsWithoutLocalUser[$0]!) }
    }

    private func buildGroupMembershipSection() {
        let sectionLabel = UILabel()
        sectionLabel.text = OWSLocalizedString(
            "MEMBER_LABEL_GROUP_LABELS_SECTION_TITLE",
            comment: "Section header for a list of group member labels",
        )
        sectionLabel.font = .dynamicTypeBodyClamped.semibold()

        let contactListStackView = UIStackView()
        contactListStackView.spacing = 5
        contactListStackView.axis = .vertical
        contactListStackView.backgroundColor = UIColor.Signal.tertiaryBackground
        contactListStackView.layer.masksToBounds = true
        contactListStackView.layer.cornerRadius = 26
        contactListStackView.layoutMargins = UIEdgeInsets(top: 8, left: 16, bottom: 8, right: 16)
        contactListStackView.isLayoutMarginsRelativeArrangement = true

        let sortedNonLocalMembers = sortedMembers()
        var cellCount = 0
        for (memberAddress, memberLabel) in sortedNonLocalMembers {
            let cell = ContactCellView()
            SSKEnvironment.shared.databaseStorageRef.read { tx in
                let configuration = ContactCellConfiguration(address: memberAddress, localUserDisplayMode: .asLocalUser)

                configuration.memberLabel = memberLabel

                let isSystemContact = SSKEnvironment.shared.contactManagerRef.fetchSignalAccount(
                    for: memberAddress,
                    transaction: tx,
                ) != nil

                configuration.shouldShowContactIcon = isSystemContact
                cell.configure(configuration: configuration, transaction: tx)

                if cellCount > 0 {
                    let separator = UIView()
                    separator.backgroundColor = UIColor.Signal.tertiaryLabel
                    contactListStackView.addArrangedSubview(separator)
                    NSLayoutConstraint.activate([
                        separator.heightAnchor.constraint(equalToConstant: 0.3),
                    ])
                    contactListStackView.setCustomSpacing(6, after: separator)
                }

                contactListStackView.addArrangedSubview(cell)
                cellCount += 1
            }
        }

        stackView.addArrangedSubview(sectionLabel)
        stackView.setCustomSpacing(8, after: sectionLabel)

        if cellCount > 0 {
            contactListStackView.translatesAutoresizingMaskIntoConstraints = false
            stackView.addArrangedSubview(contactListStackView)
            NSLayoutConstraint.activate([
                contactListStackView.leadingAnchor.constraint(equalTo: stackView.leadingAnchor),
                contactListStackView.trailingAnchor.constraint(equalTo: stackView.trailingAnchor),
            ])
        } else {
            let cellContainer = UIView()
            cellContainer.layer.cornerRadius = 27
            cellContainer.layer.masksToBounds = true
            cellContainer.backgroundColor = UIColor.Signal.tertiaryBackground

            let noOtherMembersLabel = UILabel()
            noOtherMembersLabel.text = OWSLocalizedString("MEMBER_LABEL_NO_OTHER_GROUP_MEMBERS_HAVE_LABELS", comment: "Text for section that shows other group member labels, when there are none")
            noOtherMembersLabel.font = .dynamicTypeFootnoteClamped
            noOtherMembersLabel.textColor = UIColor.Signal.secondaryLabel
            noOtherMembersLabel.textAlignment = .center
            noOtherMembersLabel.numberOfLines = 0
            cellContainer.addSubview(noOtherMembersLabel)
            noOtherMembersLabel.translatesAutoresizingMaskIntoConstraints = false
            NSLayoutConstraint.activate([
                noOtherMembersLabel.centerXAnchor.constraint(equalTo: cellContainer.centerXAnchor),
                noOtherMembersLabel.centerYAnchor.constraint(equalTo: cellContainer.centerYAnchor),
            ])
            stackView.addArrangedSubview(cellContainer)
            cellContainer.translatesAutoresizingMaskIntoConstraints = false
            NSLayoutConstraint.activate([
                cellContainer.leadingAnchor.constraint(equalTo: stackView.leadingAnchor),
                cellContainer.trailingAnchor.constraint(equalTo: stackView.trailingAnchor),
                cellContainer.heightAnchor.constraint(greaterThanOrEqualToConstant: 96),
            ])
        }
    }

    // MARK: -

    @objc
    private func dismissKeyboard() {
        view.endEditing(true)
    }
}

// MARK: -

extension MemberLabelViewController: CVComponentDelegate {
    var spoilerState: SignalUI.SpoilerRenderState {
        return SpoilerRenderState()
    }

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