Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
signalapp
GitHub Repository: signalapp/Signal-iOS
Path: blob/main/Signal/ConversationView/Components/CVComponentSticker.swift
1 views
//
// 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: ReferencedAttachmentPointer? {
        sticker.attachmentPointer
    }

    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
    private static let progressViewSize = CGSize.square(44)

    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 isUploading):
            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(
                cvAttachment: .stream(attachmentStream, isUploading: isUploading),
            ) {
            case .none:
                break
            case .uploading:
                let progressView = CVAttachmentProgressView(
                    direction: .upload(attachmentStream: attachmentStream.attachmentStream),
                    colorConfiguration: .init(conversationStyle: conversationStyle, isIncoming: isIncoming),
                )
                stackView.addSubview(progressView)
                stackView.centerSubviewOnSuperview(progressView, size: Self.progressViewSize)
            case .skipped:
                break
            case .downloading:
                break
            }
        case .downloading(let attachmentPointer):
            configureForRendering(
                attachmentPointer: attachmentPointer,
                downloadState: .enqueuedOrDownloading,
                stackView: stackView,
                cellMeasurement: cellMeasurement,
            )
        case .skipped(let attachmentPointer, let downloadState):
            configureForRendering(
                attachmentPointer: attachmentPointer,
                downloadState: downloadState,
                stackView: stackView,
                cellMeasurement: cellMeasurement,
            )
        }
    }

    private func configureForRendering(
        attachmentPointer: ReferencedAttachmentPointer,
        downloadState: 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,
                downloadState: downloadState,
            ),
            colorConfiguration: .init(conversationStyle: conversationStyle, isIncoming: isIncoming),
        )
        stackView.addSubview(progressView)
        stackView.centerSubviewOnSuperview(progressView, size: Self.progressViewSize)
    }

    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

    override public func handleTap(
        sender: UIGestureRecognizer,
        componentDelegate: CVComponentDelegate,
        componentView: CVComponentView,
        renderItem: CVRenderItem,
    ) -> Bool {

        guard
            let 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.owner == self
                {
                    reusableMediaView.load()
                }
            } else {
                if
                    let reusableMediaView,
                    reusableMediaView.owner == self
                {
                    reusableMediaView.unload()
                }
            }
        }

        public func reset() {
            stackView.reset()

            if
                let reusableMediaView,
                reusableMediaView.owner == self
            {
                reusableMediaView.unload()
            }
        }

    }
}

// MARK: -

extension CVComponentSticker: CVAccessibilityComponent {
    public var accessibilityDescription: String {
        if let approximateEmoji = stickerMetadata?.firstEmoji {
            return String.nonPluralLocalizedStringWithFormat(
                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.",
            )
        }
    }
}