TM-SGNL-iOS/SignalServiceKit/PinnedThreadManager/PinnedThreadManagerImpl.swift
TeleMessage developers dde0620daf initial commit
2025-05-03 12:28:28 -07:00

164 lines
6.4 KiB
Swift

//
// Copyright 2020 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//
import Foundation
public enum PinnedThreadError: Error {
case tooManyPinnedThreads
}
// MARK: -
public class PinnedThreadManagerImpl: PinnedThreadManager {
private let db: any DB
private let pinnedThreadStore: PinnedThreadStoreWrite
private let storageServiceManager: StorageServiceManager
private let threadStore: ThreadStore
public init(
db: any DB,
pinnedThreadStore: PinnedThreadStoreWrite,
storageServiceManager: StorageServiceManager,
threadStore: ThreadStore
) {
self.db = db
self.pinnedThreadStore = pinnedThreadStore
self.storageServiceManager = storageServiceManager
self.threadStore = threadStore
}
public func pinnedThreadIds(tx: DBReadTransaction) -> [String] {
return pinnedThreadStore.pinnedThreadIds(tx: tx)
}
public func pinnedThreads(tx: DBReadTransaction) -> [TSThread] {
return pinnedThreadIds(tx: tx).compactMap { threadId in
guard let thread = threadStore.fetchThread(uniqueId: threadId, tx: tx) else {
Logger.warn("pinned thread record no longer exists \(threadId)")
return nil
}
let associatedData = threadStore.fetchOrDefaultAssociatedData(for: thread, tx: tx)
// Ignore deleted or archived pinned threads. These should exist, but it's
// possible they are incorrectly received from linked devices.
guard canPin(thread, with: associatedData) else {
Logger.warn("Ignoring deleted or archived pinned thread \(threadId)")
return nil
}
return thread
}
}
public func isThreadPinned(_ thread: TSThread, tx: DBReadTransaction) -> Bool {
return pinnedThreadStore.isThreadPinned(thread, tx: tx)
}
public func updatePinnedThreadIds(
_ pinnedThreadIds: [String],
updateStorageService: Bool,
tx: DBWriteTransaction
) {
let previousPinnedThreadIds = pinnedThreadStore.pinnedThreadIds(tx: tx)
pinnedThreadStore.updatePinnedThreadIds(pinnedThreadIds, tx: tx)
// Read again to get the final new value.
let pinnedThreadIds = pinnedThreadStore.pinnedThreadIds(tx: tx)
if previousPinnedThreadIds != pinnedThreadIds {
let changedThreadIds = Set(previousPinnedThreadIds).symmetricDifference(pinnedThreadIds)
// Touch any threads whose pin state changed, so we update the UI
for threadId in changedThreadIds {
guard let thread = threadStore.fetchThread(uniqueId: threadId, tx: tx) else {
// In some legitimate cases, we may not yet have a thread for a pinned
// thread. For example, if you received a pinned GV2 thread via a storage
// sync, but have not yet fetched the GV2 thread. We'll update the UI to
// reflect it when the thread is ready.
continue
}
let associatedData = threadStore.fetchOrDefaultAssociatedData(for: thread, tx: tx)
if pinnedThreadIds.contains(threadId) && (associatedData.isArchived || !thread.shouldThreadBeVisible) {
// Pinning a thread should unarchive it and make it visible if it was not already so.
threadStore.updateAssociatedData(
associatedData,
isArchived: false,
updateStorageService: updateStorageService,
tx: tx
)
threadStore.update(thread, withShouldThreadBeVisible: true, tx: tx)
} else {
self.db.touch(thread, shouldReindex: false, tx: tx)
}
}
}
}
public func pinThread(
_ thread: TSThread,
updateStorageService: Bool,
tx: DBWriteTransaction
) throws {
// When pinning a thread, we want to treat the existing list of pinned
// threads as only those that actually have current threads. Otherwise,
// there may be a pinned thread that you can't see preventing you from
// pinning a new conversation (e.g. a v2 group we haven't created yet)
var pinnedThreadIds = pinnedThreads(tx: tx).map { $0.uniqueId }
guard !pinnedThreadIds.contains(thread.uniqueId) else {
throw OWSGenericError("Attempted to pin thread that is already pinned.")
}
guard pinnedThreadIds.count < PinnedThreads.maxPinnedThreads else { throw PinnedThreadError.tooManyPinnedThreads }
pinnedThreadIds.append(thread.uniqueId)
updatePinnedThreadIds(pinnedThreadIds, updateStorageService: updateStorageService, tx: tx)
if updateStorageService {
storageServiceManager.recordPendingLocalAccountUpdates()
}
}
public func unpinThread(
_ thread: TSThread,
updateStorageService: Bool,
tx: DBWriteTransaction
) throws {
var pinnedThreadIds = pinnedThreadStore.pinnedThreadIds(tx: tx)
guard let idx = pinnedThreadIds.firstIndex(of: thread.uniqueId) else {
throw OWSGenericError("Attempted to unpin thread that is not pinned.")
}
pinnedThreadIds.remove(at: idx)
updatePinnedThreadIds(pinnedThreadIds, updateStorageService: updateStorageService, tx: tx)
if updateStorageService {
self.storageServiceManager.recordPendingLocalAccountUpdates()
}
}
public func handleUpdatedThread(_ thread: TSThread, tx: DBWriteTransaction) {
guard pinnedThreadStore.pinnedThreadIds(tx: tx).contains(thread.uniqueId) else { return }
let associatedData = threadStore.fetchOrDefaultAssociatedData(for: thread, tx: tx)
// If we now can't pin a thread, we should unpin it.
guard !canPin(thread, with: associatedData) else { return }
do {
try unpinThread(thread, updateStorageService: true, tx: tx)
} catch {
owsFailDebug("Failed to upin updated thread \(error)")
}
}
private func canPin(_ thread: TSThread, with associatedData: ThreadAssociatedData) -> Bool {
owsAssertDebug(thread.uniqueId == associatedData.threadUniqueId)
return thread.shouldThreadBeVisible && !associatedData.isArchived
}
}