// // Copyright 2020 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only // import SignalServiceKit public import SignalUI public class CVComponentSticker: CVComponentBase, CVComponent { public var componentKey: CVComponentKey { .sticker } private let sticker: CVComponentState.Sticker private var stickerMetadata: (any StickerMetadata)? { sticker.stickerMetadata } private var attachmentStream: ReferencedAttachmentStream? { sticker.attachmentStream } private var attachmentPointer: ReferencedAttachmentTransitPointer? { sticker.attachmentPointer } private var stickerInfo: StickerInfo? { stickerMetadata?.stickerInfo } init(itemModel: CVItemModel, sticker: CVComponentState.Sticker) { self.sticker = sticker super.init(itemModel: itemModel) } public func buildComponentView(componentDelegate: CVComponentDelegate) -> CVComponentView { CVComponentViewSticker() } public static let stickerSize: CGFloat = 175 public func configureForRendering(componentView componentViewParam: CVComponentView, cellMeasurement: CVCellMeasurement, componentDelegate: CVComponentDelegate) { guard let componentView = componentViewParam as? CVComponentViewSticker else { owsFailDebug("Unexpected componentView.") componentViewParam.reset() return } let stackView = componentView.stackView switch sticker { case .available(_, let attachmentStream): let cacheKey = CVMediaCache.CacheKey.attachment(attachmentStream.attachment.id) let isAnimated = attachmentStream.attachmentStream.contentType.isAnimatedImage let reusableMediaView: ReusableMediaView if let cachedView = mediaCache.getMediaView(cacheKey, isAnimated: isAnimated) { reusableMediaView = cachedView } else { let mediaViewAdapter = MediaViewAdapterSticker(attachmentStream: attachmentStream.attachmentStream) reusableMediaView = ReusableMediaView(mediaViewAdapter: mediaViewAdapter, mediaCache: mediaCache) mediaCache.setMediaView(reusableMediaView, forKey: cacheKey, isAnimated: isAnimated) } reusableMediaView.owner = componentView componentView.reusableMediaView = reusableMediaView let mediaView = reusableMediaView.mediaView stackView.reset() stackView.configure(config: stackViewConfig, cellMeasurement: cellMeasurement, measurementKey: Self.measurementKey_stackView, subviews: [ mediaView ]) switch CVAttachmentProgressView.progressType( forAttachment: .stream(attachmentStream), interaction: interaction ) { case .none: break case .uploading: let progressView = CVAttachmentProgressView( direction: .upload(attachmentStream: attachmentStream.attachmentStream), isDarkThemeEnabled: conversationStyle.isDarkThemeEnabled, mediaCache: mediaCache ) stackView.addSubview(progressView) stackView.centerSubviewOnSuperview(progressView, size: progressView.layoutSize) case .pendingDownload: break case .downloading: break case .unknown: owsFailDebug("Unknown progress type.") } case .downloading(let attachmentPointer): configureForRendering( attachmentPointer: attachmentPointer, transitTierDownloadState: .enqueuedOrDownloading, stackView: stackView, cellMeasurement: cellMeasurement ) case .failedOrPending(let attachmentPointer, let transitTierDownloadState): configureForRendering( attachmentPointer: attachmentPointer, transitTierDownloadState: transitTierDownloadState, stackView: stackView, cellMeasurement: cellMeasurement ) } } private func configureForRendering( attachmentPointer: ReferencedAttachmentTransitPointer, transitTierDownloadState: AttachmentDownloadState, stackView: ManualStackView, cellMeasurement: CVCellMeasurement ) { let placeholderView = UIView() placeholderView.backgroundColor = Theme.secondaryBackgroundColor placeholderView.layer.cornerRadius = 18 stackView.reset() stackView.configure(config: stackViewConfig, cellMeasurement: cellMeasurement, measurementKey: Self.measurementKey_stackView, subviews: [ placeholderView ]) let progressView = CVAttachmentProgressView( direction: .download( attachmentPointer: attachmentPointer.attachmentPointer, transitTierDownloadState: transitTierDownloadState ), isDarkThemeEnabled: conversationStyle.isDarkThemeEnabled, mediaCache: mediaCache ) stackView.addSubview(progressView) stackView.centerSubviewOnSuperview(progressView, size: progressView.layoutSize) } private var stackViewConfig: CVStackViewConfig { CVStackViewConfig(axis: .vertical, alignment: isOutgoing ? .trailing : .leading, spacing: 0, layoutMargins: .zero) } private static let measurementKey_stackView = "CVComponentSticker.measurementKey_stackView" public func measure(maxWidth: CGFloat, measurementBuilder: CVCellMeasurement.Builder) -> CGSize { owsAssertDebug(maxWidth > 0) let size: CGFloat = ceil(min(maxWidth, Self.stickerSize)) let stickerSize = CGSize.square(size) let stickerInfo = stickerSize.asManualSubviewInfo(hasFixedSize: true) let stackMeasurement = ManualStackView.measure(config: stackViewConfig, measurementBuilder: measurementBuilder, measurementKey: Self.measurementKey_stackView, subviewInfos: [ stickerInfo ], maxWidth: maxWidth) return stackMeasurement.measuredSize } // MARK: - Events public override func handleTap(sender: UIGestureRecognizer, componentDelegate: CVComponentDelegate, componentView: CVComponentView, renderItem: CVRenderItem) -> Bool { guard let stickerMetadata = stickerMetadata, attachmentStream != nil else { // Not yet downloaded. return false } componentDelegate.didTapStickerPack(stickerMetadata.packInfo) return true } // MARK: - // Used for rendering some portion of an Conversation View item. // It could be the entire item or some part thereof. public class CVComponentViewSticker: NSObject, CVComponentView { fileprivate let stackView = ManualStackView(name: "sticker.container") fileprivate var reusableMediaView: ReusableMediaView? public var isDedicatedCellView = false public var rootView: UIView { stackView } public func setIsCellVisible(_ isCellVisible: Bool) { if isCellVisible { if let reusableMediaView = reusableMediaView, reusableMediaView.owner == self { reusableMediaView.load() } } else { if let reusableMediaView = reusableMediaView, reusableMediaView.owner == self { reusableMediaView.unload() } } } public func reset() { stackView.reset() if let reusableMediaView = reusableMediaView, reusableMediaView.owner == self { reusableMediaView.unload() } } } } // MARK: - extension CVComponentSticker: CVAccessibilityComponent { public var accessibilityDescription: String { if let approximateEmoji = stickerMetadata?.firstEmoji { return String( format: OWSLocalizedString( "ACCESSIBILITY_LABEL_STICKER_FORMAT", comment: "Accessibility label for stickers. Embeds {{ name of top emoji the sticker resembles }}"), approximateEmoji ) } else { return OWSLocalizedString( "ACCESSIBILITY_LABEL_STICKER", comment: "Accessibility label for stickers." ) } } }