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

import Foundation
public import SignalServiceKit

public class ThreadViewModel: NSObject {
    public let hasUnreadMessages: Bool
    public let isGroupThread: Bool
    public let threadRecord: TSThread
    public let unreadCount: UInt
    public let contactAddress: SignalServiceAddress?
    public let name: String
    public let shortName: String?
    public let associatedData: ThreadAssociatedData
    public let hasPendingMessageRequest: Bool
    public let disappearingMessagesConfiguration: DisappearingMessagesConfigurationRecord
    public let isBlocked: Bool
    public let isPinned: Bool

    public var isArchived: Bool { associatedData.isArchived }
    public var isMuted: Bool { associatedData.isMuted }
    public var mutedUntilTimestamp: UInt64 { associatedData.mutedUntilTimestamp }
    public var mutedUntilDate: Date? { associatedData.mutedUntilDate }
    public var isMarkedUnread: Bool { associatedData.isMarkedUnread }

    public let pinnedMessages: [TSMessage]

    public var threadUniqueId: String {
        return threadRecord.uniqueId
    }

    public var isContactThread: Bool {
        return !isGroupThread
    }

    public var isLocalUserFullMemberOfThread: Bool {
        threadRecord.isLocalUserFullMemberOfThread
    }

    public let lastMessageForInbox: TSInteraction?

    // This property is only populated if forChatList is true.
    public let chatListInfo: ChatListInfo?

    /// Instantiate a view model for the thread with the given uniqueId.
    /// - Important
    /// Crashes if the corresponding thread does not exist.
    public convenience init(threadUniqueId: String, forChatList: Bool, transaction tx: DBReadTransaction) {
        guard
            let thread = DependenciesBridge.shared.threadStore.fetchThread(
                uniqueId: threadUniqueId,
                tx: tx,
            )
        else {
            owsFail("Unexpectedly missing thread for unique ID!")
        }

        self.init(thread: thread, forChatList: forChatList, transaction: tx)
    }

    public init(thread: TSThread, forChatList: Bool, transaction: DBReadTransaction) {
        self.threadRecord = thread

        let dmConfigurationStore = DependenciesBridge.shared.disappearingMessagesConfigurationStore
        self.disappearingMessagesConfiguration = dmConfigurationStore.fetchOrBuildDefault(for: .thread(thread), tx: transaction)

        self.isGroupThread = thread.isGroupThread

        let threadDisplayName = SSKEnvironment.shared.contactManagerRef.displayName(for: thread, tx: transaction)
        self.name = threadDisplayName?.resolvedValue() ?? ""

        if case .contactThread(let displayName) = threadDisplayName {
            self.shortName = displayName.resolvedValue(useShortNameIfAvailable: true)
        } else {
            self.shortName = nil
        }

        let associatedData = ThreadAssociatedData.fetchOrDefault(for: thread, transaction: transaction)
        self.associatedData = associatedData

        if let contactThread = thread as? TSContactThread {
            self.contactAddress = contactThread.contactAddress
        } else {
            self.contactAddress = nil
        }

        let unreadCount = InteractionFinder(threadUniqueId: thread.uniqueId).unreadCount(transaction: transaction)
        self.unreadCount = unreadCount
        self.hasUnreadMessages = associatedData.isMarkedUnread || unreadCount > 0
        self.hasPendingMessageRequest = thread.hasPendingMessageRequest(transaction: transaction)

        self.lastMessageForInbox = thread.lastInteractionForInbox(forChatListSorting: false, transaction: transaction)

        if forChatList {
            chatListInfo = ChatListInfo(
                thread: thread,
                lastMessageForInbox: lastMessageForInbox,
                hasPendingMessageRequest: hasPendingMessageRequest,
                transaction: transaction,
            )
        } else {
            chatListInfo = nil
        }

        isBlocked = SSKEnvironment.shared.blockingManagerRef.isThreadBlocked(thread, transaction: transaction)
        isPinned = DependenciesBridge.shared.pinnedThreadStore.isThreadPinned(thread, tx: transaction)

        if let threadId = thread.grdbId?.int64Value {
            pinnedMessages = DependenciesBridge.shared.pinnedMessageManager.fetchPinnedMessagesForThread(threadId: threadId, tx: transaction)
        } else {
            owsAssertDebug(thread.uniqueId == "MockThread" || thread.uniqueId == "MockGroupThread", "missing thread Id")
            pinnedMessages = []
        }
    }

    override public func isEqual(_ object: Any?) -> Bool {
        guard let otherThread = object as? ThreadViewModel else {
            return super.isEqual(object)
        }

        return threadRecord.isEqual(otherThread.threadRecord)
    }
}

// MARK: -

public class ChatListInfo {

    public let lastMessageDate: Date?
    public let lastMessageOutgoingStatus: MessageReceiptStatus?
    public let snippet: CLVSnippet

    public init(
        thread: TSThread,
        lastMessageForInbox: TSInteraction?,
        hasPendingMessageRequest: Bool,
        transaction: DBReadTransaction,
    ) {

        self.lastMessageDate = lastMessageForInbox?.timestampDate
        self.lastMessageOutgoingStatus = { () -> MessageReceiptStatus? in
            guard let outgoingMessage = lastMessageForInbox as? TSOutgoingMessage else {
                return nil
            }
            if
                let paymentMessage = outgoingMessage as? OWSPaymentMessage,
                let receiptData = paymentMessage.paymentNotification?.mcReceiptData,
                let paymentModel = PaymentFinder.paymentModels(
                    forMcReceiptData: receiptData,
                    transaction: transaction,
                ).first
            {
                return MessageRecipientStatusUtils.recipientStatus(
                    outgoingMessage: outgoingMessage,
                    paymentModel: paymentModel,
                )
            } else {
                return MessageRecipientStatusUtils.recipientStatus(
                    outgoingMessage: outgoingMessage,
                    transaction: transaction,
                )
            }
        }()

        self.snippet = Self.buildCLVSnippet(
            thread: thread,
            hasPendingMessageRequest: hasPendingMessageRequest,
            lastMessageForInbox: lastMessageForInbox,
            transaction: transaction,
        )
    }

    private static func buildCLVSnippet(
        thread: TSThread,
        hasPendingMessageRequest: Bool,
        lastMessageForInbox: TSInteraction?,
        transaction: DBReadTransaction,
    ) -> CLVSnippet {

        let isBlocked = SSKEnvironment.shared.blockingManagerRef.isThreadBlocked(thread, transaction: transaction)

        func loadDraftText() -> HydratedMessageBody? {
            guard
                let draftMessageBody = thread.currentDraft(
                    shouldFetchLatest: false,
                    transaction: transaction,
                )
            else {
                return nil
            }
            return draftMessageBody
                .hydrating(
                    mentionHydrator: ContactsMentionHydrator.mentionHydrator(transaction: transaction),
                )
        }
        func hasVoiceMemoDraft() -> Bool {
            VoiceMessageInterruptedDraftStore.hasDraft(for: thread, transaction: transaction)
        }
        func loadLastMessageText() -> HydratedMessageBody? {
            if let previewable = lastMessageForInbox as? OWSPreviewText {
                return HydratedMessageBody.fromPlaintextWithoutRanges(
                    previewable.previewText(transaction: transaction).filterStringForDisplay(),
                )
            } else if let tsMessage = lastMessageForInbox as? TSMessage {
                return tsMessage.conversationListPreviewText(transaction)
            } else {
                return nil
            }
        }
        func loadLastMessageSenderName() -> String? {
            guard let groupThread = thread as? TSGroupThread else {
                return nil
            }
            if let incomingMessage = lastMessageForInbox as? TSIncomingMessage {
                return SSKEnvironment.shared.contactManagerImplRef.shortestDisplayName(
                    forGroupMember: incomingMessage.authorAddress,
                    inGroup: groupThread.groupModel,
                    transaction: transaction,
                )
            } else if lastMessageForInbox is TSOutgoingMessage {
                return CommonStrings.you
            } else {
                return nil
            }
        }
        func loadAddedToGroupByName() -> String? {
            guard
                let groupThread = thread as? TSGroupThread,
                let addedByAddress = groupThread.groupModel.addedByAddress
            else {
                return nil
            }
            return SSKEnvironment.shared.contactManagerRef.displayName(for: addedByAddress, tx: transaction).resolvedValue(useShortNameIfAvailable: true)
        }

        let draftIsLatest = thread.lastDraftInteractionRowId >= lastMessageForInbox?.sqliteRowId ?? 0

        if isBlocked {
            return .blocked
        } else if hasPendingMessageRequest {
            return .pendingMessageRequest(addedToGroupByName: loadAddedToGroupByName())
        } else if let draftText = loadDraftText()?.nilIfEmpty, draftIsLatest {
            return .draft(draftText: draftText)
        } else if hasVoiceMemoDraft() {
            return .voiceMemoDraft
        } else if let lastMessageText = loadLastMessageText()?.nilIfEmpty {
            if let senderName = loadLastMessageSenderName()?.nilIfEmpty {
                return .groupSnippet(lastMessageText: lastMessageText, senderName: senderName)
            } else {
                return .contactSnippet(lastMessageText: lastMessageText)
            }
        } else {
            return .none
        }
    }
}

// MARK: -

public enum CLVSnippet {
    case blocked
    case pendingMessageRequest(addedToGroupByName: String?)
    case draft(draftText: HydratedMessageBody)
    case voiceMemoDraft
    case contactSnippet(lastMessageText: HydratedMessageBody)
    case groupSnippet(lastMessageText: HydratedMessageBody, senderName: String)
    case none
}