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
 | |
|     }
 | |
| }
 |