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

import SignalServiceKit
import SignalUI

class PhoneNumberPrivacySettingsViewController: OWSTableViewController2 {

    private var phoneNumberDiscoverability: PhoneNumberDiscoverability!
    private var phoneNumberSharingMode: PhoneNumberSharingMode!

    // MARK: View Lifecycle

    override func viewDidLoad() {
        super.viewDidLoad()
        loadValues()
        title = OWSLocalizedString(
            "SETTINGS_PHONE_NUMBER_PRIVACY_TITLE",
            comment: "The title for phone number privacy settings.",
        )
        updateTableContents()
    }

    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        updateTableContents()
    }

    override func themeDidChange() {
        super.themeDidChange()
        updateTableContents()
    }

    private func loadValues() {
        SSKEnvironment.shared.databaseStorageRef.read { tx in
            let tsAccountManager = DependenciesBridge.shared.tsAccountManager
            phoneNumberDiscoverability = tsAccountManager.phoneNumberDiscoverability(tx: tx).orDefault
            phoneNumberSharingMode = SSKEnvironment.shared.udManagerRef.phoneNumberSharingMode(tx: tx).orDefault
        }
    }

    // MARK: Build Table

    private func updateTableContents() {
        let contents = OWSTableContents()
        var sections = [OWSTableSection]()

        let seeMyNumberSection = OWSTableSection()
        seeMyNumberSection.headerTitle = OWSLocalizedString(
            "SETTINGS_PHONE_NUMBER_SHARING_TITLE",
            comment: "The title for the phone number sharing setting section.",
        )
        seeMyNumberSection.customFooterView = createSharingFooter()
        seeMyNumberSection.add(sharingItem(.everybody))
        seeMyNumberSection.add(sharingItem(.nobody))
        sections.append(seeMyNumberSection)

        let findByNumberSection = OWSTableSection()
        findByNumberSection.headerTitle = OWSLocalizedString(
            "SETTINGS_PHONE_NUMBER_DISCOVERABILITY_TITLE",
            comment: "The title for the phone number discoverability setting section.",
        )
        findByNumberSection.footerTitle = phoneNumberDiscoverability.descriptionForDiscoverability
        findByNumberSection.add(everybodyDiscoverabilityItem())
        findByNumberSection.add(nobodyDiscoverabilityItem(sharingMode: phoneNumberSharingMode))

        sections.append(findByNumberSection)

        contents.add(sections: sections)
        self.contents = contents
    }

    // MARK: Update Setting Values

    private func everybodyDiscoverabilityItem() -> OWSTableItem {
        return OWSTableItem(
            text: PhoneNumberDiscoverability.everybody.nameForDiscoverability,
            actionBlock: { [weak self] in
                self?.updateDiscoverability(.everybody)
            },
            accessoryType: self.phoneNumberDiscoverability == .everybody ? .checkmark : .none,
        )
    }

    private func nobodyDiscoverabilityItem(sharingMode: PhoneNumberSharingMode) -> OWSTableItem {
        let textColor: UIColor = {
            switch sharingMode {
            case .everybody:
                return Theme.secondaryTextAndIconColor
            case .nobody:
                return Theme.primaryTextColor
            }
        }()

        let accessory: UITableViewCell.AccessoryType = {
            switch (sharingMode, self.phoneNumberDiscoverability) {
            case (.everybody, _), (.nobody, .everybody), (.nobody, .none):
                return .none
            case (.nobody, .nobody):
                return .checkmark
            }
        }()

        return OWSTableItem(
            text: PhoneNumberDiscoverability.nobody.nameForDiscoverability,
            textColor: textColor,
            actionBlock: { [weak self] in
                switch sharingMode {
                case .everybody:
                    self?.presentPhoneNumberDiscoverabilityDisabledToast()
                case .nobody:
                    self?.promptToDisableDiscoverability()
                }
            },
            accessoryType: accessory,
        )
    }

    private func sharingItem(_ mode: PhoneNumberSharingMode) -> OWSTableItem {
        return OWSTableItem(
            text: Self.nameForMode(mode),
            actionBlock: { [weak self] in
                self?.updatePhoneNumberSharing(mode)
            },
            accessoryType: phoneNumberSharingMode == mode ? .checkmark : .none,
        )
    }

    // MARK: Alerts

    private func presentPhoneNumberDiscoverabilityDisabledToast() {
        presentToast(text: OWSLocalizedString(
            "SETTINGS_PHONE_NUMBER_DISCOVERABILITY_DISABLED_TOAST",
            comment: "A toast that displays when the user attempts to set discoverability to 'nobody' when their phone number sharing is set to 'everybody', which is not allowed.",
        ))
    }

    private func promptToDisableDiscoverability() {
        OWSActionSheets.showConfirmationAlert(
            title: OWSLocalizedString(
                "SETTINGS_PHONE_NUMBER_DISCOVERABILITY_NOBODY_CONFIRMATION_TITLE",
                comment: "The title for a sheet asking for confirmation that the user wants to set their phone number discoverability to 'nobody'.",
            ),
            message: OWSLocalizedString(
                "SETTINGS_PHONE_NUMBER_DISCOVERABILITY_NOBODY_CONFIRMATION_DESCRIPTION",
                comment: "The description for a sheet asking for confirmation that the user wants to set their phone number discoverability to 'nobody'.",
            ),
        ) { [weak self] _ in
            self?.updateDiscoverability(.nobody)
        }
    }

    // MARK: Update Table UI

    private func updateDiscoverability(_ phoneNumberDiscoverability: PhoneNumberDiscoverability) {
        guard self.phoneNumberDiscoverability != phoneNumberDiscoverability else { return }

        SSKEnvironment.shared.databaseStorageRef.asyncWrite(block: { transaction in
            DependenciesBridge.shared.phoneNumberDiscoverabilityManager.setPhoneNumberDiscoverability(
                phoneNumberDiscoverability,
                updateAccountAttributes: true,
                updateStorageService: true,
                authedAccount: .implicit(),
                tx: transaction,
            )
        }) { [weak self] in
            guard let self else { return }
            self.loadValues()
            self.updateTableContents()
        }
    }

    private func updatePhoneNumberSharing(_ mode: PhoneNumberSharingMode) {
        guard phoneNumberSharingMode != mode else { return }

        SSKEnvironment.shared.databaseStorageRef.asyncWrite(block: { transaction in
            SSKEnvironment.shared.udManagerRef.setPhoneNumberSharingMode(mode, updateStorageServiceAndProfile: true, tx: transaction)

            // If sharing is set to `everybody`, discovery needs to be
            // updated to match this.
            if mode == .everybody {
                DependenciesBridge.shared.phoneNumberDiscoverabilityManager.setPhoneNumberDiscoverability(
                    .everybody,
                    updateAccountAttributes: true,
                    updateStorageService: true,
                    authedAccount: .implicit(),
                    tx: transaction,
                )
            }
        }) { [weak self] in
            guard let self else { return }
            self.loadValues()
            self.updateTableContents()
        }
    }

    // MARK: Phone number sharing footer

    /// Creates a footer view for the phone number sharing section.
    ///
    /// The height of this view will remain constant when
    /// `phoneNumberSharingMode` and `phoneNumberDiscoverability` change as its
    /// height is calculated based on the text of each possible description.
    private func createSharingFooter() -> UIView {
        let sharingDescriptionEverybody = sharingDescriptionEverybody()
        let sharingDescriptionNobodyDiscoverabilityNobody = sharingDescriptionNobodyDiscoverabilityNobody()
        let sharingDescriptionNobodyDiscoverabilityEverybody = sharingDescriptionNobodyDiscoverabilityEverybody()

        // Determine which footer text to show.
        switch (phoneNumberSharingMode!, phoneNumberDiscoverability!) {
        case (.everybody, _):
            sharingDescriptionEverybody.isHidden = false
            sharingDescriptionNobodyDiscoverabilityNobody.isHidden = true
            sharingDescriptionNobodyDiscoverabilityEverybody.isHidden = true
        case (.nobody, .everybody):
            sharingDescriptionEverybody.isHidden = true
            sharingDescriptionNobodyDiscoverabilityNobody.isHidden = true
            sharingDescriptionNobodyDiscoverabilityEverybody.isHidden = false
        case (.nobody, .nobody):
            sharingDescriptionEverybody.isHidden = true
            sharingDescriptionNobodyDiscoverabilityNobody.isHidden = false
            sharingDescriptionNobodyDiscoverabilityEverybody.isHidden = true
        }

        // Add all of the possible footer descriptions so that the height
        // doesn't change when the selected setting changes.
        let container = UIView.container()
        [
            sharingDescriptionEverybody,
            sharingDescriptionNobodyDiscoverabilityNobody,
            sharingDescriptionNobodyDiscoverabilityEverybody,
        ].forEach { textView in
            container.addSubview(textView)
            textView.autoPinEdges(toSuperviewEdgesExcludingEdge: .bottom)
            textView.autoPinEdge(toSuperviewEdge: .bottom, relation: .greaterThanOrEqual)
        }
        return container
    }

    private func sharingDescriptionEverybody() -> UITextView {
        let textView = buildFooterTextView(withDeepInsets: true)
        textView.text = OWSLocalizedString(
            "PHONE_NUMBER_SHARING_EVERYBODY_DESCRIPTION",
            comment: "A user friendly description of the 'everybody' phone number sharing mode.",
        )
        return textView
    }

    private func sharingDescriptionNobodyDiscoverabilityNobody() -> UITextView {
        let textView = buildFooterTextView(withDeepInsets: true)
        textView.text = OWSLocalizedString(
            "PHONE_NUMBER_SHARING_NOBODY_DESCRIPTION_DISCOVERABILITY_NOBODY",
            comment: "A user-friendly description of the 'nobody' phone number sharing mode when phone number discovery is set to 'nobody'.",
        )
        return textView
    }

    private func sharingDescriptionNobodyDiscoverabilityEverybody() -> UITextView {
        let textView = buildFooterTextView(withDeepInsets: true)
        textView.text = OWSLocalizedString(
            "PHONE_NUMBER_SHARING_NOBODY_DESCRIPTION_DISCOVERABILITY_EVERYBODY",
            comment: "A user-friendly description of the 'nobody' phone number sharing mode when phone number discovery is set to 'everybody'.",
        )
        return textView
    }
}

// MARK: - PhoneNumberDiscoverability + strings

extension PhoneNumberDiscoverability {
    var nameForDiscoverability: String {
        switch self {
        case .everybody:
            return OWSLocalizedString(
                "PHONE_NUMBER_DISCOVERABILITY_EVERYBODY",
                comment: "A user friendly name for the 'everybody' phone number discoverability mode.",
            )
        case .nobody:
            return OWSLocalizedString(
                "PHONE_NUMBER_DISCOVERABILITY_NOBODY",
                comment: "A user friendly name for the 'nobody' phone number discoverability mode.",
            )
        }
    }

    var descriptionForDiscoverability: String {
        switch self {
        case .everybody:
            return OWSLocalizedString(
                "PHONE_NUMBER_DISCOVERABILITY_EVERYBODY_DESCRIPTION",
                comment: "A user friendly description of the 'everybody' phone number discoverability mode.",
            )
        case .nobody:
            return OWSLocalizedString(
                "PHONE_NUMBER_DISCOVERABILITY_NOBODY_DESCRIPTION",
                comment: "A user friendly description of the 'nobody' phone number discoverability mode.",
            )
        }
    }
}

// MARK: - Phone number sharing strings

extension PhoneNumberPrivacySettingsViewController {
    private class func nameForMode(_ mode: PhoneNumberSharingMode) -> String {
        switch mode {
        case .everybody:
            return OWSLocalizedString(
                "PHONE_NUMBER_SHARING_EVERYBODY",
                comment: "A user friendly name for the 'everybody' phone number sharing mode.",
            )
        case .nobody:
            return OWSLocalizedString(
                "PHONE_NUMBER_SHARING_NOBODY",
                comment: "A user friendly name for the 'nobody' phone number sharing mode.",
            )
        }
    }
}