// // Copyright 2020 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only // import SignalServiceKit import SignalUI public class CVRenderItem: NSObject { public let itemModel: CVItemModel public var componentState: CVComponentState { itemModel.componentState } public var itemViewState: CVItemViewState { itemModel.itemViewState } public let rootComponent: CVRootComponent public let cellMeasurement: CVCellMeasurement public var cellSize: CGSize { cellMeasurement.cellSize } private let incomingMessageAuthorAddress: SignalServiceAddress? public var cellReuseIdentifier: String { rootComponent.cellReuseIdentifier.rawValue } public var interaction: TSInteraction { itemModel.interaction } public var interactionUniqueId: String { interaction.uniqueId } public var interactionType: OWSInteractionType { interaction.interactionType } init(itemModel: CVItemModel, rootComponent: CVRootComponent, cellMeasurement: CVCellMeasurement) { self.itemModel = itemModel self.rootComponent = rootComponent self.cellMeasurement = cellMeasurement // This value is used in a particularly hot code path. if let incomingMessage = itemModel.interaction as? TSIncomingMessage { self.incomingMessageAuthorAddress = incomingMessage.authorAddress } else { self.incomingMessageAuthorAddress = nil } } enum UpdateMode { case equal case stateChanged // The appearance of items depends on its neighbors, // so an item's appearance can change even though // its state didn't change. case appearanceChanged } func updateMode(other: CVRenderItem) -> UpdateMode { guard interactionUniqueId == other.interactionUniqueId else { // We should only compare two items that represent the same interaction. owsFailDebug("Unexpected other item.") return .stateChanged } guard componentState == other.componentState else { return .stateChanged } guard itemViewState == other.itemViewState else { return .appearanceChanged } guard itemModel.conversationStyle.isEqualForCellRendering(other.itemModel.conversationStyle) else { return .appearanceChanged } guard cellSize == other.cellSize else { Logger.verbose("cellSize: \(cellSize) != \(other.cellSize)") owsFailDebug("cellSize does not match.") return .stateChanged } guard cellMeasurement == other.cellMeasurement else { Logger.verbose("cellMeasurement: \(cellMeasurement) != \(other.cellMeasurement)") owsFailDebug("cellMeasurement does not match.") return .stateChanged } return .equal } var interactionTypeName: String { NSStringFromOWSInteractionType(interaction.interactionType) } public override var debugDescription: String { "\(interactionUniqueId) \(interactionTypeName)" } public var reactionState: InteractionReactionState? { componentState.reactions?.reactionState } } // MARK: - extension CVRenderItem: ConversationViewLayoutItem { public func vSpacing(previousLayoutItem: ConversationViewLayoutItem) -> CGFloat { guard let previousLayoutItem = previousLayoutItem as? CVRenderItem else { owsFailDebug("Invalid previousLayoutItem.") return 0 } let interaction = itemModel.interaction let previousInteraction = previousLayoutItem.itemModel.interaction switch interaction.interactionType { case .dateHeader, .unreadIndicator: return ConversationStyle.defaultMessageSpacing case .incomingMessage: switch previousInteraction.interactionType { case .incomingMessage: // Only use compact spacing within a cluster // of messages from the same author. if !itemViewState.isFirstInCluster, let selfAuthorAddress = self.incomingMessageAuthorAddress, let prevAuthorAddress = previousLayoutItem.incomingMessageAuthorAddress, selfAuthorAddress == prevAuthorAddress { return ConversationStyle.compactMessageSpacing } return ConversationStyle.defaultMessageSpacing case .call, .info, .error: return ConversationStyle.systemMessageSpacing default: return ConversationStyle.defaultMessageSpacing } case .outgoingMessage: switch previousInteraction.interactionType { case .outgoingMessage: // Only use compact spacing within a cluster. if itemViewState.isFirstInCluster { return ConversationStyle.defaultMessageSpacing } else { return ConversationStyle.compactMessageSpacing } case .call, .info, .error: return ConversationStyle.systemMessageSpacing default: return ConversationStyle.defaultMessageSpacing } case .call, .info, .error: if previousInteraction.interactionType == interaction.interactionType { switch previousInteraction.interactionType { case .error: if let errorMessage = interaction as? TSErrorMessage, let previousErrorMessage = previousInteraction as? TSErrorMessage, (errorMessage.errorType == .nonBlockingIdentityChange || previousErrorMessage.errorType != errorMessage.errorType) { return ConversationStyle.defaultMessageSpacing } return 0 case .info: if let infoMessage = interaction as? TSInfoMessage, let previousInfoMessage = previousInteraction as? TSInfoMessage, (infoMessage.messageType == .verificationStateChange || previousInfoMessage.messageType != infoMessage.messageType) { return ConversationStyle.defaultMessageSpacing } return 0 case .call: return 0 default: return ConversationStyle.defaultMessageSpacing } } else if previousInteraction.interactionType == .outgoingMessage || previousInteraction.interactionType == .incomingMessage { return ConversationStyle.systemMessageSpacing } else { return ConversationStyle.defaultMessageSpacing } default: return ConversationStyle.defaultMessageSpacing } } public var canBeUsedForContinuity: Bool { ConversationViewLayout.canInteractionBeUsedForScrollContinuity(itemModel.interaction) } public var isDateHeader: Bool { itemModel.interaction.interactionType == .dateHeader } }