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
}