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

import SignalServiceKit
import SignalUI

public class CLVReminderViews {

    let reminderViewCell = UITableViewCell()

    private let reminderStackView = UIStackView()
    fileprivate let expiredView = ExpirationNagView(
        dateProvider: Date.provider,
        appExpiry: DependenciesBridge.shared.appExpiry,
        osExpiry: OsExpiry.default,
        device: UIDevice.current,
    )
    fileprivate var deregisteredView = UIView()
    fileprivate var outageView = UIView()
    fileprivate var archiveReminderView = UIView()
    fileprivate let paymentsReminderView = UIView()
    fileprivate var usernameCorruptedReminderView = UIView()
    fileprivate var usernameLinkCorruptedReminderView = UIView()

    public weak var chatListViewController: ChatListViewController?

    init() {
        AssertIsOnMainThread()

        reminderStackView.axis = .vertical
        reminderStackView.spacing = 0
        reminderViewCell.selectionStyle = .none
        reminderViewCell.contentView.addSubview(reminderStackView)
        reminderViewCell.backgroundColor = .clear
        reminderStackView.autoPinEdgesToSuperviewEdges()

        let deregisteredText: String
        let deregisteredActionTitle: String
        if DependenciesBridge.shared.tsAccountManager.registrationStateWithMaybeSneakyTransaction.isPrimaryDevice ?? true {
            deregisteredText = OWSLocalizedString(
                "DEREGISTRATION_WARNING",
                comment: "Label warning the user that they have been de-registered.",
            )
            deregisteredActionTitle = OWSLocalizedString(
                "DEREGISTRATION_WARNING_ACTION_TITLE",
                comment: "If the user has been deregistered, they'll see a warning. This is This is the call to action on that warning.",
            )
        } else {
            deregisteredText = OWSLocalizedString(
                "UNLINKED_WARNING",
                comment: "Label warning the user that they have been unlinked from their primary device.",
            )
            deregisteredActionTitle = OWSLocalizedString(
                "UNLINKED_WARNING_ACTION_TITLE",
                comment: "If this device has become unlinked from their primary device, they'll see a warning. This is the call to action on that warning.",
            )
        }
        deregisteredView = ReminderView(
            style: .warning,
            text: deregisteredText,
            actionTitle: deregisteredActionTitle,
            tapAction: { [weak self] in self?.didTapDeregisteredView() },
        )
        reminderStackView.addArrangedSubview(deregisteredView)
        deregisteredView.accessibilityIdentifier = "deregisteredView"

        reminderStackView.addArrangedSubview(expiredView)
        expiredView.accessibilityIdentifier = "expiredView"

        outageView = ReminderView(
            style: .warning,
            text: OWSLocalizedString(
                "OUTAGE_WARNING",
                comment: "Label warning the user that the Signal service may be down.",
            ),
        )
        reminderStackView.addArrangedSubview(outageView)
        outageView.accessibilityIdentifier = "outageView"

        archiveReminderView = ReminderView(
            style: .info,
            text: {
                let shouldKeepMutedChatsArchived = SSKEnvironment.shared.databaseStorageRef.read { transaction in
                    return SSKPreferences.shouldKeepMutedChatsArchived(transaction: transaction)
                }
                if shouldKeepMutedChatsArchived {
                    return OWSLocalizedString(
                        "INBOX_VIEW_ARCHIVE_MODE_MUTED_CHATS_REMINDER",
                        comment: "Label reminding the user that they are in archive mode, and that muted chats remain archived when they receive a new message.",
                    )
                } else {
                    return OWSLocalizedString(
                        "INBOX_VIEW_ARCHIVE_MODE_REMINDER",
                        comment: "Label reminding the user that they are in archive mode, and that chats are unarchived when they receive a new message.",
                    )
                }
            }(),
        )
        reminderStackView.addArrangedSubview(archiveReminderView)
        archiveReminderView.accessibilityIdentifier = "archiveReminderView"

        reminderStackView.addArrangedSubview(paymentsReminderView)
        paymentsReminderView.accessibilityIdentifier = "paymentsReminderView"

        usernameCorruptedReminderView = ReminderView(
            style: .info,
            text: OWSLocalizedString(
                "REMINDER_VIEW_USERNAME_CORRUPTED_WARNING",
                comment: "Label warning the user that something is wrong with their username.",
            ),
            actionTitle: OWSLocalizedString(
                "REMINDER_VIEW_USERNAME_CORRUPTED_FIX_BUTTON",
                comment: "Button below the warning to fix a corrupted username.",
            ),
            tapAction: { [weak self] in self?.didTapUsernameCorruptedReminderView() },
        )
        usernameLinkCorruptedReminderView = ReminderView(
            style: .info,
            text: OWSLocalizedString(
                "REMINDER_VIEW_USERNAME_LINK_CORRUPTED_WARNING",
                comment: "Label warning the user that something is wrong with their username link.",
            ),
            actionTitle: OWSLocalizedString(
                "REMINDER_VIEW_USERNAME_LINK_CORRUPTED_FIX_BUTTON",
                comment: "Button below the warning to fix a username link.",
            ),
            tapAction: { [weak self] in self?.didTapUsernameLinkCorruptedReminderView() },
        )
        reminderStackView.addArrangedSubviews([
            usernameCorruptedReminderView,
            usernameLinkCorruptedReminderView,
        ])
    }

    @objc
    private func didTapDeregisteredView() {
        AssertIsOnMainThread()

        guard let chatListViewController else {
            return
        }

        RegistrationUtils.showReregistrationUI(
            fromViewController: chatListViewController,
            appReadiness: chatListViewController.appReadiness,
        )
    }

    @objc
    private func didTapUsernameCorruptedReminderView() {
        guard let chatListViewController else {
            return
        }

        UsernameSelectionCoordinator(
            currentUsername: nil,
            isAttemptingRecovery: true,
            usernameSelectionDelegate: chatListViewController,
            context: .init(
                databaseStorage: SSKEnvironment.shared.databaseStorageRef,
                networkManager: SSKEnvironment.shared.networkManagerRef,
                storageServiceManager: SSKEnvironment.shared.storageServiceManagerRef,
                usernameEducationManager: DependenciesBridge.shared.usernameEducationManager,
                localUsernameManager: DependenciesBridge.shared.localUsernameManager,
            ),
        )
        .present(fromViewController: chatListViewController)
    }

    @objc
    private func didTapUsernameLinkCorruptedReminderView() {
        guard let chatListViewController else {
            return
        }

        chatListViewController.showAppSettings(mode: .corruptedUsernameLinkResolution)
    }

    public var hasVisibleReminders: Bool {

        !self.archiveReminderView.isHidden ||
            !self.deregisteredView.isHidden ||
            !self.outageView.isHidden ||
            !self.expiredView.isHidden ||
            !self.paymentsReminderView.isHidden ||
            !self.usernameCorruptedReminderView.isHidden ||
            !self.usernameLinkCorruptedReminderView.isHidden

    }
}

// MARK: -

extension ChatListViewController {

    private var reminderViews: CLVReminderViews { viewState.reminderViews }
    fileprivate var expiredView: ExpirationNagView { reminderViews.expiredView }
    fileprivate var deregisteredView: UIView { reminderViews.deregisteredView }
    fileprivate var outageView: UIView { reminderViews.outageView }
    fileprivate var archiveReminderView: UIView { reminderViews.archiveReminderView }
    fileprivate var paymentsReminderView: UIView { reminderViews.paymentsReminderView }
    fileprivate var usernameCorruptedReminderView: UIView { reminderViews.usernameCorruptedReminderView }
    fileprivate var usernameLinkCorruptedReminderView: UIView { reminderViews.usernameLinkCorruptedReminderView }

    public func updateArchiveReminderView() {
        archiveReminderView.isHidden = viewState.chatListMode != .archive
    }

    public func updateRegistrationReminderView() {
        let tsRegistrationState = DependenciesBridge.shared.tsAccountManager.registrationStateWithMaybeSneakyTransaction
        deregisteredView.isHidden = !tsRegistrationState.isDeregistered
    }

    public func updateOutageDetectionReminderView() {
        outageView.isHidden = !OutageDetection.shared.hasOutage
    }

    public func updateExpirationReminderView() {
        expiredView.update()
    }

    public func updatePaymentReminderView() {
        if unreadPaymentNotificationsCount == 1, let firstUnreadPaymentModel = self.firstUnreadPaymentModel {
            self.paymentsReminderView.isHidden = false

            SSKEnvironment.shared.databaseStorageRef.read { transaction in
                self.configureUnreadPaymentsBannerSingle(
                    paymentsReminderView,
                    paymentModel: firstUnreadPaymentModel,
                    transaction: transaction,
                )
            }
        } else if unreadPaymentNotificationsCount == 0 || firstUnreadPaymentModel == nil {
            self.paymentsReminderView.isHidden = true
        } else {
            self.paymentsReminderView.isHidden = false
            self.configureUnreadPaymentsBannerMultiple(paymentsReminderView, unreadCount: unreadPaymentNotificationsCount)
        }
    }

    public func updateUsernameReminderView() {
        SSKEnvironment.shared.databaseStorageRef.read { tx in
            updateUsernameStateViews(tx: tx)
        }
    }

    // MARK: -

    public func updateBackupFailureAlertsWithSneakyTransaction() {
        typealias BackupFailureAlertType = CLVViewState.BackupFailureAlertType

        let db = DependenciesBridge.shared.db
        let failureStateManager = DependenciesBridge.shared.backupFailureStateManager

        viewState.backupFailureAlerts = db.read { tx -> Set<BackupFailureAlertType> in
            guard failureStateManager.hasFailedBackup(tx: tx) else {
                return []
            }

            var alerts: Set<BackupFailureAlertType> = [.menuItem]
            for alertType in BackupFailureAlertType.allCases {
                if
                    let errorBadgeTarget = alertType.errorBadgeTarget,
                    failureStateManager.shouldShowErrorBadge(target: errorBadgeTarget, tx: tx)
                {
                    alerts.insert(alertType)
                }
            }
            return alerts
        }
    }

    public func updateBackupSubscriptionFailedToRedeemAlertsWithSneakyTx() {
        typealias BackupSubscriptionFailedToRedeemAlertType = CLVViewState.BackupSubscriptionFailedToRedeemAlertType

        let db = DependenciesBridge.shared.db
        let backupSubscriptionIssueStore = BackupSubscriptionIssueStore()

        viewState.backupSubscriptionFailedToRedeemAlerts = db.read { tx in
            var alerts = Set<BackupSubscriptionFailedToRedeemAlertType>()
            if backupSubscriptionIssueStore.shouldShowIAPSubscriptionFailedToRenewChatListBadge(tx: tx) {
                alerts.insert(.avatarBadge)
            }
            if backupSubscriptionIssueStore.shouldShowIAPSubscriptionFailedToRenewChatListMenuItem(tx: tx) {
                alerts.insert(.menuItem)
            }
            return alerts
        }
    }

    public func updateBackupIAPNotFoundLocallyAlertsWithSneakyTx() {
        typealias BackupIAPNotFoundLocallyAlertType = CLVViewState.BackupIAPNotFoundLocallyAlertType

        let db = DependenciesBridge.shared.db
        let backupSubscriptionIssueStore = BackupSubscriptionIssueStore()

        viewState.backupIAPNotFoundLocallyAlerts = db.read { tx in
            var alerts = Set<BackupIAPNotFoundLocallyAlertType>()
            if backupSubscriptionIssueStore.shouldShowIAPSubscriptionNotFoundLocallyChatListBadge(tx: tx) {
                alerts.insert(.avatarBadge)
            }
            if backupSubscriptionIssueStore.shouldShowIAPSubscriptionNotFoundLocallyChatListMenuItem(tx: tx) {
                alerts.insert(.menuItem)
            }
            return alerts
        }
    }

    public func updateHasConsumedMediaTierCapacityWithSneakyTransaction() {
        let backupSettingsStore = BackupSettingsStore()
        viewState.hasConsumedMediaTierCapacity = SSKEnvironment.shared.databaseStorageRef.read { tx in
            backupSettingsStore.hasConsumedMediaTierCapacity(tx: tx)
        }
    }

    // MARK: -

    private var unreadPaymentNotificationsCount: UInt {
        get { viewState.unreadPaymentNotificationsCount }
        set { viewState.unreadPaymentNotificationsCount = newValue }
    }

    private var firstUnreadPaymentModel: TSPaymentModel? {
        get { viewState.firstUnreadPaymentModel }
        set { viewState.firstUnreadPaymentModel = newValue }
    }

    public func updateUnreadPaymentNotificationsCountWithSneakyTransaction() {
        AssertIsOnMainThread()

        guard SSKEnvironment.shared.paymentsHelperRef.arePaymentsEnabled else {
            var needsUpdate = false

            if unreadPaymentNotificationsCount > 0 {
                unreadPaymentNotificationsCount = 0
                needsUpdate = true
            }

            if firstUnreadPaymentModel != nil {
                firstUnreadPaymentModel = nil
                needsUpdate = true
            }

            if needsUpdate {
                updatePaymentReminderView()
            }

            return
        }

        let (unreadPaymentNotificationsCount, firstUnreadPaymentModel) = SSKEnvironment.shared.databaseStorageRef.read { transaction in
            return (
                PaymentFinder.unreadCount(transaction: transaction),
                PaymentFinder.firstUnreadPaymentModel(transaction: transaction),
            )
        }

        self.unreadPaymentNotificationsCount = unreadPaymentNotificationsCount
        self.firstUnreadPaymentModel = firstUnreadPaymentModel

        updatePaymentReminderView()
    }

    // MARK: -

    /// Update reminder views as appropriate for the current username state.
    private func updateUsernameStateViews(tx: DBReadTransaction) {
        let currentUsernameState = DependenciesBridge.shared.localUsernameManager
            .usernameState(tx: tx)

        switch currentUsernameState {
        case .unset, .available:
            usernameCorruptedReminderView.isHidden = true
            usernameLinkCorruptedReminderView.isHidden = true
        case .linkCorrupted:
            usernameCorruptedReminderView.isHidden = true
            usernameLinkCorruptedReminderView.isHidden = false
        case .usernameAndLinkCorrupted:
            usernameCorruptedReminderView.isHidden = false
            usernameLinkCorruptedReminderView.isHidden = true
        }
    }
}

extension ChatListViewController: UsernameSelectionDelegate {
    func usernameSelectionDidDismissAfterConfirmation(username: String) {
        self.presentToast(
            text: String.nonPluralLocalizedStringWithFormat(
                OWSLocalizedString(
                    "USERNAME_RESET_SUCCESSFUL_TOAST",
                    comment: "A message in a toast informing the user their username, link, and QR code have successfully been reset. Embeds {{ the user's new username }}.",
                ),
                username,
            ),
            extraVInset: 8,
        )
    }
}