// // Copyright 2020 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only // import SignalServiceKit // TODO: This will be part of our reuse strategy. // We'll probably want to have reuse identifiers // that correspond to certain CVComponentState common // variations, e.g.: // // * Text-only message with optional sender name + footer. // * Media message with optional text + sender name + footer. public enum CVCellReuseIdentifier: String, CaseIterable { case `default` case dateHeader case unreadIndicator case typingIndicator case threadDetails case systemMessage case unknownThreadWarning } // MARK: - // Represents a single item in the conversation history. // Could be a date header or a unread indicator. public protocol CVItemCell where Self: UICollectionViewCell { var isCellVisible: Bool { get set } } // MARK: - public class CVCell: UICollectionViewCell, CVItemCell, CVRootComponentHost { public var isCellVisible: Bool = false { didSet { componentView?.setIsCellVisible(isCellVisible) if isCellVisible { guard let renderItem = renderItem, let componentView = componentView, let messageSwipeActionState = messageSwipeActionState else { return } renderItem.rootComponent.cellDidBecomeVisible(componentView: componentView, renderItem: renderItem, messageSwipeActionState: messageSwipeActionState) } } } public var renderItem: CVRenderItem? public var componentView: CVComponentView? public var hostView: UIView { contentView } public var rootComponent: CVRootComponent? { renderItem?.rootComponent } private var messageSwipeActionState: CVMessageSwipeActionState? override init(frame: CGRect) { super.init(frame: frame) layoutMargins = .zero contentView.layoutMargins = .zero } @available(*, unavailable, message: "Unimplemented") required public init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } public static func registerReuseIdentifiers(collectionView: UICollectionView) { for value in CVCellReuseIdentifier.allCases { collectionView.register(self, forCellWithReuseIdentifier: value.rawValue) } } public override func systemLayoutSizeFitting(_ targetSize: CGSize) -> CGSize { guard let renderItem = renderItem else { owsFailDebug("Missing renderItem.") return super.systemLayoutSizeFitting(targetSize) } let cellSize = renderItem.cellSize if cellSize.width > targetSize.width || cellSize.height > targetSize.height { // This can happen due to races or incorrect initial view size on iPad. Logger.verbose("Unexpected cellSize: \(cellSize), targetSize: \(targetSize)") } return targetSize } public override func systemLayoutSizeFitting(_ targetSize: CGSize, withHorizontalFittingPriority horizontalFittingPriority: UILayoutPriority, verticalFittingPriority: UILayoutPriority) -> CGSize { guard let renderItem = renderItem else { owsFailDebug("Missing renderItem.") return super.systemLayoutSizeFitting(targetSize, withHorizontalFittingPriority: horizontalFittingPriority, verticalFittingPriority: verticalFittingPriority) } let cellSize = renderItem.cellSize if cellSize.width > targetSize.width || cellSize.height > targetSize.height { // This can happen due to races or incorrect initial view size on iPad. Logger.verbose("Unexpected cellSize: \(cellSize), targetSize: \(targetSize)") } return targetSize } // For perf reasons, skip the default implementation which is only relevant for self-sizing cells. public override func preferredLayoutAttributesFitting(_ layoutAttributes: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutAttributes { layoutAttributes } private var lastLayoutAttributes: CVCollectionViewLayoutAttributes? public override func apply(_ layoutAttributes: UICollectionViewLayoutAttributes) { super.apply(layoutAttributes) guard let layoutAttributes = layoutAttributes as? CVCollectionViewLayoutAttributes else { owsFailDebug("Could not apply layoutAttributes.") return } lastLayoutAttributes = layoutAttributes applyLastLayoutAttributes() } func configure(renderItem: CVRenderItem, componentDelegate: CVComponentDelegate, messageSwipeActionState: CVMessageSwipeActionState) { let isReusingDedicatedCell = componentView != nil && renderItem.rootComponent.isDedicatedCell if !isReusingDedicatedCell { layoutMargins = .zero contentView.layoutMargins = .zero } configureForHosting(renderItem: renderItem, componentDelegate: componentDelegate, messageSwipeActionState: messageSwipeActionState) self.messageSwipeActionState = messageSwipeActionState applyLastLayoutAttributes() } private func applyLastLayoutAttributes() { guard let layoutAttributes = self.lastLayoutAttributes else { Logger.warn("Missing layoutAttributes.") return } // Insist that the cell honor its zIndex. layer.zPosition = CGFloat(layoutAttributes.zIndex) guard let rootComponent = self.rootComponent, let componentView = self.componentView else { return } rootComponent.apply(layoutAttributes: layoutAttributes, componentView: componentView) } override public func prepareForReuse() { super.prepareForReuse() var isDedicatedCell = false if let rootComponent = self.rootComponent { isDedicatedCell = rootComponent.isDedicatedCell } else { owsFailDebug("Missing rootComponent.") } renderItem = nil if !isDedicatedCell { contentView.removeAllSubviews() } if let componentView = componentView { componentView.reset() } else { owsFailDebug("Missing componentView.") } isCellVisible = false messageSwipeActionState = nil lastLayoutAttributes = nil layer.zPosition = 0 } public override func layoutSubviews() { super.layoutSubviews() guard let renderItem = renderItem, let componentView = componentView, let messageSwipeActionState = messageSwipeActionState else { return } renderItem.rootComponent.cellDidLayoutSubviews(componentView: componentView, renderItem: renderItem, messageSwipeActionState: messageSwipeActionState) } } // MARK: - // This view hosts the cell contents. // This allows us to display message cells outside of // UICollectionView, e.g. in the message details view. public class CVCellView: UIView, CVRootComponentHost { public var isCellVisible: Bool = false { didSet { componentView?.setIsCellVisible(isCellVisible) } } public var renderItem: CVRenderItem? public var componentView: CVComponentView? public var hostView: UIView { self } public var rootComponent: CVRootComponent? { renderItem?.rootComponent } init() { super.init(frame: .zero) } public func configure(renderItem: CVRenderItem, componentDelegate: CVComponentDelegate) { self.layoutMargins = .zero let messageSwipeActionState = CVMessageSwipeActionState() configureForHosting(renderItem: renderItem, componentDelegate: componentDelegate, messageSwipeActionState: messageSwipeActionState) owsAssertDebug(componentView != nil) } @available(*, unavailable, message: "Unimplemented") required public init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } func reset() { renderItem = nil removeAllSubviews() if let componentView = componentView { componentView.reset() } } } // MARK: - public protocol CVRootComponentHost: AnyObject { var renderItem: CVRenderItem? { get set } var componentView: CVComponentView? { get set } var rootComponent: CVRootComponent? { get } var hostView: UIView { get } var isCellVisible: Bool { get } } // MARK: - public extension CVRootComponentHost { fileprivate func configureForHosting(renderItem: CVRenderItem, componentDelegate: CVComponentDelegate, messageSwipeActionState: CVMessageSwipeActionState) { self.renderItem = renderItem #if TESTABLE_BUILD GRDBDatabaseStorageAdapter.canOpenTransaction = false #endif let rootComponent = renderItem.rootComponent let componentView: CVComponentView if let componentViewForReuse = self.componentView { componentView = componentViewForReuse } else { componentView = rootComponent.buildComponentView(componentDelegate: componentDelegate) } self.componentView = componentView componentView.setIsCellVisible(isCellVisible) componentView.isDedicatedCellView = rootComponent.isDedicatedCell rootComponent.configureCellRootComponent(cellView: hostView, cellMeasurement: renderItem.cellMeasurement, componentDelegate: componentDelegate, messageSwipeActionState: messageSwipeActionState, componentView: componentView) #if TESTABLE_BUILD GRDBDatabaseStorageAdapter.canOpenTransaction = true #endif } func handleTap(sender: UIGestureRecognizer, componentDelegate: CVComponentDelegate) -> Bool { guard let renderItem = renderItem else { owsFailDebug("Missing renderItem.") return false } guard let componentView = componentView else { owsFailDebug("Missing componentView.") return false } return renderItem.rootComponent.handleTap(sender: sender, componentDelegate: componentDelegate, componentView: componentView, renderItem: renderItem) } func canHandleDoubleTap(sender: UIGestureRecognizer, componentDelegate: any CVComponentDelegate) -> Bool { guard let componentView = componentView else { owsFailDebug("Missing componentView.") return false } // Can the _view_ handle the double tap? guard componentView.canHandleDoubleTapGesture?(sender) == true else { return false } guard let renderItem = renderItem else { owsFailDebug("Missing renderItem.") return false } // Can the _contents_ handle the double tap? return renderItem.rootComponent.canHandleDoubleTap(sender: sender, componentDelegate: componentDelegate, renderItem: renderItem) } func handleDoubleTap(sender: UIGestureRecognizer, componentDelegate: any CVComponentDelegate) -> Bool { guard canHandleDoubleTap(sender: sender, componentDelegate: componentDelegate) else { return false } guard let renderItem = renderItem else { owsFailDebug("Missing renderItem.") return false } return renderItem.rootComponent.handleDoubleTap(sender: sender, componentDelegate: componentDelegate, renderItem: renderItem) } func findLongPressHandler(sender: UIGestureRecognizer, componentDelegate: CVComponentDelegate) -> CVLongPressHandler? { guard let renderItem = renderItem else { owsFailDebug("Missing renderItem.") return nil } guard let componentView = componentView else { owsFailDebug("Missing componentView.") return nil } return renderItem.rootComponent.findLongPressHandler(sender: sender, componentDelegate: componentDelegate, componentView: componentView, renderItem: renderItem) } func findPanHandler(sender: UIPanGestureRecognizer, componentDelegate: CVComponentDelegate, messageSwipeActionState: CVMessageSwipeActionState) -> CVPanHandler? { guard let renderItem = renderItem else { owsFailDebug("Missing renderItem.") return nil } guard let componentView = componentView else { owsFailDebug("Missing componentView.") return nil } return renderItem.rootComponent.findPanHandler(sender: sender, componentDelegate: componentDelegate, componentView: componentView, renderItem: renderItem, messageSwipeActionState: messageSwipeActionState) } func startPanGesture(sender: UIPanGestureRecognizer, panHandler: CVPanHandler, componentDelegate: CVComponentDelegate, messageSwipeActionState: CVMessageSwipeActionState) { guard let renderItem = renderItem else { owsFailDebug("Missing renderItem.") return } guard let componentView = componentView else { owsFailDebug("Missing componentView.") return } renderItem.rootComponent.startPanGesture(sender: sender, panHandler: panHandler, componentDelegate: componentDelegate, componentView: componentView, renderItem: renderItem, messageSwipeActionState: messageSwipeActionState) } func handlePanGesture(sender: UIPanGestureRecognizer, panHandler: CVPanHandler, componentDelegate: CVComponentDelegate, messageSwipeActionState: CVMessageSwipeActionState) { guard let renderItem = renderItem else { owsFailDebug("Missing renderItem.") return } guard let componentView = componentView else { owsFailDebug("Missing componentView.") return } renderItem.rootComponent.handlePanGesture(sender: sender, panHandler: panHandler, componentDelegate: componentDelegate, componentView: componentView, renderItem: renderItem, messageSwipeActionState: messageSwipeActionState) } func albumItemView(forAttachment attachment: ReferencedTSResource) -> UIView? { guard let renderItem = renderItem else { owsFailDebug("Missing renderItem.") return nil } guard let componentView = componentView else { owsFailDebug("Missing componentView.") return nil } guard let messageComponent = renderItem.rootComponent as? CVComponentMessage else { owsFailDebug("Invalid rootComponent.") return nil } return messageComponent.albumItemView(forAttachment: attachment, componentView: componentView) } func updateScrollingContent() { guard let rootComponent = rootComponent, let componentView = componentView else { owsFailDebug("Missing component.") return } rootComponent.updateScrollingContent(componentView: componentView) } }