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

import LibSignalClient
public import SignalServiceKit
public import SignalUI

public class AddToGroupViewController: OWSTableViewController2 {

    private let address: SignalServiceAddress

    init(address: SignalServiceAddress) {
        self.address = address
        super.init()
    }

    public class func presentForUser(
        _ address: SignalServiceAddress,
        from fromViewController: UIViewController,
    ) {
        AssertIsOnMainThread()

        let view = AddToGroupViewController(address: address)
        let modal = OWSNavigationController(rootViewController: view)
        fromViewController.presentFormSheet(modal, animated: true)
    }

    // MARK: -

    override public func viewDidLoad() {
        super.viewDidLoad()

        title = OWSLocalizedString("ADD_TO_GROUP_TITLE", comment: "Title of the 'add to group' view.")

        navigationItem.rightBarButtonItem = .cancelButton { [weak self] in
            self?.didPressCloseButton()
        }

        defaultSeparatorInsetLeading = Self.cellHInnerMargin + CGFloat(AvatarBuilder.smallAvatarSizePoints) + ContactCellView.avatarTextHSpacing

        Task {
            await self.updateGroupThreads()
        }
    }

    private var groupThreads = [TSGroupThread]() {
        didSet {
            AssertIsOnMainThread()
            updateTableContents()
        }
    }

    private func updateGroupThreads() async {
        self.groupThreads = await fetchGroupThreads()
    }

    private nonisolated func fetchGroupThreads() async -> [TSGroupThread] {
        let databaseStorage = SSKEnvironment.shared.databaseStorageRef
        return databaseStorage.read { transaction in
            var result = [TSGroupThread]()

            do {
                try ThreadFinder().enumerateGroupThreads(transaction: transaction) { thread -> Bool in
                    if thread.isGroupV2Thread {
                        let groupViewHelper = GroupViewHelper(
                            threadViewModel: ThreadViewModel(
                                thread: thread,
                                forChatList: false,
                                transaction: transaction,
                            ),
                            memberLabelCoordinator: nil,
                        )

                        if groupViewHelper.canEditConversationMembership {
                            result.append(thread)
                        }
                    }

                    return true
                }
            } catch {
                owsFailDebug("Failed to fetch group threads: \(error). Returning an empty array")
            }

            return result
        }
    }

    private func updateTableContents() {
        AssertIsOnMainThread()
        let databaseStorage = SSKEnvironment.shared.databaseStorageRef
        let groupsSection = databaseStorage.read { tx in
            return OWSTableSection(items: groupThreads.map { item(forGroupThread: $0, tx: tx) })
        }
        self.contents = OWSTableContents(sections: [groupsSection])
    }

    // MARK: Helpers

    override public func themeDidChange() {
        super.themeDidChange()
        self.tableView.sectionIndexColor = Theme.primaryTextColor
        updateTableContents()
    }

    private func didPressCloseButton() {
        Logger.info("")

        self.dismiss(animated: true)
    }

    private func didSelectGroup(_ groupThread: TSGroupThread) {
        let shortName = SSKEnvironment.shared.databaseStorageRef.read { transaction in
            return SSKEnvironment.shared.contactManagerRef.displayName(for: self.address, tx: transaction).resolvedValue(useShortNameIfAvailable: true)
        }

        let messageFormat = OWSLocalizedString(
            "ADD_TO_GROUP_ACTION_SHEET_MESSAGE_FORMAT",
            comment: "The title on the 'add to group' confirmation action sheet. Embeds {contact name, group name}",
        )

        OWSActionSheets.showConfirmationAlert(
            title: OWSLocalizedString(
                "ADD_TO_GROUP_ACTION_SHEET_TITLE",
                comment: "The title on the 'add to group' confirmation action sheet.",
            ),
            message: String.nonPluralLocalizedStringWithFormat(messageFormat, shortName, groupThread.groupNameOrDefault),
            proceedTitle: OWSLocalizedString(
                "ADD_TO_GROUP_ACTION_PROCEED_BUTTON",
                comment: "The button on the 'add to group' confirmation to add the user to the group.",
            ),
            proceedStyle: .default,
        ) { _ in
            self.addToGroup(groupThread, shortName: shortName)
        }
    }

    private func addToGroup(_ groupThread: TSGroupThread, shortName: String) {
        AssertIsOnMainThread()
        owsPrecondition(groupThread.isGroupV2Thread) // non-gv2 filtered above when fetching groups

        guard let serviceId = self.address.serviceId else {
            GroupViewUtils.showInvalidGroupMemberAlert(fromViewController: self)
            return
        }

        let oldGroupModel = groupThread.groupModel

        GroupViewUtils.updateGroupWithActivityIndicator(
            fromViewController: self,
            updateBlock: {
                try await GroupManager.addOrInvite(
                    serviceIds: [serviceId],
                    toExistingGroup: oldGroupModel,
                )
            },
            completion: { [weak self] in
                self?.notifyOfAddedAndDismiss(groupThread: groupThread, shortName: shortName)
            },
        )
    }

    private func notifyOfAddedAndDismiss(groupThread: TSGroupThread, shortName: String) {
        dismiss(animated: true) { [presentingViewController] in
            let toastFormat = OWSLocalizedString(
                "ADD_TO_GROUP_SUCCESS_TOAST_FORMAT",
                comment: "A toast on the 'add to group' view indicating the user was added. Embeds {contact name} and {group name}",
            )
            let toastText = String.nonPluralLocalizedStringWithFormat(toastFormat, shortName, groupThread.groupNameOrDefault)
            presentingViewController?.presentToast(text: toastText)
        }
    }

    // MARK: -

    private func item(forGroupThread groupThread: TSGroupThread, tx: DBReadTransaction) -> OWSTableItem {
        let alreadyAMemberText = OWSLocalizedString(
            "ADD_TO_GROUP_ALREADY_A_MEMBER",
            comment: "Text indicating your contact is already a member of the group on the 'add to group' view.",
        )
        let isAlreadyAMember: Bool
        if let serviceId = self.address.serviceId {
            switch groupThread.groupMembership.canTryToAddToGroup(serviceId: serviceId) {
            case .alreadyInGroup:
                isAlreadyAMember = true
            case .addableWithProfileKeyCredential:
                let canAddToGroup = GroupMembership.canTryToAddWithProfileKeyCredential(serviceId: serviceId, tx: tx)
                isAlreadyAMember = !canAddToGroup
            case .addableOrInvitable:
                isAlreadyAMember = false
            }
        } else {
            isAlreadyAMember = false
        }

        return OWSTableItem(
            customCellBlock: {
                let cell = GroupTableViewCell()
                cell.configure(
                    thread: groupThread,
                    customSubtitle: isAlreadyAMember ? alreadyAMemberText : nil,
                    customTextColor: isAlreadyAMember ? .Signal.tertiaryLabel : nil,
                )
                cell.isUserInteractionEnabled = !isAlreadyAMember
                return cell
            },
            actionBlock: { [weak self] in
                self?.didSelectGroup(groupThread)
            },
        )
    }
}