Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
signalapp
GitHub Repository: signalapp/Signal-iOS
Path: blob/main/Signal/src/ViewControllers/GifPicker/GifPickerCell.swift
1 views
//
// Copyright 2017 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//

import SDWebImage
import SignalServiceKit
import SignalUI

class GifPickerCell: UICollectionViewCell {

    private let imageView = SDAnimatedImageView()
    private let mp4View = LoopingVideoView()
    private let activityIndicator: UIActivityIndicatorView = {
        let view = UIActivityIndicatorView(style: .medium)
        view.backgroundColor = UIColor.white.withAlphaComponent(0.3)
        view.autoSetDimension(.width, toSize: 30)
        view.autoSetDimension(.height, toSize: 30)
        view.layer.cornerRadius = 3
        view.layer.shadowColor = UIColor.black.cgColor
        view.layer.shadowOffset = CGSize(square: 1)
        view.layer.shadowOpacity = 0.7
        view.layer.shadowRadius = 1.0
        view.hidesWhenStopped = true
        return view
    }()

    private var previewAsset: ProxiedContentAsset?
    private var previewAssetRequest: ProxiedContentAssetRequest? {
        didSet { oldValue?.cancel() }
    }

    // MARK: - Lifecycle

    override init(frame: CGRect) {
        super.init(frame: frame)

        [imageView, mp4View, activityIndicator].forEach {
            contentView.addSubview($0)
        }
        imageView.isHidden = true
        mp4View.isHidden = true

        imageView.autoPinEdgesToSuperviewEdges()
        mp4View.autoPinEdgesToSuperviewEdges()
        activityIndicator.autoCenterInSuperview()

        NotificationCenter.default.addObserver(
            self,
            selector: #selector(applyTheme),
            name: .themeDidChange,
            object: nil,
        )

        applyTheme()
    }

    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    deinit {
        previewAssetRequest?.cancel()
    }

    // MARK: Public

    func ensureCellState() {
        ensureLoadState()
        ensureViewState()
    }

    var imageInfo: GiphyImageInfo? {
        didSet {
            AssertIsOnMainThread()
            if imageInfo?.isValidImage == false {
                owsFailDebug("Invalid image info set on cell")
                imageInfo = nil
            }
            ensureCellState()
        }
    }

    // Loading and playing GIFs is quite expensive (network, memory, cpu).
    // Here's a bit of logic to not preload offscreen cells that are prefetched.
    var isCellVisible = false {
        didSet {
            AssertIsOnMainThread()
            ensureCellState()
        }
    }

    override var isSelected: Bool {
        didSet {
            AssertIsOnMainThread()
            ensureCellState()
        }
    }

    var isDisplayingPreview: Bool {
        (previewAsset != nil) && (mp4View.video != nil || imageView.image != nil)
    }

    func requestRenditionForSending() async throws(GiphyError) -> ProxiedContentAsset {
        guard
            let imageInfo,
            let fullSizeAsset = imageInfo.fullSizeAsset
        else {
            owsFailDebug("fullSizeAsset was unexpectedly nil")
            throw GiphyError.assertionError(description: "fullSizeAsset was unexpectedly nil")
        }

        // We don't retain a handle on the asset request, since there will only ever
        // be one selected asset, and we never want to cancel it.
        do {
            return try await GiphyDownloader.giphyDownloader.requestAsset(assetDescription: fullSizeAsset, priority: .high)
        } catch {
            // TODO GiphyDownloader API should pass through a useful failing error
            // so we can pass it through here
            Logger.error("request failed")
            throw .fetchFailure
        }
    }

    // MARK: UICollectionViewCell

    override func prepareForReuse() {
        super.prepareForReuse()
        imageInfo = nil
        isCellVisible = false
        isSelected = false
        previewAssetRequest = nil
        previewAsset = nil
        clearViewState()
    }

    // MARK: - Private

    @objc
    private func applyTheme() {
        backgroundColor = Theme.isDarkThemeEnabled ? .ows_gray75 : .ows_gray05
    }

    private func ensureLoadState() {
        guard isCellVisible, let imageInfo else {
            // Nothing to load. We don't load non-visible cell content
            previewAssetRequest = nil
            return
        }
        guard previewAssetRequest == nil, previewAsset == nil else {
            // We already have a load in progress, or we've already loaded the asset
            return
        }
        guard let previewAssetDescription = imageInfo.animatedPreviewAsset else {
            Logger.warn("could not pick gif rendition: \(imageInfo.giphyId)")
            return
        }

        previewAssetRequest = GiphyDownloader.giphyDownloader.requestAsset(
            assetDescription: previewAssetDescription,
            priority: .low,
            success: { [weak self] assetRequest, asset in
                AssertIsOnMainThread()
                guard let self else { return }
                guard assetRequest == self.previewAssetRequest else {
                    owsFailDebug("Obsolete request callback.")
                    return
                }
                self.previewAssetRequest = nil
                self.previewAsset = asset
                self.ensureViewState()
            },
            failure: { [weak self] assetRequest in
                AssertIsOnMainThread()
                guard let self else { return }
                guard assetRequest == self.previewAssetRequest else {
                    owsFailDebug("Obsolete request callback.")
                    return
                }
                self.previewAssetRequest = nil
            },
        )
    }

    private func ensureViewState() {
        AssertIsOnMainThread()

        guard isCellVisible, let asset = previewAsset else {
            // Nothing to show,
            clearViewState()
            return
        }
        if isSelected {
            activityIndicator.startAnimating()
        } else {
            activityIndicator.stopAnimating()
        }

        if asset.assetDescription.fileExtension == "mp4" {
            let video = LoopingVideo(decryptedLocalFileUrl: URL(fileURLWithPath: asset.filePath))
            mp4View.video = video
            mp4View.isHidden = false
        } else if (try? DataImageSource.forPath(asset.filePath))?.ows_isValidImage ?? false, let image = SDAnimatedImage(contentsOfFile: asset.filePath) {
            imageView.image = image
            imageView.isHidden = false
        } else if (try? DataImageSource.forPath(asset.filePath))?.ows_isValidImage ?? false, let image = UIImage(contentsOfFile: asset.filePath) {
            imageView.image = image
            imageView.isHidden = false
        } else {
            owsFailDebug("could not load asset.")
            clearViewState()
            return
        }
    }

    private func clearViewState() {
        AssertIsOnMainThread()

        imageView.image = nil
        imageView.isHidden = true
        mp4View.video = nil
        mp4View.isHidden = true
        activityIndicator.stopAnimating()
    }
}