Signal-iOS/Signal/src/ViewControllers/ConversationView/CV/CVLoader.swift
2021-08-19 17:07:15 -03:00

413 lines
22 KiB
Swift

//
// Copyright (c) 2021 Open Whisper Systems. All rights reserved.
//
import Foundation
import PromiseKit
// This entity performs a single load.
public class CVLoader: NSObject {
private let threadUniqueId: String
private let loadRequest: CVLoadRequest
private let viewStateSnapshot: CVViewStateSnapshot
private let prevRenderState: CVRenderState
private let messageMapping: CVMessageMapping
private let benchSteps = BenchSteps(title: "CVLoader")
required init(threadUniqueId: String,
loadRequest: CVLoadRequest,
viewStateSnapshot: CVViewStateSnapshot,
prevRenderState: CVRenderState,
messageMapping: CVMessageMapping) {
self.threadUniqueId = threadUniqueId
self.loadRequest = loadRequest
self.viewStateSnapshot = viewStateSnapshot
self.prevRenderState = prevRenderState
self.messageMapping = messageMapping
}
func loadPromise() -> Promise<CVUpdate> {
let threadUniqueId = self.threadUniqueId
let loadRequest = self.loadRequest
let viewStateSnapshot = self.viewStateSnapshot
let prevRenderState = self.prevRenderState
let messageMapping = self.messageMapping
Logger.verbose("LoadType: \(loadRequest.loadType)")
struct LoadState {
let threadViewModel: ThreadViewModel
let items: [CVRenderItem]
let threadInteractionCount: UInt
}
return firstly(on: CVUtils.workQueue(isInitialLoad: loadRequest.isInitialLoad)) { () -> CVUpdate in
// To ensure coherency, the entire load should be done with a single transaction.
let loadState: LoadState = try Self.databaseStorage.read { transaction in
self.benchSteps.step("start")
let loadThreadViewModel = { () -> ThreadViewModel in
guard let thread = TSThread.anyFetch(uniqueId: threadUniqueId, transaction: transaction) else {
// If thread has been deleted from the database, use last known model.
return prevRenderState.threadViewModel
}
return ThreadViewModel(thread: thread,
forHomeView: false,
transaction: transaction)
}
let threadViewModel = loadThreadViewModel()
let loadContext = CVLoadContext(loadRequest: loadRequest,
threadViewModel: threadViewModel,
viewStateSnapshot: viewStateSnapshot,
messageMapping: messageMapping,
prevRenderState: prevRenderState,
transaction: transaction)
self.benchSteps.step("threadViewModel")
if loadRequest.shouldClearOldestUnreadInteraction {
messageMapping.oldestUnreadInteraction = nil
}
// Don't cache in the reset() case.
let canReuseInteractions = loadRequest.canReuseInteractionModels && !loadRequest.didReset
var updatedInteractionIds = loadRequest.updatedInteractionIds
let deletedInteractionIds: Set<String>? = loadRequest.didReset ? loadRequest.deletedInteractionIds : nil
let didThreadDetailsChange: Bool = {
let prevThreadViewModel = prevRenderState.threadViewModel
guard let groupModel = threadViewModel.threadRecord.groupModelIfGroupThread else {
return false
}
guard let prevGroupModel = prevThreadViewModel.threadRecord.groupModelIfGroupThread else {
owsFailDebug("Missing groupModel.")
return false
}
let groupDescriptionDidChange = (groupModel as? TSGroupModelV2)?.descriptionText
!= (prevGroupModel as? TSGroupModelV2)?.descriptionText
return (groupModel.groupName != prevGroupModel.groupName ||
groupDescriptionDidChange ||
groupModel.groupAvatarData != prevGroupModel.groupAvatarData ||
groupModel.groupMembership.fullMembers.count != prevGroupModel.groupMembership.fullMembers.count)
}()
// If the thread details did change, reload the thread details
// item if one is in the load window.
if didThreadDetailsChange,
let prevFirstRenderItem = prevRenderState.items.first,
prevFirstRenderItem.interactionType == .threadDetails {
updatedInteractionIds.insert(prevFirstRenderItem.interactionUniqueId)
}
var reusableInteractions = [String: TSInteraction]()
if canReuseInteractions {
for renderItem in prevRenderState.items {
let interaction = renderItem.interaction
let interactionId = interaction.uniqueId
if !updatedInteractionIds.contains(interactionId) {
reusableInteractions[interactionId] = interaction
}
}
}
do {
switch loadRequest.loadType {
case .loadInitialMapping(let focusMessageIdOnOpen, _):
owsAssertDebug(reusableInteractions.isEmpty)
try messageMapping.loadInitialMessagePage(focusMessageId: focusMessageIdOnOpen,
reusableInteractions: [:],
deletedInteractionIds: [],
transaction: transaction)
case .loadSameLocation:
try messageMapping.loadSameLocation(reusableInteractions: reusableInteractions,
deletedInteractionIds: deletedInteractionIds,
transaction: transaction)
case .loadOlder:
try messageMapping.loadOlderMessagePage(reusableInteractions: reusableInteractions,
deletedInteractionIds: deletedInteractionIds,
transaction: transaction)
case .loadNewer:
try messageMapping.loadNewerMessagePage(reusableInteractions: reusableInteractions,
deletedInteractionIds: deletedInteractionIds,
transaction: transaction)
case .loadNewest:
try messageMapping.loadNewestMessagePage(reusableInteractions: reusableInteractions,
deletedInteractionIds: deletedInteractionIds,
transaction: transaction)
case .loadPageAroundInteraction(let interactionId, _):
try messageMapping.loadMessagePage(aroundInteractionId: interactionId,
reusableInteractions: reusableInteractions,
deletedInteractionIds: deletedInteractionIds,
transaction: transaction)
}
} catch {
owsFailDebug("Error: \(error)")
// Fail over to try to load newest.
try messageMapping.loadNewestMessagePage(reusableInteractions: reusableInteractions,
deletedInteractionIds: deletedInteractionIds,
transaction: transaction)
}
self.benchSteps.step("messageMapping")
let thread = threadViewModel.threadRecord
let threadInteractionCount = thread.numberOfInteractions(with: transaction)
self.benchSteps.step("threadInteractionCount")
let items: [CVRenderItem] = self.buildRenderItems(loadContext: loadContext,
updatedInteractionIds: updatedInteractionIds)
self.benchSteps.step("buildRenderItems")
return LoadState(threadViewModel: threadViewModel,
items: items,
threadInteractionCount: threadInteractionCount)
}
let items = loadState.items
let threadViewModel = loadState.threadViewModel
let renderState = CVRenderState(threadViewModel: threadViewModel,
prevThreadViewModel: prevRenderState.threadViewModel,
items: items,
canLoadOlderItems: messageMapping.canLoadOlder,
canLoadNewerItems: messageMapping.canLoadNewer,
viewStateSnapshot: viewStateSnapshot,
loadType: loadRequest.loadType)
self.benchSteps.step("build render state")
let threadInteractionCount = loadState.threadInteractionCount
let update = CVUpdate.build(renderState: renderState,
prevRenderState: prevRenderState,
loadRequest: loadRequest,
threadInteractionCount: threadInteractionCount)
self.benchSteps.step("build render update")
self.benchSteps.logAll()
return update
}
}
// MARK: -
private func buildRenderItems(loadContext: CVLoadContext,
updatedInteractionIds: Set<String>) -> [CVRenderItem] {
let conversationStyle = loadContext.conversationStyle
// Don't cache in the reset() case.
let canReuseState = (loadRequest.canReuseComponentStates &&
conversationStyle.isEqualForCellRendering(prevRenderState.conversationStyle))
var itemModelBuilder = CVItemModelBuilder(loadContext: loadContext)
// CVComponentStates are loaded from the database; these loads
// can be expensive. Therefore we want to reuse them _unless_:
//
// * The corresponding interaction was updated.
// * We're do a "reset" reload where we deliberately reload everything, e.g.
// in response to an error or a cross-process write, etc.
if canReuseState {
itemModelBuilder.reuseComponentStates(prevRenderState: prevRenderState,
updatedInteractionIds: updatedInteractionIds)
}
let itemModels: [CVItemModel] = itemModelBuilder.buildItems()
var renderItems = [CVRenderItem]()
for itemModel in itemModels {
guard let renderItem = buildRenderItem(itemBuildingContext: loadContext,
itemModel: itemModel) else {
continue
}
renderItems.append(renderItem)
}
return renderItems
}
private func buildRenderItem(itemBuildingContext: CVItemBuildingContext,
itemModel: CVItemModel) -> CVRenderItem? {
Self.buildRenderItem(itemBuildingContext: itemBuildingContext,
itemModel: itemModel)
}
@objc
public static func buildStandaloneRenderItem(interaction: TSInteraction,
thread: TSThread,
containerView: UIView,
transaction: SDSAnyReadTransaction) -> CVRenderItem? {
let chatColor = ChatColors.chatColorForRendering(thread: thread, transaction: transaction)
let conversationStyle = ConversationStyle(type: .`default`,
thread: thread,
viewWidth: containerView.width,
hasWallpaper: false,
isWallpaperPhoto: false,
chatColor: chatColor)
let coreState = CVCoreState(conversationStyle: conversationStyle,
mediaCache: CVMediaCache())
return CVLoader.buildStandaloneRenderItem(interaction: interaction,
thread: thread,
coreState: coreState,
transaction: transaction)
}
@objc
public static func buildStandaloneRenderItem(interaction: TSInteraction,
thread: TSThread,
conversationStyle: ConversationStyle,
transaction: SDSAnyReadTransaction) -> CVRenderItem? {
let coreState = CVCoreState(conversationStyle: conversationStyle,
mediaCache: CVMediaCache())
return CVLoader.buildStandaloneRenderItem(interaction: interaction,
thread: thread,
coreState: coreState,
transaction: transaction)
}
private static func buildStandaloneRenderItem(interaction: TSInteraction,
thread: TSThread,
coreState: CVCoreState,
transaction: SDSAnyReadTransaction) -> CVRenderItem? {
AssertIsOnMainThread()
let threadViewModel = ThreadViewModel(thread: thread,
forHomeView: false,
transaction: transaction)
let viewStateSnapshot = CVViewStateSnapshot.mockSnapshotForStandaloneItems(coreState: coreState)
let avatarBuilder = CVAvatarBuilder(transaction: transaction)
let itemBuildingContext = CVItemBuildingContextImpl(threadViewModel: threadViewModel,
viewStateSnapshot: viewStateSnapshot,
transaction: transaction,
avatarBuilder: avatarBuilder)
guard let itemModel = CVItemModelBuilder.buildStandaloneItem(interaction: interaction,
thread: thread,
threadViewModel: threadViewModel,
itemBuildingContext: itemBuildingContext,
transaction: transaction) else {
owsFailDebug("Couldn't build item model.")
return nil
}
return Self.buildRenderItem(itemBuildingContext: itemBuildingContext,
itemModel: itemModel)
}
public static func buildStandaloneComponentState(interaction: TSInteraction,
transaction: SDSAnyReadTransaction) -> CVComponentState? {
AssertIsOnMainThread()
let thread = interaction.thread(transaction: transaction)
let chatColor = ChatColors.chatColorForRendering(thread: thread, transaction: transaction)
let mockViewWidth: CGFloat = 800
let conversationStyle = ConversationStyle(type: .`default`,
thread: thread,
viewWidth: mockViewWidth,
hasWallpaper: false,
isWallpaperPhoto: false,
chatColor: chatColor)
let coreState = CVCoreState(conversationStyle: conversationStyle,
mediaCache: CVMediaCache())
let threadViewModel = ThreadViewModel(thread: thread,
forHomeView: false,
transaction: transaction)
let viewStateSnapshot = CVViewStateSnapshot.mockSnapshotForStandaloneItems(coreState: coreState)
let avatarBuilder = CVAvatarBuilder(transaction: transaction)
let itemBuildingContext = CVItemBuildingContextImpl(threadViewModel: threadViewModel,
viewStateSnapshot: viewStateSnapshot,
transaction: transaction,
avatarBuilder: avatarBuilder)
do {
return try CVComponentState.build(interaction: interaction,
itemBuildingContext: itemBuildingContext)
} catch {
owsFailDebug("Error: \(error)")
return nil
}
}
private static func buildRenderItem(itemBuildingContext: CVItemBuildingContext,
itemModel: CVItemModel) -> CVRenderItem? {
let conversationStyle = itemBuildingContext.conversationStyle
let rootComponent: CVRootComponent
switch itemModel.messageCellType {
case .dateHeader:
guard let dateHeaderState = itemModel.itemViewState.dateHeaderState else {
owsFailDebug("Missing dateHeader.")
return nil
}
rootComponent = CVComponentDateHeader(itemModel: itemModel,
dateHeaderState: dateHeaderState)
case .unreadIndicator:
rootComponent = CVComponentUnreadIndicator(itemModel: itemModel)
case .threadDetails:
guard let threadDetails = itemModel.componentState.threadDetails else {
owsFailDebug("Missing threadDetails.")
return nil
}
rootComponent = CVComponentThreadDetails(itemModel: itemModel, threadDetails: threadDetails)
case .unknownThreadWarning:
guard let unknownThreadWarning = itemModel.componentState.unknownThreadWarning else {
owsFailDebug("Missing unknownThreadWarning.")
return nil
}
rootComponent = CVComponentSystemMessage(itemModel: itemModel,
systemMessage: unknownThreadWarning)
case .defaultDisappearingMessageTimer:
guard let defaultDisappearingMessageTimer = itemModel.componentState.defaultDisappearingMessageTimer else {
owsFailDebug("Missing unknownThreadWarning.")
return nil
}
rootComponent = CVComponentSystemMessage(itemModel: itemModel,
systemMessage: defaultDisappearingMessageTimer)
case .textOnlyMessage, .audio, .genericAttachment, .contactShare,
.bodyMedia, .viewOnce, .stickerMessage:
rootComponent = CVComponentMessage(itemModel: itemModel)
case .typingIndicator:
guard let typingIndicator = itemModel.componentState.typingIndicator else {
owsFailDebug("Missing typingIndicator.")
return nil
}
rootComponent = CVComponentTypingIndicator(itemModel: itemModel,
typingIndicator: typingIndicator)
case .systemMessage:
guard let systemMessage = itemModel.componentState.systemMessage else {
owsFailDebug("Missing systemMessage.")
return nil
}
rootComponent = CVComponentSystemMessage(itemModel: itemModel, systemMessage: systemMessage)
case .unknown:
Logger.warn("Discarding item: \(itemModel.messageCellType).")
return nil
}
let cellMeasurement = buildCellMeasurement(rootComponent: rootComponent,
conversationStyle: conversationStyle)
return CVRenderItem(itemModel: itemModel,
rootComponent: rootComponent,
cellMeasurement: cellMeasurement)
}
private static func buildEmptyCellMeasurement() -> CVCellMeasurement {
CVCellMeasurement.Builder().build()
}
private static func buildCellMeasurement(rootComponent: CVRootComponent,
conversationStyle: ConversationStyle) -> CVCellMeasurement {
let measurementBuilder = CVCellMeasurement.Builder()
measurementBuilder.cellSize = rootComponent.measure(maxWidth: conversationStyle.viewWidth,
measurementBuilder: measurementBuilder)
let cellMeasurement = measurementBuilder.build()
owsAssertDebug(cellMeasurement.cellSize.width <= conversationStyle.viewWidth)
return cellMeasurement
}
}