426 lines
15 KiB
Swift
426 lines
15 KiB
Swift
//
|
|
// Copyright 2020 Signal Messenger, LLC
|
|
// SPDX-License-Identifier: AGPL-3.0-only
|
|
//
|
|
|
|
public import SignalServiceKit
|
|
public import SignalUI
|
|
|
|
// Represents some _renderable_ portion of an Conversation View item.
|
|
// It could be the entire item or some part thereof.
|
|
public protocol CVComponent: AnyObject {
|
|
|
|
var componentKey: CVComponentKey { get }
|
|
|
|
var itemModel: CVItemModel { get }
|
|
|
|
func buildComponentView(componentDelegate: CVComponentDelegate) -> CVComponentView
|
|
|
|
func configureForRendering(componentView: CVComponentView,
|
|
cellMeasurement: CVCellMeasurement,
|
|
componentDelegate: CVComponentDelegate)
|
|
|
|
// This method should only be called on workQueue.
|
|
func measure(maxWidth: CGFloat, measurementBuilder: CVCellMeasurement.Builder) -> CGSize
|
|
|
|
// return true IFF the tap was handled.
|
|
func handleTap(sender: UIGestureRecognizer,
|
|
componentDelegate: CVComponentDelegate,
|
|
componentView: CVComponentView,
|
|
renderItem: CVRenderItem) -> Bool
|
|
|
|
func canHandleDoubleTap(sender: UIGestureRecognizer,
|
|
componentDelegate: any CVComponentDelegate,
|
|
renderItem: CVRenderItem) -> Bool
|
|
|
|
func handleDoubleTap(sender: UIGestureRecognizer,
|
|
componentDelegate: any CVComponentDelegate,
|
|
renderItem: CVRenderItem) -> Bool
|
|
|
|
func findLongPressHandler(sender: UIGestureRecognizer,
|
|
componentDelegate: CVComponentDelegate,
|
|
componentView: CVComponentView,
|
|
renderItem: CVRenderItem) -> CVLongPressHandler?
|
|
|
|
func findPanHandler(sender: UIPanGestureRecognizer,
|
|
componentDelegate: CVComponentDelegate,
|
|
componentView: CVComponentView,
|
|
renderItem: CVRenderItem,
|
|
messageSwipeActionState: CVMessageSwipeActionState) -> CVPanHandler?
|
|
func startPanGesture(sender: UIPanGestureRecognizer,
|
|
panHandler: CVPanHandler,
|
|
componentDelegate: CVComponentDelegate,
|
|
componentView: CVComponentView,
|
|
renderItem: CVRenderItem,
|
|
messageSwipeActionState: CVMessageSwipeActionState)
|
|
func handlePanGesture(sender: UIPanGestureRecognizer,
|
|
panHandler: CVPanHandler,
|
|
componentDelegate: CVComponentDelegate,
|
|
componentView: CVComponentView,
|
|
renderItem: CVRenderItem,
|
|
messageSwipeActionState: CVMessageSwipeActionState)
|
|
|
|
func cellDidLayoutSubviews(componentView: CVComponentView,
|
|
renderItem: CVRenderItem,
|
|
messageSwipeActionState: CVMessageSwipeActionState)
|
|
|
|
func cellDidBecomeVisible(componentView: CVComponentView,
|
|
renderItem: CVRenderItem,
|
|
messageSwipeActionState: CVMessageSwipeActionState)
|
|
|
|
func updateScrollingContent(componentView: CVComponentView)
|
|
|
|
func contextMenuAccessoryViews(componentView: CVComponentView) -> [ContextMenuTargetedPreviewAccessory]?
|
|
|
|
func apply(layoutAttributes: CVCollectionViewLayoutAttributes,
|
|
componentView: CVComponentView)
|
|
}
|
|
|
|
// MARK: -
|
|
|
|
public protocol CVRootComponent: CVComponent {
|
|
|
|
var componentState: CVComponentState { get }
|
|
|
|
var cellReuseIdentifier: CVCellReuseIdentifier { get }
|
|
|
|
func configureCellRootComponent(cellView: UIView,
|
|
cellMeasurement: CVCellMeasurement,
|
|
componentDelegate: CVComponentDelegate,
|
|
messageSwipeActionState: CVMessageSwipeActionState,
|
|
componentView: CVComponentView)
|
|
|
|
var isDedicatedCell: Bool { get }
|
|
}
|
|
|
|
// MARK: -
|
|
|
|
public protocol CVAccessibilityComponent: CVComponent {
|
|
var accessibilityDescription: String { get }
|
|
|
|
// TODO: We should have a getter for "accessiblity actions",
|
|
// presumably as [CVMessageAction].
|
|
}
|
|
|
|
// MARK: -
|
|
|
|
public class CVComponentBase: NSObject {
|
|
public let itemModel: CVItemModel
|
|
|
|
init(itemModel: CVItemModel) {
|
|
self.itemModel = itemModel
|
|
}
|
|
|
|
public func handleTap(sender: UIGestureRecognizer,
|
|
componentDelegate: CVComponentDelegate,
|
|
componentView: CVComponentView,
|
|
renderItem: CVRenderItem) -> Bool {
|
|
return false
|
|
}
|
|
|
|
public func canHandleDoubleTap(sender: UIGestureRecognizer,
|
|
componentDelegate: any CVComponentDelegate,
|
|
renderItem: CVRenderItem) -> Bool {
|
|
return false
|
|
}
|
|
|
|
public func handleDoubleTap(sender: UIGestureRecognizer,
|
|
componentDelegate: any CVComponentDelegate,
|
|
renderItem: CVRenderItem) -> Bool {
|
|
false
|
|
}
|
|
|
|
public func findLongPressHandler(sender: UIGestureRecognizer,
|
|
componentDelegate: CVComponentDelegate,
|
|
componentView: CVComponentView,
|
|
renderItem: CVRenderItem) -> CVLongPressHandler? {
|
|
return nil
|
|
}
|
|
|
|
public func findPanHandler(sender: UIPanGestureRecognizer,
|
|
componentDelegate: CVComponentDelegate,
|
|
componentView: CVComponentView,
|
|
renderItem: CVRenderItem,
|
|
messageSwipeActionState: CVMessageSwipeActionState) -> CVPanHandler? {
|
|
return nil
|
|
}
|
|
|
|
public func startPanGesture(sender: UIPanGestureRecognizer,
|
|
panHandler: CVPanHandler,
|
|
componentDelegate: CVComponentDelegate,
|
|
componentView: CVComponentView,
|
|
renderItem: CVRenderItem,
|
|
messageSwipeActionState: CVMessageSwipeActionState) {
|
|
owsFailDebug("No pan in progress.")
|
|
}
|
|
|
|
public func handlePanGesture(sender: UIPanGestureRecognizer,
|
|
panHandler: CVPanHandler,
|
|
componentDelegate: CVComponentDelegate,
|
|
componentView: CVComponentView,
|
|
renderItem: CVRenderItem,
|
|
messageSwipeActionState: CVMessageSwipeActionState) {
|
|
owsFailDebug("No pan in progress.")
|
|
}
|
|
|
|
public func cellDidLayoutSubviews(componentView: CVComponentView,
|
|
renderItem: CVRenderItem,
|
|
messageSwipeActionState: CVMessageSwipeActionState) {
|
|
// Do nothing.
|
|
}
|
|
|
|
public func cellDidBecomeVisible(componentView: CVComponentView,
|
|
renderItem: CVRenderItem,
|
|
messageSwipeActionState: CVMessageSwipeActionState) {
|
|
// Do nothing.
|
|
}
|
|
|
|
public func contextMenuAccessoryViews(componentView: CVComponentView) -> [ContextMenuTargetedPreviewAccessory]? {
|
|
return nil
|
|
}
|
|
|
|
public func apply(layoutAttributes: CVCollectionViewLayoutAttributes,
|
|
componentView: CVComponentView) {
|
|
// Do nothing.
|
|
}
|
|
|
|
var uiMode: ConversationUIMode { itemModel.itemViewState.uiMode }
|
|
var previousUIMode: ConversationUIMode { itemModel.itemViewState.previousUIMode }
|
|
var isShowingSelectionUI: Bool { uiMode.hasSelectionUI }
|
|
var wasShowingSelectionUI: Bool { previousUIMode.hasSelectionUI }
|
|
|
|
// MARK: - Root Components
|
|
|
|
public static func configureCellRootComponent(rootComponent: CVRootComponent,
|
|
cellView: UIView,
|
|
cellMeasurement: CVCellMeasurement,
|
|
componentDelegate: CVComponentDelegate,
|
|
componentView: CVComponentView) {
|
|
owsAssertDebug(cellView.layoutMargins == .zero)
|
|
|
|
rootComponent.configureForRendering(componentView: componentView,
|
|
cellMeasurement: cellMeasurement,
|
|
componentDelegate: componentDelegate)
|
|
|
|
let rootView = componentView.rootView
|
|
if rootView.superview == nil {
|
|
owsAssertDebug(cellView.subviews.isEmpty)
|
|
|
|
cellView.addSubview(rootView)
|
|
cellView.layoutMargins = .zero
|
|
rootView.autoPinEdgesToSuperviewEdges()
|
|
}
|
|
}
|
|
|
|
// MARK: -
|
|
|
|
public func wallpaperBlurView(componentView: CVComponentView) -> CVWallpaperBlurView? {
|
|
nil
|
|
}
|
|
|
|
public func configureWallpaperBlurView(wallpaperBlurView: CVWallpaperBlurView,
|
|
maskCornerRadius: CGFloat,
|
|
componentDelegate: CVComponentDelegate) {
|
|
Self.configureWallpaperBlurView(wallpaperBlurView: wallpaperBlurView,
|
|
maskCornerRadius: maskCornerRadius,
|
|
componentDelegate: componentDelegate)
|
|
}
|
|
|
|
public static func configureWallpaperBlurView(wallpaperBlurView: CVWallpaperBlurView,
|
|
maskCornerRadius: CGFloat,
|
|
componentDelegate: CVComponentDelegate) {
|
|
if componentDelegate.isConversationPreview {
|
|
wallpaperBlurView.configureForPreview(maskCornerRadius: maskCornerRadius)
|
|
} else if let wallpaperBlurProvider = componentDelegate.wallpaperBlurProvider {
|
|
wallpaperBlurView.configure(provider: wallpaperBlurProvider,
|
|
maskCornerRadius: maskCornerRadius)
|
|
} else {
|
|
owsFailDebug("Missing wallpaperBlurProvider.")
|
|
wallpaperBlurView.configureForPreview(maskCornerRadius: maskCornerRadius)
|
|
}
|
|
}
|
|
|
|
public func updateScrollingContent(componentView: CVComponentView) {
|
|
updateWallpaperBlur(componentView: componentView)
|
|
}
|
|
|
|
private func updateWallpaperBlur(componentView: CVComponentView) {
|
|
guard let wallpaperBlurView = self.wallpaperBlurView(componentView: componentView) else {
|
|
return
|
|
}
|
|
wallpaperBlurView.updateIfNecessary()
|
|
}
|
|
}
|
|
|
|
// MARK: -
|
|
|
|
extension CVComponentBase: CVNode {
|
|
public var thread: TSThread { itemModel.thread }
|
|
public var interaction: TSInteraction { itemModel.interaction }
|
|
public var componentState: CVComponentState { itemModel.componentState }
|
|
public var itemViewState: CVItemViewState { itemModel.itemViewState }
|
|
public var messageCellType: CVMessageCellType { componentState.messageCellType }
|
|
public var conversationStyle: ConversationStyle { itemModel.conversationStyle }
|
|
public var mediaCache: CVMediaCache { itemModel.mediaCache }
|
|
public var isDarkThemeEnabled: Bool { conversationStyle.isDarkThemeEnabled }
|
|
|
|
public var isGroupThread: Bool {
|
|
thread.isGroupThread
|
|
}
|
|
|
|
public var isBorderless: Bool {
|
|
if componentState.isBorderlessJumbomojiMessage {
|
|
return true
|
|
}
|
|
if componentState.isBorderlessBodyMediaMessage {
|
|
return true
|
|
}
|
|
|
|
switch messageCellType {
|
|
case .stickerMessage:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: -
|
|
|
|
// Used for rendering some portion of an Conversation View item.
|
|
// It could be the entire item or some part thereof.
|
|
@objc
|
|
public protocol CVComponentView {
|
|
|
|
var rootView: UIView { get }
|
|
|
|
var isDedicatedCellView: Bool { get set }
|
|
|
|
func setIsCellVisible(_ isCellVisible: Bool)
|
|
|
|
func reset()
|
|
|
|
@objc
|
|
optional func canHandleDoubleTapGesture(_ sender: UIGestureRecognizer) -> Bool
|
|
|
|
// Allows component opportunity to configure and return a subview for context menu previews
|
|
@objc
|
|
optional func contextMenuContentView() -> UIView?
|
|
|
|
// Allows component opportunity to configure and return an auxiliary content subview for context menu previews
|
|
// This will only be used if contextMenuContentView() is implemented
|
|
@objc
|
|
optional func contextMenuAuxiliaryContentView() -> UIView?
|
|
|
|
// Called when the context menu presentation will begin,
|
|
// can be used to configure component view below presenting context menu
|
|
@objc
|
|
optional func contextMenuPresentationWillBegin()
|
|
|
|
// Called once the context menu presentation ends
|
|
@objc
|
|
optional func contextMenuPresentationDidEnd()
|
|
}
|
|
|
|
// MARK: -
|
|
|
|
public struct CVComponentAndView {
|
|
let key: CVComponentKey
|
|
let component: CVComponent
|
|
let componentView: CVComponentView
|
|
}
|
|
|
|
// MARK: -
|
|
|
|
public enum CVComponentKey: CustomStringConvertible, CaseIterable {
|
|
// These components appear in CVComponentMessage.
|
|
case footer
|
|
case bodyText
|
|
case bodyMedia
|
|
case senderName
|
|
case senderAvatar
|
|
case sticker
|
|
case quotedReply
|
|
case linkPreview
|
|
case giftBadge
|
|
case reactions
|
|
case viewOnce
|
|
case audioAttachment
|
|
case genericAttachment
|
|
case paymentAttachment
|
|
case archivedPaymentAttachment
|
|
case contactShare
|
|
case bottomButtons
|
|
case sendFailureBadge
|
|
|
|
case systemMessage
|
|
case dateHeader
|
|
case unreadIndicator
|
|
case typingIndicator
|
|
case threadDetails
|
|
case failedOrPendingDownloads
|
|
case unknownThreadWarning
|
|
case defaultDisappearingMessageTimer
|
|
case messageRoot
|
|
|
|
public var description: String {
|
|
switch self {
|
|
case .bodyText:
|
|
return ".bodyText"
|
|
case .bodyMedia:
|
|
return ".bodyMedia"
|
|
case .senderName:
|
|
return ".senderName"
|
|
case .senderAvatar:
|
|
return ".senderAvatar"
|
|
case .footer:
|
|
return ".footer"
|
|
case .sticker:
|
|
return ".sticker"
|
|
case .quotedReply:
|
|
return ".quotedReply"
|
|
case .linkPreview:
|
|
return ".linkPreview"
|
|
case .giftBadge:
|
|
return ".giftBadge"
|
|
case .reactions:
|
|
return ".reactions"
|
|
case .viewOnce:
|
|
return ".viewOnce"
|
|
case .audioAttachment:
|
|
return ".audioAttachment"
|
|
case .genericAttachment:
|
|
return ".genericAttachment"
|
|
case .paymentAttachment:
|
|
return ".paymentAttchment"
|
|
case .archivedPaymentAttachment:
|
|
return ".archivedPaymentAttachment"
|
|
case .contactShare:
|
|
return ".contactShare"
|
|
case .bottomButtons:
|
|
return ".bottomButtons"
|
|
case .systemMessage:
|
|
return ".systemMessage"
|
|
case .dateHeader:
|
|
return ".dateHeader"
|
|
case .unreadIndicator:
|
|
return ".unreadIndicator"
|
|
case .typingIndicator:
|
|
return ".typingIndicator"
|
|
case .threadDetails:
|
|
return ".threadDetails"
|
|
case .unknownThreadWarning:
|
|
return ".unknownThreadWarning"
|
|
case .failedOrPendingDownloads:
|
|
return ".failedOrPendingDownloads"
|
|
case .sendFailureBadge:
|
|
return ".sendFailureBadge"
|
|
case .defaultDisappearingMessageTimer:
|
|
return ".defaultDisappearingMessageTimer"
|
|
case .messageRoot:
|
|
return ".messageRoot"
|
|
}
|
|
}
|
|
|
|
var asKey: String { description }
|
|
}
|