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