Path: blob/main/Signal/src/ViewControllers/HomeView/Chat List/CLVLoader.swift
1 views
//
// Copyright 2019 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//
import SignalServiceKit
enum CLVRowChangeType {
case delete(oldIndexPath: IndexPath)
case insert(newIndexPath: IndexPath)
case move(oldIndexPath: IndexPath, newIndexPath: IndexPath)
case update(oldIndexPath: IndexPath)
// MARK: -
var logSafeDescription: String {
switch self {
case .delete(let oldIndexPath):
return "delete(oldIndexPath: \(oldIndexPath))"
case .insert(let newIndexPath):
return "insert(newIndexPath: \(newIndexPath))"
case .move(let oldIndexPath, let newIndexPath):
return "move(oldIndexPath: \(oldIndexPath), newIndexPath: \(newIndexPath))"
case .update(let oldIndexPath):
return "update(oldIndexPath: \(oldIndexPath))"
}
}
}
// MARK: -
struct CLVRowChange {
let type: CLVRowChangeType
let threadUniqueId: String
// MARK: -
var logSafeDescription: String {
"\(type), \(threadUniqueId)"
}
}
// MARK: -
enum CLVLoadResult {
case renderStateForReset(renderState: CLVRenderState)
case renderStateWithRowChanges(renderState: CLVRenderState, rowChanges: [CLVRowChange])
case reloadTable
case noChanges
}
// MARK: -
public class CLVLoader {
static func loadRenderStateForReset(viewInfo: CLVViewInfo, transaction: DBReadTransaction) -> CLVLoadResult {
AssertIsOnMainThread()
do {
let renderState = try Self.loadRenderStateInternal(viewInfo: viewInfo, transaction: transaction)
return CLVLoadResult.renderStateForReset(renderState: renderState)
} catch {
owsFailDebug("error: \(error)")
return .reloadTable
}
}
private static func loadRenderStateInternal(viewInfo: CLVViewInfo, transaction: DBReadTransaction) throws -> CLVRenderState {
let threadFinder = ThreadFinder()
let isViewingArchive = viewInfo.chatListMode == .archive
let pinnedThreadUniqueIds = DependenciesBridge.shared.pinnedThreadStore
.pinnedThreadIds(tx: transaction)
let visibleThreadUniqueIds: [String]
if isViewingArchive {
visibleThreadUniqueIds = try threadFinder.visibleArchivedThreadIds(transaction: transaction)
} else {
visibleThreadUniqueIds = try threadFinder.visibleInboxThreadIds(
filteredBy: viewInfo.inboxFilter,
requiredVisibleThreadIds: viewInfo.requiredVisibleThreadIds,
transaction: transaction,
)
}
var pinnedThreadUniqueIdsToRender = Set<String>()
var unpinnedThreadUniqueIdsForRender = [String]()
for threadUniqueId in visibleThreadUniqueIds {
if !isViewingArchive, pinnedThreadUniqueIds.contains(threadUniqueId) {
pinnedThreadUniqueIdsToRender.insert(threadUniqueId)
} else {
unpinnedThreadUniqueIdsForRender.append(threadUniqueId)
}
}
// Preserve the order from pinnedThreadUniqueIds
let orderedPinnedThreadUniqueIdsForRender = pinnedThreadUniqueIds.filter(pinnedThreadUniqueIdsToRender.contains(_:))
return CLVRenderState(
viewInfo: viewInfo,
pinnedThreadUniqueIds: orderedPinnedThreadUniqueIdsForRender,
unpinnedThreadUniqueIds: unpinnedThreadUniqueIdsForRender,
)
}
static func loadRenderStateAndDiff(
viewInfo: CLVViewInfo,
updatedItemIds: Set<String>,
lastRenderState: CLVRenderState,
transaction: DBReadTransaction,
) -> CLVLoadResult {
do {
return try loadRenderStateAndDiffInternal(
viewInfo: viewInfo,
updatedItemIds: updatedItemIds,
lastRenderState: lastRenderState,
transaction: transaction,
)
} catch {
owsFailDebug("Error: \(error)")
// Fail over to reloading the table view with a new render state.
return loadRenderStateForReset(viewInfo: viewInfo, transaction: transaction)
}
}
static func newRenderStateWithViewInfo(_ viewInfo: CLVViewInfo, lastRenderState: CLVRenderState) -> CLVLoadResult {
.renderStateWithRowChanges(
renderState: CLVRenderState(
viewInfo: viewInfo,
pinnedThreadUniqueIds: lastRenderState.pinnedThreadUniqueIds,
unpinnedThreadUniqueIds: lastRenderState.unpinnedThreadUniqueIds,
),
rowChanges: [],
)
}
private static func loadRenderStateAndDiffInternal(
viewInfo: CLVViewInfo,
updatedItemIds allUpdatedItemIds: Set<String>,
lastRenderState: CLVRenderState,
transaction: DBReadTransaction,
) throws -> CLVLoadResult {
// Ignore updates to non-visible threads.
var updatedItemIds = Set<String>()
for threadId in allUpdatedItemIds {
guard let thread = TSThread.fetchViaCache(uniqueId: threadId, transaction: transaction) else {
// Missing thread, it was deleted and should no longer be visible.
continue
}
if thread.shouldThreadBeVisible {
updatedItemIds.insert(threadId)
}
}
let newRenderState = try Self.loadRenderStateInternal(viewInfo: viewInfo, transaction: transaction)
let oldPinnedThreadIds: [String] = lastRenderState.pinnedThreadUniqueIds
let oldUnpinnedThreadIds: [String] = lastRenderState.unpinnedThreadUniqueIds
let newPinnedThreadIds: [String] = newRenderState.pinnedThreadUniqueIds
let newUnpinnedThreadIds: [String] = newRenderState.unpinnedThreadUniqueIds
struct CLVBatchUpdateValue: BatchUpdateValue {
let threadUniqueId: String
var batchUpdateId: String { threadUniqueId }
}
let oldPinnedValues = oldPinnedThreadIds.map { CLVBatchUpdateValue(threadUniqueId: $0) }
let newPinnedValues = newPinnedThreadIds.map { CLVBatchUpdateValue(threadUniqueId: $0) }
let oldUnpinnedValues = oldUnpinnedThreadIds.map { CLVBatchUpdateValue(threadUniqueId: $0) }
let newUnpinnedValues = newUnpinnedThreadIds.map { CLVBatchUpdateValue(threadUniqueId: $0) }
let pinnedChangedValues = newPinnedValues.filter { updatedItemIds.contains($0.threadUniqueId) }
let unpinnedChangedValues = newUnpinnedValues.filter { updatedItemIds.contains($0.threadUniqueId) }
let pinnedBatchUpdateItems: [BatchUpdate.Item] = try BatchUpdate.build(
viewType: .uiTableView,
oldValues: oldPinnedValues,
newValues: newPinnedValues,
changedValues: pinnedChangedValues,
)
let unpinnedBatchUpdateItems: [BatchUpdate.Item] = try BatchUpdate.build(
viewType: .uiTableView,
oldValues: oldUnpinnedValues,
newValues: newUnpinnedValues,
changedValues: unpinnedChangedValues,
)
/// For a given batch update, build a `CLVRowChangeType` with an
/// `IndexPath` in the appropriate section.
///
/// The section index is looked up dynamically based on change type, from
/// either the new or old render state (e.g., deletes use old index paths
/// while insertions use new index paths).
func rowChangeType(forBatchUpdateType batchUpdateType: BatchUpdateType, section: (CLVRenderState) -> Int) -> CLVRowChangeType {
switch batchUpdateType {
case .delete(let oldIndex):
.delete(oldIndexPath: IndexPath(row: oldIndex, section: section(lastRenderState)))
case .insert(let newIndex):
.insert(newIndexPath: IndexPath(row: newIndex, section: section(newRenderState)))
case .move(let oldIndex, let newIndex):
.move(
oldIndexPath: IndexPath(row: oldIndex, section: section(lastRenderState)),
newIndexPath: IndexPath(row: newIndex, section: section(newRenderState)),
)
case .update(let oldIndex, _):
.update(oldIndexPath: IndexPath(row: oldIndex, section: section(lastRenderState)))
}
}
func rowChanges(forBatchUpdateItems batchUpdateItems: [BatchUpdate<CLVBatchUpdateValue>.Item], section: (CLVRenderState) -> Int) -> [CLVRowChange] {
batchUpdateItems.map { item in
CLVRowChange(
type: rowChangeType(forBatchUpdateType: item.updateType, section: section),
threadUniqueId: item.value.threadUniqueId,
)
}
}
let pinnedRowChanges = rowChanges(forBatchUpdateItems: pinnedBatchUpdateItems, section: { $0.sectionIndex(for: .pinned)! })
let unpinnedRowChanges = rowChanges(forBatchUpdateItems: unpinnedBatchUpdateItems, section: { $0.sectionIndex(for: .unpinned)! })
var allRowChanges = pinnedRowChanges + unpinnedRowChanges
// The "row change" logic above deals with the .pinned and
// .unpinned sections separately.
//
// We need to special-case one kind of update: pinning and
// unpinning, where a thread moves from one section to the
// other.
if
pinnedRowChanges.count == 1,
let pinnedRowChange = pinnedRowChanges.first,
unpinnedRowChanges.count == 1,
let unpinnedRowChange = unpinnedRowChanges.first,
pinnedRowChange.threadUniqueId == unpinnedRowChange.threadUniqueId
{
switch pinnedRowChange.type {
case .delete(let oldIndexPath):
switch unpinnedRowChange.type {
case .insert(let newIndexPath):
// Unpin: Move from .pinned to .unpinned section.
allRowChanges = [CLVRowChange(
type: .move(
oldIndexPath: oldIndexPath,
newIndexPath: newIndexPath,
),
threadUniqueId: pinnedRowChange.threadUniqueId,
)]
default:
owsFailDebug("Unexpected changes. pinnedRowChange: \(pinnedRowChange)")
}
case .insert(let newIndexPath):
switch unpinnedRowChange.type {
case .delete(let oldIndexPath):
// Pin: Move from .unpinned to .pinned section.
allRowChanges = [CLVRowChange(
type: .move(
oldIndexPath: oldIndexPath,
newIndexPath: newIndexPath,
),
threadUniqueId: pinnedRowChange.threadUniqueId,
)]
default:
owsFailDebug("Unexpected changes. pinnedRowChange: \(pinnedRowChange)")
}
default:
owsFailDebug("Unexpected changes. pinnedRowChange: \(pinnedRowChange)")
}
}
return .renderStateWithRowChanges(renderState: newRenderState, rowChanges: allRowChanges)
}
}
// MARK: -
extension Collection where Element: Equatable {
func firstIndexAsInt(of element: Element) -> Int? {
guard let index = firstIndex(of: element) else { return nil }
return distance(from: startIndex, to: index)
}
}