mirror of https://github.com/oxen-io/session-ios
				
				
				
			
			You cannot select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
	
	
		
			282 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			Swift
		
	
			
		
		
	
	
			282 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			Swift
		
	
//
 | 
						|
//  Copyright (c) 2019 Open Whisper Systems. All rights reserved.
 | 
						|
//
 | 
						|
 | 
						|
import Foundation
 | 
						|
import PromiseKit
 | 
						|
import SignalUtilitiesKit
 | 
						|
import SignalUtilitiesKit
 | 
						|
import YYImage
 | 
						|
 | 
						|
class GifPickerCell: UICollectionViewCell {
 | 
						|
 | 
						|
    // MARK: Properties
 | 
						|
 | 
						|
    var imageInfo: GiphyImageInfo? {
 | 
						|
        didSet {
 | 
						|
            AssertIsOnMainThread()
 | 
						|
 | 
						|
            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()
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    // We do "progressive" loading by loading stills (jpg or gif) and "animated" gifs.
 | 
						|
    // This is critical on cellular connections.
 | 
						|
    var stillAssetRequest: ProxiedContentAssetRequest?
 | 
						|
    var stillAsset: ProxiedContentAsset?
 | 
						|
    var animatedAssetRequest: ProxiedContentAssetRequest?
 | 
						|
    var animatedAsset: ProxiedContentAsset?
 | 
						|
    var imageView: YYAnimatedImageView?
 | 
						|
    var activityIndicator: UIActivityIndicatorView?
 | 
						|
 | 
						|
    var isCellSelected: Bool = false {
 | 
						|
        didSet {
 | 
						|
            AssertIsOnMainThread()
 | 
						|
            ensureCellState()
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    // As another bandwidth saving measure, we only fetch the full sized GIF when the user selects it.
 | 
						|
    private var renditionForSending: GiphyRendition?
 | 
						|
 | 
						|
    // MARK: Initializers
 | 
						|
 | 
						|
    deinit {
 | 
						|
        stillAssetRequest?.cancel()
 | 
						|
        animatedAssetRequest?.cancel()
 | 
						|
    }
 | 
						|
 | 
						|
    override func prepareForReuse() {
 | 
						|
        super.prepareForReuse()
 | 
						|
 | 
						|
        imageInfo = nil
 | 
						|
        isCellVisible = false
 | 
						|
        stillAsset = nil
 | 
						|
        stillAssetRequest?.cancel()
 | 
						|
        stillAssetRequest = nil
 | 
						|
        animatedAsset = nil
 | 
						|
        animatedAssetRequest?.cancel()
 | 
						|
        animatedAssetRequest = nil
 | 
						|
        imageView?.removeFromSuperview()
 | 
						|
        imageView = nil
 | 
						|
        activityIndicator = nil
 | 
						|
        isCellSelected = false
 | 
						|
    }
 | 
						|
 | 
						|
    private func clearStillAssetRequest() {
 | 
						|
        stillAssetRequest?.cancel()
 | 
						|
        stillAssetRequest = nil
 | 
						|
    }
 | 
						|
 | 
						|
    private func clearAnimatedAssetRequest() {
 | 
						|
        animatedAssetRequest?.cancel()
 | 
						|
        animatedAssetRequest = nil
 | 
						|
    }
 | 
						|
 | 
						|
    private func clearAssetRequests() {
 | 
						|
        clearStillAssetRequest()
 | 
						|
        clearAnimatedAssetRequest()
 | 
						|
    }
 | 
						|
 | 
						|
    public func ensureCellState() {
 | 
						|
        ensureLoadState()
 | 
						|
        ensureViewState()
 | 
						|
    }
 | 
						|
 | 
						|
    public func ensureLoadState() {
 | 
						|
        guard isCellVisible else {
 | 
						|
            // Don't load if cell is not visible.
 | 
						|
            clearAssetRequests()
 | 
						|
            return
 | 
						|
        }
 | 
						|
        guard let imageInfo = imageInfo else {
 | 
						|
            // Don't load if cell is not configured.
 | 
						|
            clearAssetRequests()
 | 
						|
            return
 | 
						|
        }
 | 
						|
        guard self.animatedAsset == nil else {
 | 
						|
            // Don't load if cell is already loaded.
 | 
						|
            clearAssetRequests()
 | 
						|
            return
 | 
						|
        }
 | 
						|
 | 
						|
        // Record high quality animated rendition, but to save bandwidth, don't start downloading
 | 
						|
        // until it's selected.
 | 
						|
        guard let highQualityAnimatedRendition = imageInfo.pickSendingRendition() else {
 | 
						|
            Logger.warn("could not pick gif rendition: \(imageInfo.giphyId)")
 | 
						|
            clearAssetRequests()
 | 
						|
            return
 | 
						|
        }
 | 
						|
        self.renditionForSending = highQualityAnimatedRendition
 | 
						|
 | 
						|
        // The Giphy API returns a slew of "renditions" for a given image.
 | 
						|
        // It's critical that we carefully "pick" the best rendition to use.
 | 
						|
        guard let animatedRendition = imageInfo.pickPreviewRendition() else {
 | 
						|
            Logger.warn("could not pick gif rendition: \(imageInfo.giphyId)")
 | 
						|
            clearAssetRequests()
 | 
						|
            return
 | 
						|
        }
 | 
						|
        guard let stillRendition = imageInfo.pickStillRendition() else {
 | 
						|
            Logger.warn("could not pick still rendition: \(imageInfo.giphyId)")
 | 
						|
            clearAssetRequests()
 | 
						|
            return
 | 
						|
        }
 | 
						|
 | 
						|
        // Start still asset request if necessary.
 | 
						|
        if stillAsset != nil || animatedAsset != nil {
 | 
						|
            clearStillAssetRequest()
 | 
						|
        } else if stillAssetRequest == nil {
 | 
						|
            stillAssetRequest = GiphyDownloader.giphyDownloader.requestAsset(assetDescription: stillRendition,
 | 
						|
                                                                             priority: .high,
 | 
						|
                                                                             success: { [weak self] assetRequest, asset in
 | 
						|
                                                                                guard let strongSelf = self else { return }
 | 
						|
                                                                                if assetRequest != nil && assetRequest != strongSelf.stillAssetRequest {
 | 
						|
                                                                                    owsFailDebug("Obsolete request callback.")
 | 
						|
                                                                                    return
 | 
						|
                                                                                }
 | 
						|
                                                                                strongSelf.clearStillAssetRequest()
 | 
						|
                                                                                strongSelf.stillAsset = asset
 | 
						|
                                                                                strongSelf.ensureViewState()
 | 
						|
                },
 | 
						|
                                                                             failure: { [weak self] assetRequest in
 | 
						|
                                                                                guard let strongSelf = self else { return }
 | 
						|
                                                                                if assetRequest != strongSelf.stillAssetRequest {
 | 
						|
                                                                                    owsFailDebug("Obsolete request callback.")
 | 
						|
                                                                                    return
 | 
						|
                                                                                }
 | 
						|
                                                                                strongSelf.clearStillAssetRequest()
 | 
						|
            })
 | 
						|
        }
 | 
						|
 | 
						|
        // Start animated asset request if necessary.
 | 
						|
        if animatedAsset != nil {
 | 
						|
            clearAnimatedAssetRequest()
 | 
						|
        } else if animatedAssetRequest == nil {
 | 
						|
            animatedAssetRequest = GiphyDownloader.giphyDownloader.requestAsset(assetDescription: animatedRendition,
 | 
						|
                                                                                priority: .low,
 | 
						|
                                                                                success: { [weak self] assetRequest, asset in
 | 
						|
                                                                                    guard let strongSelf = self else { return }
 | 
						|
                                                                                    if assetRequest != nil && assetRequest != strongSelf.animatedAssetRequest {
 | 
						|
                                                                                        owsFailDebug("Obsolete request callback.")
 | 
						|
                                                                                        return
 | 
						|
                                                                                    }
 | 
						|
                                                                                    // If we have the animated asset, we don't need the still asset.
 | 
						|
                                                                                    strongSelf.clearAssetRequests()
 | 
						|
                                                                                    strongSelf.animatedAsset = asset
 | 
						|
                                                                                    strongSelf.ensureViewState()
 | 
						|
                },
 | 
						|
                                                                                failure: { [weak self] assetRequest in
 | 
						|
                                                                                    guard let strongSelf = self else { return }
 | 
						|
                                                                                    if assetRequest != strongSelf.animatedAssetRequest {
 | 
						|
                                                                                        owsFailDebug("Obsolete request callback.")
 | 
						|
                                                                                        return
 | 
						|
                                                                                    }
 | 
						|
                                                                                    strongSelf.clearAnimatedAssetRequest()
 | 
						|
            })
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    private func ensureViewState() {
 | 
						|
        guard isCellVisible else {
 | 
						|
            // Clear image view so we don't animate offscreen GIFs.
 | 
						|
            clearViewState()
 | 
						|
            return
 | 
						|
        }
 | 
						|
        guard let asset = pickBestAsset() else {
 | 
						|
            clearViewState()
 | 
						|
            return
 | 
						|
        }
 | 
						|
        guard NSData.ows_isValidImage(atPath: asset.filePath, mimeType: OWSMimeTypeImageGif) else {
 | 
						|
            owsFailDebug("invalid asset.")
 | 
						|
            clearViewState()
 | 
						|
            return
 | 
						|
        }
 | 
						|
        guard let image = YYImage(contentsOfFile: asset.filePath) else {
 | 
						|
            owsFailDebug("could not load asset.")
 | 
						|
            clearViewState()
 | 
						|
            return
 | 
						|
        }
 | 
						|
        if imageView == nil {
 | 
						|
            let imageView = YYAnimatedImageView()
 | 
						|
            self.imageView = imageView
 | 
						|
            self.contentView.addSubview(imageView)
 | 
						|
            imageView.ows_autoPinToSuperviewEdges()
 | 
						|
        }
 | 
						|
        guard let imageView = imageView else {
 | 
						|
            owsFailDebug("missing imageview.")
 | 
						|
            clearViewState()
 | 
						|
            return
 | 
						|
        }
 | 
						|
        imageView.image = image
 | 
						|
        self.backgroundColor = nil
 | 
						|
 | 
						|
        if self.isCellSelected {
 | 
						|
            let activityIndicator = UIActivityIndicatorView(style: .gray)
 | 
						|
            self.activityIndicator = activityIndicator
 | 
						|
            addSubview(activityIndicator)
 | 
						|
            activityIndicator.autoCenterInSuperview()
 | 
						|
            activityIndicator.startAnimating()
 | 
						|
 | 
						|
            // Render activityIndicator on a white tile to ensure it's visible on
 | 
						|
            // when overlayed on a variety of potential gifs.
 | 
						|
            activityIndicator.backgroundColor = UIColor.white.withAlphaComponent(0.3)
 | 
						|
            activityIndicator.autoSetDimension(.width, toSize: 30)
 | 
						|
            activityIndicator.autoSetDimension(.height, toSize: 30)
 | 
						|
            activityIndicator.layer.cornerRadius = 3
 | 
						|
            activityIndicator.layer.shadowColor = UIColor.black.cgColor
 | 
						|
            activityIndicator.layer.shadowOffset = CGSize(width: 1, height: 1)
 | 
						|
            activityIndicator.layer.shadowOpacity = 0.7
 | 
						|
            activityIndicator.layer.shadowRadius = 1.0
 | 
						|
        } else {
 | 
						|
            self.activityIndicator?.stopAnimating()
 | 
						|
            self.activityIndicator = nil
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    public func requestRenditionForSending() -> Promise<ProxiedContentAsset> {
 | 
						|
        guard let renditionForSending = self.renditionForSending else {
 | 
						|
            owsFailDebug("renditionForSending was unexpectedly nil")
 | 
						|
            return Promise(error: GiphyError.assertionError(description: "renditionForSending was unexpectedly nil"))
 | 
						|
        }
 | 
						|
 | 
						|
        let (promise, resolver) = Promise<ProxiedContentAsset>.pending()
 | 
						|
 | 
						|
        // 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.
 | 
						|
        _ = GiphyDownloader.giphyDownloader.requestAsset(assetDescription: renditionForSending,
 | 
						|
                                                         priority: .high,
 | 
						|
                                                         success: { _, asset in
 | 
						|
                                                            resolver.fulfill(asset)
 | 
						|
        },
 | 
						|
                                                         failure: { _ in
 | 
						|
                                                            // TODO GiphyDownloader API should pass through a useful failing error
 | 
						|
                                                            // so we can pass it through here
 | 
						|
                                                            Logger.error("request failed")
 | 
						|
                                                            resolver.reject(GiphyError.fetchFailure)
 | 
						|
        })
 | 
						|
 | 
						|
        return promise
 | 
						|
    }
 | 
						|
 | 
						|
    private func clearViewState() {
 | 
						|
        imageView?.image = nil
 | 
						|
        self.backgroundColor = (Theme.isDarkThemeEnabled
 | 
						|
            ? UIColor(white: 0.25, alpha: 1.0)
 | 
						|
            : UIColor(white: 0.95, alpha: 1.0))
 | 
						|
    }
 | 
						|
 | 
						|
    private func pickBestAsset() -> ProxiedContentAsset? {
 | 
						|
        return animatedAsset ?? stillAsset
 | 
						|
    }
 | 
						|
}
 |