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

import SafariServices
import SignalServiceKit
public import SignalUI

public class NewGroupConfirmViewController: OWSTableViewController2 {

    private let newGroupState: NewGroupState

    private var groupSeed: NewGroupSeed {
        return newGroupState.groupSeed
    }

    private var recipientSet: OrderedSet<PickedRecipient> {
        return newGroupState.recipientSet
    }

    private let helper: GroupAttributesEditorHelper

    private var nameTextField: UITextField {
        return helper.nameTextField
    }

    private lazy var disappearingMessagesConfiguration = SSKEnvironment.shared.databaseStorageRef.read { tx in
        let dmConfigurationStore = DependenciesBridge.shared.disappearingMessagesConfigurationStore
        return dmConfigurationStore.fetchOrBuildDefault(for: .universal, tx: tx)
    }

    init(newGroupState: NewGroupState) {
        self.newGroupState = newGroupState

        self.helper = GroupAttributesEditorHelper(
            groupId: try! newGroupState.groupSeed.groupSecretParams.getPublicParams().getGroupIdentifier().serialize(),
            groupNameOriginal: newGroupState.groupName,
            groupDescriptionOriginal: nil,
            avatarOriginalData: newGroupState.avatarData,
            iconViewSize: 64,
        )

        super.init()

        self.shouldAvoidKeyboard = true
    }

    // MARK: - View Lifecycle

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

        title = OWSLocalizedString(
            "NEW_GROUP_NAME_GROUP_VIEW_TITLE",
            comment: "The title for the 'name new group' view.",
        )

        navigationItem.rightBarButtonItem = UIBarButtonItem(
            title: OWSLocalizedString(
                "NEW_GROUP_CREATE_BUTTON",
                comment: "The title for the 'create group' button.",
            ),
            style: .plain,
            target: self,
            action: #selector(createNewGroup),
            accessibilityIdentifier: UIView.accessibilityIdentifier(in: self, name: "create"),
        )

        // First section.

        helper.delegate = self
        helper.buildContents()

        tableView.register(ContactTableViewCell.self, forCellReuseIdentifier: ContactTableViewCell.reuseIdentifier)

        updateTableContents()
    }

    private var lastViewSize = CGSize.zero
    override public func viewWillLayoutSubviews() {
        super.viewWillLayoutSubviews()

        guard view.frame.size != lastViewSize else { return }
        lastViewSize = view.frame.size
        updateTableContents()
    }

    private func allMembersSupportGroupsV2() -> Bool {
        return recipientSet.orderedMembers.allSatisfy {
            guard let address = $0.address else {
                return false
            }
            return GroupManager.doesUserSupportGroupsV2(address: address)
        }
    }

    override public func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)

        nameTextField.becomeFirstResponder()
    }

    // MARK: -

    private func updateTableContents() {
        let contents = OWSTableContents()

        let nameAndAvatarSection = OWSTableSection()

        let members = SSKEnvironment.shared.databaseStorageRef.read { transaction in
            BaseGroupMemberViewController.sortedMemberAddresses(recipientSet: self.recipientSet, tx: transaction)
        }

        if members.isEmpty {
            nameAndAvatarSection.footerTitle = OWSLocalizedString(
                "GROUP_MEMBERS_NO_OTHER_MEMBERS",
                comment: "Label indicating that a new group has no other members.",
            )
        }

        nameAndAvatarSection.add(.init(
            customCellBlock: { [weak self] in
                let cell = OWSTableItem.newCell()
                cell.selectionStyle = .none
                guard let self else { return cell }

                self.helper.avatarWrapper.setContentHuggingVerticalHigh()
                self.helper.nameTextField.setContentHuggingHorizontalLow()
                let firstSection = UIStackView(arrangedSubviews: [
                    self.helper.avatarWrapper,
                    self.helper.nameTextField,
                ])
                firstSection.axis = .horizontal
                firstSection.alignment = .center
                firstSection.spacing = ContactCellView.avatarTextHSpacing

                cell.contentView.addSubview(firstSection)
                firstSection.autoPinEdgesToSuperviewMargins()

                return cell
            },
            actionBlock: {},
        ))
        contents.add(nameAndAvatarSection)

        let disappearingMessagesSection = OWSTableSection()
        disappearingMessagesSection.add(.init(
            customCellBlock: { [weak self] in
                guard let self else { return UITableViewCell() }
                let cell = OWSTableItem.buildCell(
                    icon: self.disappearingMessagesConfiguration.isEnabled
                        ? .chatSettingsTimerOn
                        : .chatSettingsTimerOff,
                    itemName: OWSLocalizedString(
                        "DISAPPEARING_MESSAGES",
                        comment: "table cell label in conversation settings",
                    ),
                    accessoryText: self.disappearingMessagesConfiguration.isEnabled
                        ? DateUtil.formatDuration(seconds: self.disappearingMessagesConfiguration.durationSeconds, useShortFormat: true)
                        : CommonStrings.switchOff,
                    accessoryType: .disclosureIndicator,
                    accessibilityIdentifier: UIView.accessibilityIdentifier(in: self, name: "disappearing_messages"),
                )
                return cell
            },
            actionBlock: { [weak self] in
                guard let self else { return }
                let vc = DisappearingMessagesTimerSettingsViewController(
                    initialConfiguration: self.disappearingMessagesConfiguration,
                    settingsMode: .newGroup,
                ) { configuration in
                    self.disappearingMessagesConfiguration = configuration
                    self.updateTableContents()
                }
                self.presentFormSheet(OWSNavigationController(rootViewController: vc), animated: true)
            },
        ))
        contents.add(disappearingMessagesSection)

        if members.count > 0 {
            let section = OWSTableSection()
            section.headerTitle = OWSLocalizedString(
                "GROUP_MEMBERS_SECTION_TITLE_MEMBERS",
                comment: "Title for the 'members' section of the 'group members' view.",
            )

            for address in members {
                section.add(OWSTableItem(
                    dequeueCellBlock: { tableView in
                        guard let cell = tableView.dequeueReusableCell(withIdentifier: ContactTableViewCell.reuseIdentifier) as? ContactTableViewCell else {
                            owsFailDebug("Missing cell.")
                            return UITableViewCell()
                        }

                        cell.selectionStyle = .none

                        SSKEnvironment.shared.databaseStorageRef.read { transaction in
                            let configuration = ContactCellConfiguration(address: address, localUserDisplayMode: .asUser)
                            cell.configure(configuration: configuration, transaction: transaction)
                        }
                        return cell
                    },
                ))
            }
            contents.add(section)
        }

        self.contents = contents
    }

    // MARK: - Actions

    @objc
    private func createNewGroup() {
        AssertIsOnMainThread()

        guard let localAddress = DependenciesBridge.shared.tsAccountManager.localIdentifiersWithMaybeSneakyTransaction?.aciAddress else {
            owsFailDebug("missing local address")
            return
        }
        guard let groupName = newGroupState.groupName.flatMap({ StrippedNonEmptyString(rawValue: $0) }) else {
            Self.showMissingGroupNameAlert()
            return
        }
        owsAssertDebug(allMembersSupportGroupsV2(), "Members must already be checked for v2 support.")

        let avatarData = newGroupState.avatarData
        let memberSet = Set([localAddress] + recipientSet.orderedMembers.compactMap { $0.address })
        let members = Array(memberSet)
        let newGroupSeed = groupSeed
        let disappearingMessageToken = disappearingMessagesConfiguration.asToken

        // GroupsV2 TODO: Should we allow cancel here?
        ModalActivityIndicatorViewController.present(
            fromViewController: self,
            title: OWSLocalizedString(
                "GROUP_CREATING",
                comment: "Displayed in a full screen modal when creating a new group.",
            ),
            canCancel: false,
        ) { modal in
            Task {
                do {
                    let groupThread = try await GroupManager.localCreateNewGroup(
                        seed: newGroupSeed,
                        members: members,
                        name: groupName,
                        avatarData: avatarData,
                        disappearingMessageToken: disappearingMessageToken,
                    )
                    self.groupWasCreated(
                        groupThread: groupThread,
                        modalActivityIndicator: modal,
                    )
                } catch {
                    owsFailDebug("Could not create group: \(error)")

                    modal.dismiss {
                        // Partial success could create the group on the service. This would cause
                        // retries to fail with 409. Therefore we rotate the seed after failures.
                        self.newGroupState.deriveNewGroupSeedForRetry()

                        NewGroupConfirmViewController.showCreateErrorUI(error: error)
                    }
                }
            }
        }
    }

    public class func showCreateErrorUI(error: Error) {
        AssertIsOnMainThread()

        if error.isNetworkFailureOrTimeout {
            OWSActionSheets.showActionSheet(
                title: OWSLocalizedString(
                    "ERROR_NETWORK_FAILURE",
                    comment: "Error indicating network connectivity problems.",
                ),
                message: OWSLocalizedString(
                    "NEW_GROUP_CREATION_FAILED_DUE_TO_NETWORK",
                    comment: "Error indicating that a new group could not be created due to network connectivity problems.",
                ),
            )
            return
        }

        OWSActionSheets.showActionSheet(title: OWSLocalizedString(
            "NEW_GROUP_CREATION_FAILED",
            comment: "Error indicating that a new group could not be created.",
        ))
    }

    public class func showMissingGroupNameAlert() {
        AssertIsOnMainThread()

        OWSActionSheets.showActionSheet(
            title: OWSLocalizedString(
                "NEW_GROUP_CREATION_MISSING_NAME_ALERT_TITLE",
                comment: "Title for error alert indicating that a group name is required.",
            ),
            message: OWSLocalizedString(
                "NEW_GROUP_CREATION_MISSING_NAME_ALERT_MESSAGE",
                comment: "Message for error alert indicating that a group name is required.",
            ),
        )
    }

    func groupWasCreated(
        groupThread: TSGroupThread,
        modalActivityIndicator: ModalActivityIndicatorViewController,
    ) {
        AssertIsOnMainThread()

        let hasAnyRemoteMembers = groupThread.groupModel.groupMembership.allMembersOfAnyKind.count > 1

        func navigateToNewGroup(completion: (() -> Void)?) {
            _ = self.presentingViewController?.dismiss(animated: true) {
                SignalApp.shared.presentConversationForThread(
                    threadUniqueId: groupThread.uniqueId,
                    action: hasAnyRemoteMembers ? .none : .newGroupActionSheet,
                    animated: false,
                )
                completion?()
            }
        }

        let pendingMembers = groupThread.groupModel.groupMembership.invitedMembers
        guard let firstPendingMember = pendingMembers.first else {
            // No pending members.
            return navigateToNewGroup(completion: nil)
        }

        let alertTitle: String
        let alertMessage: String
        let alertTitleFormat = OWSLocalizedString(
            "GROUP_INVITES_SENT_ALERT_TITLE_%d",
            tableName: "PluralAware",
            comment: "Format for the title for an alert indicating that some members were invited to a group. Embeds: {{ the number of invites sent. }}",
        )
        if pendingMembers.count > 1 {
            alertTitle = String.localizedStringWithFormat(alertTitleFormat, pendingMembers.count)
            alertMessage = OWSLocalizedString(
                "GROUP_INVITES_SENT_ALERT_TITLE_N_MESSAGE",
                comment: "Message for an alert indicating that some members were invited to a group.",
            )
        } else {
            alertTitle = String.localizedStringWithFormat(alertTitleFormat, 1)
            let inviteeName = SSKEnvironment.shared.databaseStorageRef.read { tx in
                return SSKEnvironment.shared.contactManagerRef.displayName(for: firstPendingMember, tx: tx).resolvedValue()
            }
            let alertMessageFormat = OWSLocalizedString(
                "GROUP_INVITES_SENT_ALERT_MESSAGE_1_FORMAT",
                comment: "Format for the message for an alert indicating that a member was invited to a group. Embeds: {{ the name of the member. }}",
            )
            alertMessage = String.nonPluralLocalizedStringWithFormat(alertMessageFormat, inviteeName)
        }

        let actionSheet = ActionSheetController(title: alertTitle, message: alertMessage)

        actionSheet.addAction(ActionSheetAction(
            title: CommonStrings.learnMore,
            style: .default,
        ) { _ in
            // We present the "learn more" view atop the
            // new conversation view to avoid users getting
            // stucks in the "create group" view.
            navigateToNewGroup {
                Self.showLearnMoreView()
            }
        })
        actionSheet.addAction(ActionSheetAction(
            title: CommonStrings.okayButton,
            style: .default,
        ) { _ in
            navigateToNewGroup(completion: nil)
        })

        modalActivityIndicator.dismiss {
            self.presentActionSheet(actionSheet)
        }
    }

    private class func showLearnMoreView() {
        guard let fromViewController = CurrentAppContext().frontmostViewController() else {
            owsFailDebug("Missing fromViewController.")
            return
        }
        let vc = SFSafariViewController(url: URL.Support.groups)
        fromViewController.present(vc, animated: true, completion: nil)
    }
}

// MARK: -

extension NewGroupConfirmViewController: GroupAttributesEditorHelperDelegate {
    func groupAttributesEditorContentsDidChange() {
        newGroupState.groupName = helper.groupNameCurrent
        newGroupState.avatarData = helper.avatarCurrent?.imageData
    }
}