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+Helpers.swift
1 views
//
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//

import SignalServiceKit
import SignalUI

public extension ChatListViewController {

    var presentedChatListViewController: ChatListViewController? {
        AssertIsOnMainThread()

        guard
            let topViewController = navigationController?.topViewController as? ChatListViewController,
            topViewController != self
        else {
            return nil
        }
        return topViewController
    }

    func isConversationActive(threadUniqueId: String) -> Bool {
        AssertIsOnMainThread()

        guard let conversationSplitViewController = splitViewController as? ConversationSplitViewController else {
            owsFailDebug("Missing conversationSplitViewController.")
            return false
        }
        return conversationSplitViewController.selectedThread?.uniqueId == threadUniqueId
    }

    func updateLastViewedThreadUniqueId(_ threadUniqueId: String, animated: Bool) {
        lastViewedThreadUniqueId = threadUniqueId

        // First ensure that the given thread is selected in the current state
        // of the table. This will update the view state and set a flag
        // indicating whether a load is necessary.
        ensureSelectedThreadUniqueId(threadUniqueId, animated: animated)

        // Schedule a load if necessary.
        loadCoordinator.loadIfNecessary(suppressAnimations: !animated)

        // The chat list render state may now be changed, invalidating the
        // just-selected index path, so update the selection once more to
        // reflect the loaded data.
        ensureSelectedThreadUniqueId(threadUniqueId, animated: animated)

        tableView.scrollToNearestSelectedRow(at: .none, animated: animated)
    }

    /// Verifies that the currently selected cell matches the provided thread.
    /// If it does or if the user's in multi-select: Do nothing.
    /// If it doesn't: Select the first cell matching the provided thread, if one exists. Otherwise, deselect the current row.
    func ensureSelectedThreadUniqueId(_ targetThreadUniqueId: String, animated: Bool) {
        // Ignore any updates if we're in multiselect mode. I don't think this can happen,
        // but if it does let's avoid stepping over the user's manual selection.
        let selectedIndexPaths = tableView.indexPathsForSelectedRows ?? []

        guard viewState.multiSelectState.isActive == false, selectedIndexPaths.count < 2 else {
            return
        }

        let selectedIndexPath = selectedIndexPaths.first
        let selectedThreadUniqueId = selectedIndexPath.flatMap { renderState.threadUniqueId(forIndexPath: $0) }

        tableView.performBatchUpdates {
            viewState.lastSelectedThreadId = targetThreadUniqueId

            if let selectedIndexPath, selectedThreadUniqueId != targetThreadUniqueId {
                tableView.deselectRow(at: selectedIndexPath, animated: animated)
            }

            if let targetIndexPath = renderState.indexPath(forUniqueId: targetThreadUniqueId) {
                tableView.selectRow(at: targetIndexPath, animated: animated, scrollPosition: .none)
            }
        }
    }

    // MARK: Archive

    func archiveSelectedConversation() {
        AssertIsOnMainThread()

        Logger.info("")

        guard let selectedThread = conversationSplitViewController?.selectedThread else { return }

        let threadAssociatedData = SSKEnvironment.shared.databaseStorageRef.read { transaction in
            ThreadAssociatedData.fetchOrDefault(for: selectedThread, transaction: transaction)
        }

        guard !threadAssociatedData.isArchived else { return }

        conversationSplitViewController?.closeSelectedConversation(animated: true)

        SSKEnvironment.shared.databaseStorageRef.write { transaction in
            threadAssociatedData.updateWith(isArchived: true, updateStorageService: true, transaction: transaction)
        }

        updateViewState()
    }

    func unarchiveSelectedConversation() {
        AssertIsOnMainThread()

        Logger.info("")

        guard let selectedThread = conversationSplitViewController?.selectedThread else { return }

        let threadAssociatedData = SSKEnvironment.shared.databaseStorageRef.read { transaction in
            ThreadAssociatedData.fetchOrDefault(for: selectedThread, transaction: transaction)
        }

        guard threadAssociatedData.isArchived else { return }

        conversationSplitViewController?.closeSelectedConversation(animated: true)

        SSKEnvironment.shared.databaseStorageRef.write { transaction in
            threadAssociatedData.updateWith(isArchived: false, updateStorageService: true, transaction: transaction)
        }

        updateViewState()
    }

    func showArchivedConversations(offerMultiSelectMode: Bool = true) {
        AssertIsOnMainThread()

        owsAssertDebug(viewState.chatListMode == .inbox)

        // When showing archived conversations, we want to use a conventional "back" button
        // to return to the "inbox" conversation list.
        applyArchiveBackButton()

        // Push a separate instance of this view using "archive" mode.
        let chatList = ChatListViewController(chatListMode: .archive, appReadiness: appReadiness)
        chatList.hidesBottomBarWhenPushed = true

        if offerMultiSelectMode {
            chatList.navigationItem.rightBarButtonItem = UIBarButtonItem(
                title: CommonStrings.selectButton,
                style: .plain,
                target: chatList,
                action: #selector(chatList.switchMultiSelectState),
                accessibilityIdentifier: "select",
            )
        }
        show(chatList, sender: self)
    }

    func applyArchiveBackButton() {
        AssertIsOnMainThread()

        if #available(iOS 26, *) { return }

        navigationItem.backBarButtonItem = UIBarButtonItem(
            title: CommonStrings.backButton,
            style: .plain,
            target: nil,
            action: nil,
            accessibilityIdentifier: "back",
        )
    }
}

// MARK: Previews

extension ChatListViewController {
    func canPresentPreview(fromIndexPath indexPath: IndexPath) -> Bool {
        guard !tableView.isEditing else {
            return false
        }

        switch renderState.sections[indexPath.section].type {
        case .pinned, .unpinned:
            let currentSelectedThreadId = conversationSplitViewController?.selectedThread?.uniqueId
            // Currently, no previewing the currently selected thread.
            // Though, in a scene-aware, multiwindow world, we may opt to permit this.
            // If only to allow the user to pick up and drag a conversation to a new window.
            return renderState.threadUniqueId(forIndexPath: indexPath) != currentSelectedThreadId

        default:
            return false
        }
    }

    func createPreviewController(atIndexPath indexPath: IndexPath) -> UIViewController? {
        guard let threadViewModel = tableDataSource.threadViewModel(forIndexPath: indexPath) else {
            owsFailDebug("Missing threadViewModel.")
            return nil
        }
        let vc = SSKEnvironment.shared.databaseStorageRef.read { tx in
            ConversationViewController.load(
                appReadiness: appReadiness,
                threadViewModel: threadViewModel,
                action: .none,
                focusMessageId: nil,
                tx: tx,
            )
        }
        vc.previewSetup()
        return vc
    }

    func commitPreviewController(_ previewController: UIViewController) {
        guard let previewController = previewController as? ConversationViewController else {
            owsFailDebug("Invalid previewController: \(type(of: previewController))")
            return
        }
        presentThread(
            threadUniqueId: previewController.thread.uniqueId,
            animated: false,
        )
    }
}