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

import SignalServiceKit
import SignalUI

protocol GroupPermissionsSettingsDelegate: AnyObject {
    func groupPermissionSettingsDidUpdate()
}

class GroupPermissionsSettingsViewController: OWSTableViewController2 {
    private var threadViewModel: ThreadViewModel
    private var thread: TSThread { threadViewModel.threadRecord }
    private var groupViewHelper: GroupViewHelper
    private weak var permissionsDelegate: GroupPermissionsSettingsDelegate?

    private var groupModelV2: TSGroupModelV2! {
        thread.groupModelIfGroupThread as? TSGroupModelV2
    }

    private var oldAccessMembers: GroupV2Access { groupModelV2.access.members }
    private var oldAccessAttributes: GroupV2Access { groupModelV2.access.attributes }
    private var oldIsAnnouncementsOnly: Bool { groupModelV2.isAnnouncementsOnly }
    private var oldAccessMemberLabels: GroupV2Access { groupModelV2.access.memberLabels }

    private lazy var newAccessMembers = oldAccessMembers
    private lazy var newAccessAttributes = oldAccessAttributes
    private lazy var newIsAnnouncementsOnly = oldIsAnnouncementsOnly
    private lazy var newAccessMemberLabels = oldAccessMemberLabels

    init(threadViewModel: ThreadViewModel, delegate: GroupPermissionsSettingsDelegate) {
        owsAssertDebug(threadViewModel.threadRecord.isGroupV2Thread)
        self.threadViewModel = threadViewModel
        self.groupViewHelper = GroupViewHelper(threadViewModel: threadViewModel, memberLabelCoordinator: nil)
        self.permissionsDelegate = delegate

        super.init()

        self.groupViewHelper.delegate = self
    }

    override func viewDidLoad() {
        super.viewDidLoad()

        title = OWSLocalizedString(
            "CONVERSATION_SETTINGS_PERMISSIONS",
            comment: "Label for 'permissions' action in conversation settings view.",
        )

        navigationItem.leftBarButtonItem = .cancelButton(
            dismissingFrom: self,
            hasUnsavedChanges: { [weak self] in self?.hasUnsavedChanges },
        )

        navigationItem.rightBarButtonItem = .setButton { [weak self] in
            self?.didTapSet()
        }

        updateTableContents()
        updateNavigation()
    }

    private var hasUnsavedChanges: Bool {
        guard groupViewHelper.canEditPermissions else {
            return false
        }

        return oldAccessMembers != newAccessMembers ||
            oldAccessAttributes != newAccessAttributes ||
            oldIsAnnouncementsOnly != newIsAnnouncementsOnly ||
            oldAccessMemberLabels != newAccessMemberLabels
    }

    // Don't allow interactive dismiss when there are unsaved changes.
    override var isModalInPresentation: Bool {
        get { hasUnsavedChanges }
        set {}
    }

    private func updateNavigation() {
        navigationItem.rightBarButtonItem?.isEnabled = hasUnsavedChanges
    }

    func updateTableContents() {
        let contents = OWSTableContents()
        defer { self.contents = contents }

        let accessMembersSection = OWSTableSection()
        accessMembersSection.headerTitle = OWSLocalizedString(
            "CONVERSATION_SETTINGS_EDIT_MEMBERSHIP_ACCESS",
            comment: "Label for 'edit membership access' action in conversation settings view.",
        )
        accessMembersSection.footerTitle = OWSLocalizedString(
            "CONVERSATION_SETTINGS_EDIT_MEMBERSHIP_ACCESS_FOOTER",
            comment: "Description for the 'edit membership access'.",
        )

        accessMembersSection.add(.init(
            text: OWSLocalizedString(
                "CONVERSATION_SETTINGS_EDIT_ATTRIBUTES_ACCESS_ALERT_MEMBERS_BUTTON",
                comment: "Label for button that sets 'group attributes access' to 'members-only'.",
            ),
            actionBlock: { [weak self] in
                self?.tryToSetAccessMembers(.member)
            },
            accessoryType: newAccessMembers == .member ? .checkmark : .none,
        ))
        accessMembersSection.add(.init(
            text: OWSLocalizedString(
                "CONVERSATION_SETTINGS_EDIT_ATTRIBUTES_ACCESS_ALERT_ADMINISTRATORS_BUTTON",
                comment: "Label for button that sets 'group attributes access' to 'administrators-only'.",
            ),
            actionBlock: { [weak self] in
                self?.tryToSetAccessMembers(.administrator)
            },
            accessoryType: newAccessMembers == .administrator ? .checkmark : .none,
        ))

        contents.add(accessMembersSection)

        let accessAttributesSection = OWSTableSection()
        accessAttributesSection.headerTitle = OWSLocalizedString(
            "CONVERSATION_SETTINGS_EDIT_ATTRIBUTES_ACCESS",
            comment: "Label for 'edit attributes access' action in conversation settings view.",
        )
        accessAttributesSection.footerTitle = OWSLocalizedString(
            "CONVERSATION_SETTINGS_ATTRIBUTES_ACCESS_SECTION_FOOTER_V2",
            comment: "Footer for the 'attributes access' section in conversation settings view with pinned messages added.",
        )

        accessAttributesSection.add(.init(
            text: OWSLocalizedString(
                "CONVERSATION_SETTINGS_EDIT_ATTRIBUTES_ACCESS_ALERT_MEMBERS_BUTTON",
                comment: "Label for button that sets 'group attributes access' to 'members-only'.",
            ),
            actionBlock: { [weak self] in
                self?.tryToSetAccessAttributes(.member)
            },
            accessoryType: newAccessAttributes == .member ? .checkmark : .none,
        ))
        accessAttributesSection.add(.init(
            text: OWSLocalizedString(
                "CONVERSATION_SETTINGS_EDIT_ATTRIBUTES_ACCESS_ALERT_ADMINISTRATORS_BUTTON",
                comment: "Label for button that sets 'group attributes access' to 'administrators-only'.",
            ),
            actionBlock: { [weak self] in
                self?.tryToSetAccessAttributes(.administrator)
            },
            accessoryType: newAccessAttributes == .administrator ? .checkmark : .none,
        ))

        contents.add(accessAttributesSection)

        let isAnnouncementsOnly = self.newIsAnnouncementsOnly

        let announcementOnlySection = OWSTableSection()
        announcementOnlySection.headerTitle = OWSLocalizedString(
            "CONVERSATION_SETTINGS_SEND_MESSAGES_SECTION_HEADER",
            comment: "Label for 'send messages' action in conversation settings permissions view.",
        )
        announcementOnlySection.footerTitle = OWSLocalizedString(
            "CONVERSATION_SETTINGS_SEND_MESSAGES_SECTION_FOOTER",
            comment: "Footer for the 'send messages' section in conversation settings permissions view.",
        )

        announcementOnlySection.add(.init(
            text: OWSLocalizedString(
                "CONVERSATION_SETTINGS_SEND_MESSAGES_SECTION_ALL_MEMBERS",
                comment: "Label for button that sets 'send messages permission' for a group to 'all members'.",
            ),
            actionBlock: { [weak self] in
                self?.tryToSetIsAnnouncementsOnly(false)
            },
            accessoryType: !isAnnouncementsOnly ? .checkmark : .none,
        ))
        announcementOnlySection.add(.init(
            text: OWSLocalizedString(
                "CONVERSATION_SETTINGS_SEND_MESSAGES_SECTION_ONLY_ADMINS",
                comment: "Label for button that sets 'send messages permission' for a group to 'administrators only'.",
            ),
            actionBlock: { [weak self] in
                self?.tryToSetIsAnnouncementsOnly(true)
            },
            accessoryType: isAnnouncementsOnly ? .checkmark : .none,
        ))

        contents.add(announcementOnlySection)

        let accessMemberLabelsSection = OWSTableSection()
        accessMemberLabelsSection.headerTitle = OWSLocalizedString(
            "CONVERSATION_SETTINGS_EDIT_MEMBER_LABELS_ACCESS",
            comment: "Label for 'edit member label access' action in conversation settings view.",
        )
        accessMemberLabelsSection.footerTitle = OWSLocalizedString(
            "CONVERSATION_SETTINGS_EDIT_MEMBER_LABELS_ACCESS_FOOTER",
            comment: "Description for the 'edit member label access'.",
        )

        accessMemberLabelsSection.add(.init(
            text: OWSLocalizedString(
                "CONVERSATION_SETTINGS_MEMBER_LABELS_ACCESS_ALERT_MEMBERS_BUTTON",
                comment: "Label for button that sets 'member labels access' to 'members-only'.",
            ),
            actionBlock: { [weak self] in
                self?.tryToSetAccessMemberLabels(.member)
            },
            accessoryType: newAccessMemberLabels == .member ? .checkmark : .none,
        ))
        accessMemberLabelsSection.add(.init(
            text: OWSLocalizedString(
                "CONVERSATION_SETTINGS_MEMBER_LABELS_ACCESS_ADMINISTRATORS_BUTTON",
                comment: "Label for button that sets 'member labels access' to 'administrators-only'.",
            ),
            actionBlock: { [weak self] in
                self?.tryToSetAccessMemberLabels(.administrator)
            },
            accessoryType: newAccessMemberLabels == .administrator ? .checkmark : .none,
        ))

        if BuildFlags.MemberLabel.send {
            contents.add(accessMemberLabelsSection)
        }
    }

    private func tryToSetAccessMembers(_ value: GroupV2Access) {
        guard groupViewHelper.canEditPermissions else {
            showAdminOnlyWarningAlert()
            return
        }
        self.newAccessMembers = value
        self.updateTableContents()
        self.updateNavigation()
    }

    private func nonAdminsHaveMemberLabels() -> Bool {
        let groupMembership = groupModelV2.groupMembership
        let nonAdmins = groupMembership.fullMembers.filter { !groupMembership.isFullMemberAndAdministrator($0) }
        for nonAdmin in nonAdmins {
            if let aci = nonAdmin.aci, groupMembership.memberLabel(for: aci) != nil {
                return true
            }
        }
        return false
    }

    private func tryToSetAccessMemberLabels(_ value: GroupV2Access) {
        guard groupViewHelper.canEditPermissions else {
            showAdminOnlyWarningAlert()
            return
        }
        let continueBlock = {
            self.newAccessMemberLabels = value
            self.updateTableContents()
            self.updateNavigation()
        }

        switch value {
        case .administrator:
            if oldAccessMemberLabels != .administrator, nonAdminsHaveMemberLabels() {
                showClearMemberLabelWarning(continueHandler: {
                    continueBlock()
                })
            } else {
                fallthrough
            }
        case .member, .unknown, .any, .unsatisfiable:
            continueBlock()
        }
    }

    private func showClearMemberLabelWarning(continueHandler: @escaping () -> Void) {
        let actionSheet = ActionSheetController(
            title: OWSLocalizedString(
                "MEMBER_LABEL_CLEAR_WARNING_TITLE",
                comment: "Title for action sheet when attempting to change the group attributes access to 'administrator'",
            ),
            message: OWSLocalizedString(
                "MEMBER_LABEL_CLEAR_WARNING_BODY",
                comment: "Body for action sheet when attempting to change the group attributes access to 'administrator'",
            ),
        )
        actionSheet.addAction(.init(
            title: OWSLocalizedString(
                "MEMBER_LABEL_CLEAR_CHANGE_PERMISSION_CONTINUE",
                comment: "Button label on action sheet when attempting to change the group attributes access to 'administrator' and then continue",
            ),
            style: .default,
            handler: { _ in
                continueHandler()
            },
        ))
        actionSheet.addAction(.cancel)

        if BuildFlags.MemberLabel.send {
            self.presentActionSheet(actionSheet)
        } else {
            continueHandler()
        }
    }

    private func tryToSetAccessAttributes(_ value: GroupV2Access) {
        guard groupViewHelper.canEditPermissions else {
            showAdminOnlyWarningAlert()
            return
        }

        self.newAccessAttributes = value
        self.updateTableContents()
        self.updateNavigation()
    }

    private func tryToSetIsAnnouncementsOnly(_ value: Bool) {
        guard groupViewHelper.canEditPermissions else {
            showAdminOnlyWarningAlert()
            return
        }
        newIsAnnouncementsOnly = value
        updateTableContents()
        updateNavigation()
    }

    private func showAdminOnlyWarningAlert() {
        let message = OWSLocalizedString(
            "GROUP_ADMIN_ONLY_WARNING",
            comment: "Message indicating that a feature can only be used by group admins.",
        )
        presentToast(text: message)
        updateTableContents()
    }

    private func reloadThreadAndUpdateContent() {
        let didUpdate = SSKEnvironment.shared.databaseStorageRef.read { transaction -> Bool in
            guard
                let newThread = TSThread.fetchViaCache(
                    uniqueId: self.thread.uniqueId,
                    transaction: transaction,
                )
            else {
                return false
            }
            let newThreadViewModel = ThreadViewModel(
                thread: newThread,
                forChatList: false,
                transaction: transaction,
            )
            self.threadViewModel = newThreadViewModel
            self.groupViewHelper = GroupViewHelper(threadViewModel: newThreadViewModel, memberLabelCoordinator: nil)
            self.groupViewHelper.delegate = self
            return true
        }
        if !didUpdate {
            owsFailDebug("Invalid thread.")
            navigationController?.popViewController(animated: true)
            return
        }

        updateTableContents()
    }

    private func didTapSet() {
        guard groupViewHelper.canEditPermissions else {
            owsFailDebug("Missing edit permission.")
            return
        }

        // TODO: We might consolidate this from (up to) 3 separate group changes
        // into a single change.
        GroupViewUtils.updateGroupWithActivityIndicator(
            fromViewController: self,
            updateBlock: { @MainActor [thread] in
                await withCheckedContinuation { continuation in
                    DispatchQueue.global().async {
                        // We're sending a message, so we're accepting any pending message request.
                        ThreadUtil.addThreadToProfileWhitelistIfEmptyOrPendingRequestAndSetDefaultTimerWithSneakyTransaction(thread)
                        continuation.resume()
                    }
                }

                if self.newAccessMembers != self.oldAccessMembers {
                    try await GroupManager.changeGroupMembershipAccessV2(
                        groupModel: self.groupModelV2,
                        access: self.newAccessMembers,
                    )
                }
                if self.newAccessAttributes != self.oldAccessAttributes {
                    try await GroupManager.changeGroupAttributesAccessV2(
                        groupModel: self.groupModelV2,
                        access: self.newAccessAttributes,
                    )
                }
                if self.newIsAnnouncementsOnly != self.oldIsAnnouncementsOnly {
                    try await GroupManager.setIsAnnouncementsOnly(
                        groupModel: self.groupModelV2,
                        isAnnouncementsOnly: self.newIsAnnouncementsOnly,
                    )
                }

                if self.newAccessMemberLabels != self.oldAccessMemberLabels {
                    try await GroupManager.changeGroupMemberLabelsAccessV2(
                        groupModel: self.groupModelV2,
                        access: self.newAccessMemberLabels,
                    )
                }
            },
            completion: { [weak self] in
                self?.permissionsDelegate?.groupPermissionSettingsDidUpdate()
                self?.dismiss(animated: true)
            },
        )
    }
}

extension GroupPermissionsSettingsViewController: GroupViewHelperDelegate {
    func groupViewHelperDidUpdateGroup() {
        reloadThreadAndUpdateContent()
    }

    var currentGroupModel: TSGroupModel? {
        thread.groupModelIfGroupThread
    }

    var fromViewController: UIViewController? {
        self
    }
}