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

import Contacts
import ContactsUI
import LibSignalClient
import SafariServices
public import SignalServiceKit

@objc
public protocol ContactsViewHelperObserver: AnyObject {
    func contactsViewHelperDidUpdateContacts()
}

public class ContactsViewHelper {

    public init() {}

    public func performInitialSetup(appReadiness: AppReadiness) {
        appReadiness.runNowOrWhenUIDidBecomeReadySync {
            self.setup()
        }
    }

    deinit {
        notificationObservers.forEach { NotificationCenter.default.removeObserver($0) }
    }

    private func setup() {
        guard !CurrentAppContext().isNSE else { return }

        setupNotificationObservations()
    }

    // MARK: Notifications

    private var notificationObservers = [NSObjectProtocol]()

    private func setupNotificationObservations() {
        notificationObservers.append(contentsOf: [
            NotificationCenter.default.addObserver(
                forName: .OWSContactsManagerSignalAccountsDidChange,
                object: nil,
                queue: nil,
                using: { [weak self] _ in
                    self?.updateContacts()
                },
            ),
            NotificationCenter.default.addObserver(
                forName: UserProfileNotifications.profileWhitelistDidChange,
                object: nil,
                queue: nil,
                using: { [weak self] _ in
                    self?.updateContacts()
                },
            ),
            NotificationCenter.default.addObserver(
                forName: BlockingManager.blockListDidChange,
                object: nil,
                queue: nil,
                using: { [weak self] _ in
                    self?.updateContacts()
                },
            ),
            NotificationCenter.default.addObserver(
                forName: RecipientHidingManagerImpl.hideListDidChange,
                object: nil,
                queue: nil,
                using: { [weak self] _ in
                    // Hiding a recipient who is a system contact or is someone you've
                    // chatted with 1:1 updates the profile whitelist, which already
                    // triggers a call to `updateContacts`. However, recipients who
                    // do not fit into these categories need this other mechanism to
                    // trigger `updateContacts`.
                    self?.updateContacts()
                },
            ),
        ])
    }

    // MARK: Observation

    private let observers = NSHashTable<ContactsViewHelperObserver>.weakObjects()

    public func addObserver(_ observer: ContactsViewHelperObserver) {
        AssertIsOnMainThread()
        observers.add(observer)
    }

    private func updateContacts() {
        AssertIsOnMainThread()
        owsAssertDebug(!CurrentAppContext().isNSE)
        fireDidUpdateContacts()
    }

    private func fireDidUpdateContacts() {
        for delegate in observers.allObjects {
            delegate.contactsViewHelperDidUpdateContacts()
        }
    }
}

// MARK: Presenting Permission-Gated Views

public extension ContactsViewHelper {

    enum ReadPurpose {
        case share
        case invite
    }

    private enum Access {
        case edit
        case read(ReadPurpose)
    }

    func checkEditAuthorization(
        performWhenAllowed: () -> Void,
        presentErrorFrom viewController: UIViewController,
    ) {
        AssertIsOnMainThread()

        switch SSKEnvironment.shared.contactManagerImplRef.editingAuthorization {
        case .notAllowed:
            Self.presentContactAccessNotAllowedAlert(from: viewController)
        case .notAuthorized:
            Self.presentContactAccessDeniedAlert(from: viewController, access: .edit)
        case .authorized:
            performWhenAllowed()
        }
    }

    func checkReadAuthorization(
        purpose: ReadPurpose,
        performWhenAllowed: @escaping () -> Void,
        presentErrorFrom viewController: UIViewController,
    ) {
        let deniedBlock = {
            Self.presentContactAccessDeniedAlert(from: viewController, access: .read(purpose))
        }

        switch SSKEnvironment.shared.contactManagerImplRef.sharingAuthorization {
        case .notDetermined:
            CNContactStore().requestAccess(for: .contacts) { granted, error in
                DispatchQueue.main.async {
                    if granted {
                        performWhenAllowed()
                    } else {
                        deniedBlock()
                    }
                }
            }
        case .authorized:
            performWhenAllowed()
        case .denied:
            deniedBlock()
        }
    }

    private static func presentContactAccessDeniedAlert(from viewController: UIViewController, access: Access) {
        owsAssertDebug(!CurrentAppContext().isNSE)

        let title: String
        let message: String

        switch access {
        case .edit:
            title = OWSLocalizedString(
                "EDIT_CONTACT_WITHOUT_CONTACTS_PERMISSION_ALERT_TITLE",
                comment: "Alert title for when the user has just tried to edit a contacts after declining to give Signal contacts permissions",
            )
            message = OWSLocalizedString(
                "EDIT_CONTACT_WITHOUT_CONTACTS_PERMISSION_ALERT_BODY",
                comment: "Alert body for when the user has just tried to edit a contacts after declining to give Signal contacts permissions",
            )

        case .read(let readPurpose):
            switch readPurpose {
            case .share:
                title = OWSLocalizedString(
                    "CONTACT_SHARING_NO_ACCESS_TITLE",
                    comment: "Alert title when contacts disabled while trying to share a contact.",
                )
                message = OWSLocalizedString(
                    "CONTACT_SHARING_NO_ACCESS_BODY",
                    comment: "Alert body when contacts disabled while trying to share a contact.",
                )

            case .invite:
                title = OWSLocalizedString(
                    "INVITE_FLOW_REQUIRES_CONTACT_ACCESS_TITLE",
                    comment: "Alert title when contacts disabled while trying to invite contacts to signal",
                )
                message = OWSLocalizedString(
                    "INVITE_FLOW_REQUIRES_CONTACT_ACCESS_BODY",
                    comment: "Alert body when contacts disabled while trying to invite contacts to signal",
                )
            }
        }

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

        actionSheet.addAction(ActionSheetAction(
            title: OWSLocalizedString(
                "AB_PERMISSION_MISSING_ACTION_NOT_NOW",
                comment: "Button text to dismiss missing contacts permission alert",
            ),
            style: .cancel,
        ))

        if let openSystemSettingsAction = AppContextUtils.openSystemSettingsAction(completion: nil) {
            actionSheet.addAction(openSystemSettingsAction)
        }

        viewController.presentActionSheet(actionSheet)
    }

    private static func presentContactAccessNotAllowedAlert(from viewController: UIViewController) {
        let actionSheet = ActionSheetController(
            message: OWSLocalizedString(
                "LINKED_DEVICE_CONTACTS_NOT_ALLOWED",
                comment: "Shown in an alert when trying to edit a contact.",
            ),
        )

        actionSheet.addAction(ActionSheetAction(title: CommonStrings.learnMore) { [weak viewController] _ in
            guard let viewController else { return }
            presentContactAccessNotAllowedLearnMore(from: viewController)
        })

        actionSheet.addAction(ActionSheetAction(title: CommonStrings.okButton, style: .cancel))

        viewController.presentActionSheet(actionSheet)
    }

    static func presentContactAccessNotAllowedLearnMore(from viewController: UIViewController) {
        viewController.present(
            SFSafariViewController(url: URL.Support.contactAccessNotAllowed),
            animated: true,
        )
    }
}