// // Copyright 2020 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only // import Foundation @objc public extension TSThread { var isGroupThread: Bool { self is TSGroupThread } var isNonContactThread: Bool { !(self is TSContactThread) } var usesSenderKey: Bool { self is TSGroupThread || self is TSPrivateStoryThread } var isGroupV1Thread: Bool { guard let groupThread = self as? TSGroupThread else { return false } return groupThread.groupModel.groupsVersion == .V1 } var isGroupV2Thread: Bool { guard let groupThread = self as? TSGroupThread else { return false } return groupThread.groupModel.groupsVersion == .V2 } var groupModelIfGroupThread: TSGroupModel? { guard let groupThread = self as? TSGroupThread else { return nil } return groupThread.groupModel } var canSendReactionToThread: Bool { guard !isGroupV1Thread else { return false } return true } var canSendNonChatMessagesToThread: Bool { guard !isGroupV1Thread else { return false } return true } @available(swift, obsoleted: 1.0) func canSendChatMessagesToThread() -> Bool { canSendChatMessagesToThread(ignoreAnnouncementOnly: false) } func canSendChatMessagesToThread(ignoreAnnouncementOnly: Bool = false) -> Bool { guard !isGroupV1Thread else { return false } if !ignoreAnnouncementOnly { guard !isBlockedByAnnouncementOnly else { return false } } if let groupThread = self as? TSGroupThread { guard groupThread.isLocalUserFullMember else { return false } } return true } var isBlockedByAnnouncementOnly: Bool { guard let groupThread = self as? TSGroupThread else { return false } guard let groupModel = groupThread.groupModel as? TSGroupModelV2 else { return false } // In "announcement-only" groups, only admins can send messages and start group calls. return (groupModel.isAnnouncementsOnly && !groupModel.groupMembership.isLocalUserFullMemberAndAdministrator) } var isAnnouncementOnlyGroupThread: Bool { guard let groupThread = self as? TSGroupThread else { return false } guard let groupModel = groupThread.groupModel as? TSGroupModelV2 else { return false } return groupModel.isAnnouncementsOnly } func hasPendingMessageRequest(transaction: SDSAnyReadTransaction) -> Bool { return ThreadFinder().hasPendingMessageRequest(thread: self, transaction: transaction) } @nonobjc func isSystemContact(contactsManager: ContactManager, tx: SDSAnyReadTransaction) -> Bool { guard let contactThread = self as? TSContactThread else { return false } return contactsManager.fetchSignalAccount(for: contactThread.contactAddress, transaction: tx) != nil } // MARK: - Database Hooks internal func _anyDidInsert(tx: SDSAnyWriteTransaction) { let searchableNameIndexer = DependenciesBridge.shared.searchableNameIndexer searchableNameIndexer.insert(self, tx: tx.asV2Write) } } // MARK: - public extension TSThread { struct LastVisibleInteraction: Codable, Equatable { public let sortId: UInt64 public let onScreenPercentage: CGFloat public init(sortId: UInt64, onScreenPercentage: CGFloat) { self.sortId = sortId self.onScreenPercentage = onScreenPercentage } } private static let lastVisibleInteractionStore = SDSKeyValueStore(collection: "lastVisibleInteractionStore") @objc func hasLastVisibleInteraction(transaction: SDSAnyReadTransaction) -> Bool { nil != Self.lastVisibleInteraction(forThread: self, transaction: transaction) } @objc func lastVisibleSortId(transaction: SDSAnyReadTransaction) -> NSNumber? { guard let lastVisibleInteraction = lastVisibleInteraction(transaction: transaction) else { return nil } return NSNumber(value: lastVisibleInteraction.sortId) } func lastVisibleInteraction(transaction: SDSAnyReadTransaction) -> LastVisibleInteraction? { Self.lastVisibleInteraction(forThread: self, transaction: transaction) } static func lastVisibleInteraction(forThread thread: TSThread, transaction: SDSAnyReadTransaction) -> LastVisibleInteraction? { guard let data = lastVisibleInteractionStore.getData(thread.uniqueId, transaction: transaction) else { return nil } do { return try JSONDecoder().decode(LastVisibleInteraction.self, from: data) } catch { owsFailDebug("Error: \(error)") return nil } } @objc func clearLastVisibleInteraction(transaction: SDSAnyWriteTransaction) { Self.setLastVisibleInteraction(nil, forThread: self, transaction: transaction) } @objc func setLastVisibleInteraction(sortId: UInt64, onScreenPercentage: CGFloat, transaction: SDSAnyWriteTransaction) { let lastVisibleInteraction = LastVisibleInteraction(sortId: sortId, onScreenPercentage: onScreenPercentage) Self.setLastVisibleInteraction(lastVisibleInteraction, forThread: self, transaction: transaction) } func setLastVisibleInteraction(_ lastVisibleInteraction: LastVisibleInteraction?, transaction: SDSAnyWriteTransaction) { Self.setLastVisibleInteraction(lastVisibleInteraction, forThread: self, transaction: transaction) } static func setLastVisibleInteraction(_ lastVisibleInteraction: LastVisibleInteraction?, forThread thread: TSThread, transaction: SDSAnyWriteTransaction) { guard let lastVisibleInteraction = lastVisibleInteraction else { lastVisibleInteractionStore.removeValue(forKey: thread.uniqueId, transaction: transaction) return } let data: Data do { data = try JSONEncoder().encode(lastVisibleInteraction) } catch { owsFailDebug("Error: \(error)") lastVisibleInteractionStore.removeValue(forKey: thread.uniqueId, transaction: transaction) return } lastVisibleInteractionStore.setData(data, key: thread.uniqueId, transaction: transaction) } } // MARK: - Drafts extension TSThread { @objc public func currentDraft(transaction: SDSAnyReadTransaction) -> MessageBody? { currentDraft(shouldFetchLatest: true, transaction: transaction) } @objc public func currentDraft(shouldFetchLatest: Bool, transaction: SDSAnyReadTransaction) -> MessageBody? { if shouldFetchLatest { guard let thread = TSThread.anyFetch(uniqueId: uniqueId, transaction: transaction) else { return nil } return Self.draft(forThread: thread) } else { return Self.draft(forThread: self) } } private static func draft(forThread thread: TSThread) -> MessageBody? { guard let messageDraft = thread.messageDraft else { return nil } let ranges: MessageBodyRanges = thread.messageDraftBodyRanges ?? .empty return MessageBody(text: messageDraft, ranges: ranges) } @objc public func editTarget(transaction: SDSAnyReadTransaction) -> TSOutgoingMessage? { guard let editTargetTimestamp = editTargetTimestamp?.uint64Value, let localAddress = DependenciesBridge.shared.tsAccountManager.localIdentifiers(tx: transaction.asV2Read)?.aciAddress else { return nil } return InteractionFinder.findMessage( withTimestamp: editTargetTimestamp, threadId: uniqueId, author: localAddress, transaction: transaction ) as? TSOutgoingMessage } }