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

import LibSignalClient
import SignalServiceKit
import SignalUI

protocol MemberLabelViewControllerPresenter: UIViewController {
    func reloadMemberLabelIfNeeded()
}

/// Responsible for flows related to setting or editing a group member label.
/// Since users can only set/edit their own member label, this is local-user specific.
/// See also `MemberLabelViewController` which allows a user to set/edit their own member label.
public final class MemberLabelCoordinator {
    weak var presenter: MemberLabelViewControllerPresenter?

    var groupModel: TSGroupModelV2
    private let memberLabel: MemberLabel?
    private let kvStore: NewKeyValueStore
    private let groupNameColors: GroupNameColors
    private let localIdentifiers: LocalIdentifiers
    private let db: DB
    private let profileManager: ProfileManager

    private enum KVStoreKeys {
        static let ignoreMemberLabelAboutOverrideKey = "ignoreMemberLabelAboutOverrideKeyV2"
    }

    convenience init(
        groupModel: TSGroupModelV2,
        groupNameColors: GroupNameColors,
        localIdentifiers: LocalIdentifiers,
    ) {
        self.init(
            groupModel: groupModel,
            groupNameColors: groupNameColors,
            localIdentifiers: localIdentifiers,
            db: DependenciesBridge.shared.db,
            profileManager: SSKEnvironment.shared.profileManagerRef,
        )
    }

    init(
        groupModel: TSGroupModelV2,
        groupNameColors: GroupNameColors,
        localIdentifiers: LocalIdentifiers,
        db: DB,
        profileManager: ProfileManager,
    ) {
        self.groupModel = groupModel
        self.memberLabel = groupModel.groupMembership.localUserMemberLabel
        self.groupNameColors = groupNameColors
        self.localIdentifiers = localIdentifiers
        self.db = db
        self.profileManager = profileManager
        self.kvStore = NewKeyValueStore(collection: "MemberLabelCoordinator")
    }

    func presentWithEducationSheet(
        localUserHasMemberLabel: Bool,
        canEditMemberLabel: Bool,
    ) {
        var editBlock: (() -> Void)?
        if canEditMemberLabel {
            editBlock = { [weak self] in
                self?.present()
            }
        }

        let hero = MemberLabelEducationHeroSheet(
            hasMemberLabel: localUserHasMemberLabel,
            editMemberLabelHandler: editBlock,
        )
        presenter?.present(hero, animated: true)
    }

    private func buildGroupMemberLabelsWithoutLocalUser() -> [SignalServiceAddress: MemberLabelForRendering] {
        var memberLabels: [SignalServiceAddress: MemberLabelForRendering] = [:]
        let groupMembership = groupModel.groupMembership
        for member in groupMembership.fullMembers {
            guard
                !localIdentifiers.contains(address: member),
                let aci = member.aci,
                let memberLabel = groupMembership.memberLabel(for: aci)
            else {
                continue
            }
            let groupNameColor = groupNameColors.color(for: aci)
            let memberLabelForRendering = MemberLabelForRendering(
                label: memberLabel.labelForRendering(),
                groupNameColor: groupNameColor,
            )
            memberLabels[member] = memberLabelForRendering
        }
        return memberLabels
    }

    func present() {
        let memberLabelViewController = MemberLabelViewController(
            memberLabel: memberLabel?.label,
            emoji: memberLabel?.labelEmoji,
            groupNameColors: groupNameColors,
            groupMemberLabelsWithoutLocalUser: buildGroupMemberLabelsWithoutLocalUser(),
            groupModel: groupModel,
            db: db,
            contactManager: SSKEnvironment.shared.contactManagerImplRef,
        )
        memberLabelViewController.updateDelegate = self

        presenter?.present(OWSNavigationController(rootViewController: memberLabelViewController), animated: true)
    }

    private func showOverrideAboutWarningIfNeeded(localUserBio: String?) {
        let ignoreMemberLabelAboutOverrideKey = db.read { tx in
            let value = self.kvStore.fetchValue(
                Bool.self,
                forKey: KVStoreKeys.ignoreMemberLabelAboutOverrideKey,
                tx: tx,
            )
            return value == true
        }

        if localUserBio != nil, !ignoreMemberLabelAboutOverrideKey {
            let hero = MemberLabelAboutOverrideHeroSheet(
                dontShowAgainHandler: { [weak self] in
                    self?.db.write { tx in
                        self?.kvStore.writeValue(
                            true,
                            forKey: KVStoreKeys.ignoreMemberLabelAboutOverrideKey,
                            tx: tx,
                        )
                    }
                },
            )
            presenter?.present(hero, animated: true)
        }
    }

    private func showMemberLabelSaveFailed() {
        OWSActionSheets.showActionSheet(
            title: OWSLocalizedString(
                "MEMBER_LABEL_FAIL_TO_SAVE",
                comment: "Error indicating member label could not save.",
            ),
            message: OWSLocalizedString(
                "CHECK_YOUR_CONNECTION_TRY_AGAIN_WARNING",
                comment: "Message indicating a user should check connection and try again.",
            ),
        )
    }

    func updateLabelForLocalUser(memberLabel: MemberLabel?) {
        guard let presenter else { return }
        let changeLabelBlock: () -> Void = {
            Task { @MainActor in
                let localUserBio = self.db.read(block: { tx -> String? in
                    return self.profileManager.userProfile(for: SignalServiceAddress(self.localIdentifiers.aci), tx: tx)?.bioForDisplay
                })

                do {
                    try await ModalActivityIndicatorViewController.presentAndPropagateResult(
                        from: presenter,
                        title: CommonStrings.updatingModal,
                        wrappedAsyncBlock: {
                            try await GroupManager.changeMemberLabel(
                                groupModel: self.groupModel,
                                aci: self.localIdentifiers.aci,
                                label: memberLabel,
                            )
                        },
                    )

                    if memberLabel != nil {
                        self.showOverrideAboutWarningIfNeeded(localUserBio: localUserBio)
                    }
                    presenter.reloadMemberLabelIfNeeded()
                } catch {
                    self.showMemberLabelSaveFailed()
                }
            }
        }

        if let p = presenter.presentedViewController {
            p.dismiss(animated: true, completion: {
                changeLabelBlock()
            })
            return
        }
        changeLabelBlock()
    }
}