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.
		
		
		
		
		
			
		
			
				
	
	
		
			436 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			Swift
		
	
			
		
		
	
	
			436 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			Swift
		
	
| //
 | |
| //  Copyright (c) 2018 Open Whisper Systems. All rights reserved.
 | |
| //
 | |
| 
 | |
| import Foundation
 | |
| import MediaPlayer
 | |
| import YYImage
 | |
| import SignalServiceKit
 | |
| 
 | |
| @objc
 | |
| public enum MediaMessageViewMode: UInt {
 | |
|     case large
 | |
|     case small
 | |
|     case attachmentApproval
 | |
| }
 | |
| 
 | |
| @objc
 | |
| public class MediaMessageView: UIView, OWSAudioPlayerDelegate {
 | |
| 
 | |
|     // MARK: Properties
 | |
| 
 | |
|     @objc
 | |
|     public let mode: MediaMessageViewMode
 | |
| 
 | |
|     @objc
 | |
|     public let attachment: SignalAttachment
 | |
| 
 | |
|     @objc
 | |
|     public var audioPlayer: OWSAudioPlayer?
 | |
| 
 | |
|     @objc
 | |
|     public var audioPlayButton: UIButton?
 | |
| 
 | |
|     @objc
 | |
|     public var videoPlayButton: UIImageView?
 | |
| 
 | |
|     @objc
 | |
|     public var playbackState = AudioPlaybackState.stopped {
 | |
|         didSet {
 | |
|             AssertIsOnMainThread()
 | |
| 
 | |
|             ensureButtonState()
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     @objc
 | |
|     public var audioProgressSeconds: CGFloat = 0
 | |
| 
 | |
|     @objc
 | |
|     public var audioDurationSeconds: CGFloat = 0
 | |
| 
 | |
|     @objc
 | |
|     public var contentView: UIView?
 | |
| 
 | |
|     // MARK: Initializers
 | |
| 
 | |
|     @available(*, unavailable, message:"use other constructor instead.")
 | |
|     required public init?(coder aDecoder: NSCoder) {
 | |
|         notImplemented()
 | |
|     }
 | |
| 
 | |
|     // Currently we only use one mode (AttachmentApproval), so we could simplify this class, but it's kind
 | |
|     // of nice that it's written in a flexible way in case we'd want to use it elsewhere again in the future.
 | |
|     @objc
 | |
|     public required init(attachment: SignalAttachment, mode: MediaMessageViewMode) {
 | |
|         assert(!attachment.hasError)
 | |
|         self.attachment = attachment
 | |
|         self.mode = mode
 | |
|         super.init(frame: CGRect.zero)
 | |
| 
 | |
|         createViews()
 | |
|     }
 | |
| 
 | |
|     deinit {
 | |
|         NotificationCenter.default.removeObserver(self)
 | |
|     }
 | |
| 
 | |
|     // MARK: - Create Views
 | |
| 
 | |
|     private func createViews() {
 | |
|         if attachment.isAnimatedImage {
 | |
|             createAnimatedPreview()
 | |
|         } else if attachment.isImage {
 | |
|             createImagePreview()
 | |
|         } else if attachment.isVideo {
 | |
|             createVideoPreview()
 | |
|         } else if attachment.isAudio {
 | |
|             createAudioPreview()
 | |
|         } else {
 | |
|             createGenericPreview()
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     private func wrapViewsInVerticalStack(subviews: [UIView]) -> UIView {
 | |
|         assert(subviews.count > 0)
 | |
| 
 | |
|         let stackView = UIView()
 | |
| 
 | |
|         var lastView: UIView?
 | |
|         for subview in subviews {
 | |
| 
 | |
|             stackView.addSubview(subview)
 | |
|             subview.autoHCenterInSuperview()
 | |
| 
 | |
|             if lastView == nil {
 | |
|                 subview.autoPinEdge(toSuperviewEdge: .top)
 | |
|             } else {
 | |
|                 subview.autoPinEdge(.top, to: .bottom, of: lastView!, withOffset: stackSpacing())
 | |
|             }
 | |
| 
 | |
|             lastView = subview
 | |
|         }
 | |
| 
 | |
|         lastView?.autoPinEdge(toSuperviewEdge: .bottom)
 | |
| 
 | |
|         return stackView
 | |
|     }
 | |
| 
 | |
|     private func stackSpacing() -> CGFloat {
 | |
|         switch mode {
 | |
|         case .large, .attachmentApproval:
 | |
|             return CGFloat(10)
 | |
|         case .small:
 | |
|             return CGFloat(5)
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     private func createAudioPreview() {
 | |
|         guard let dataUrl = attachment.dataUrl else {
 | |
|             createGenericPreview()
 | |
|             return
 | |
|         }
 | |
| 
 | |
|         audioPlayer = OWSAudioPlayer(mediaUrl: dataUrl, audioBehavior: .playback, delegate: self)
 | |
| 
 | |
|         var subviews = [UIView]()
 | |
| 
 | |
|         let audioPlayButton = UIButton()
 | |
|         self.audioPlayButton = audioPlayButton
 | |
|         setAudioIconToPlay()
 | |
|         audioPlayButton.imageView?.layer.minificationFilter = kCAFilterTrilinear
 | |
|         audioPlayButton.imageView?.layer.magnificationFilter = kCAFilterTrilinear
 | |
|         audioPlayButton.addTarget(self, action: #selector(audioPlayButtonPressed), for: .touchUpInside)
 | |
|         let buttonSize = createHeroViewSize()
 | |
|         audioPlayButton.autoSetDimension(.width, toSize: buttonSize)
 | |
|         audioPlayButton.autoSetDimension(.height, toSize: buttonSize)
 | |
|         subviews.append(audioPlayButton)
 | |
| 
 | |
|         let fileNameLabel = createFileNameLabel()
 | |
|         if let fileNameLabel = fileNameLabel {
 | |
|             subviews.append(fileNameLabel)
 | |
|         }
 | |
| 
 | |
|         let fileSizeLabel = createFileSizeLabel()
 | |
|         subviews.append(fileSizeLabel)
 | |
| 
 | |
|         let stackView = wrapViewsInVerticalStack(subviews: subviews)
 | |
|         self.addSubview(stackView)
 | |
|         fileNameLabel?.autoPinWidthToSuperview(withMargin: 32)
 | |
| 
 | |
|         // We want to center the stackView in it's superview while also ensuring
 | |
|         // it's superview is big enough to contain it.
 | |
|         stackView.autoPinWidthToSuperview()
 | |
|         stackView.autoVCenterInSuperview()
 | |
|         NSLayoutConstraint.autoSetPriority(UILayoutPriority.defaultLow) {
 | |
|             stackView.autoPinHeightToSuperview()
 | |
|         }
 | |
|         stackView.autoPinEdge(toSuperviewEdge: .top, withInset: 0, relation: .greaterThanOrEqual)
 | |
|         stackView.autoPinEdge(toSuperviewEdge: .bottom, withInset: 0, relation: .greaterThanOrEqual)
 | |
|     }
 | |
| 
 | |
|     private func createAnimatedPreview() {
 | |
|         guard attachment.isValidImage else {
 | |
|             createGenericPreview()
 | |
|             return
 | |
|         }
 | |
|         guard let dataUrl = attachment.dataUrl else {
 | |
|             createGenericPreview()
 | |
|             return
 | |
|         }
 | |
|         guard let image = YYImage(contentsOfFile: dataUrl.path) else {
 | |
|             createGenericPreview()
 | |
|             return
 | |
|         }
 | |
|         guard image.size.width > 0 && image.size.height > 0 else {
 | |
|             createGenericPreview()
 | |
|             return
 | |
|         }
 | |
|         let animatedImageView = YYAnimatedImageView()
 | |
|         animatedImageView.image = image
 | |
|         let aspectRatio = image.size.width / image.size.height
 | |
|         addSubviewWithScaleAspectFitLayout(view: animatedImageView, aspectRatio: aspectRatio)
 | |
|         contentView = animatedImageView
 | |
|     }
 | |
| 
 | |
|     private func addSubviewWithScaleAspectFitLayout(view: UIView, aspectRatio: CGFloat) {
 | |
|         self.addSubview(view)
 | |
|         // This emulates the behavior of contentMode = .scaleAspectFit using
 | |
|         // iOS auto layout constraints.  
 | |
|         //
 | |
|         // This allows ConversationInputToolbar to place the "cancel" button
 | |
|         // in the upper-right hand corner of the preview content.
 | |
|         view.autoCenterInSuperview()
 | |
|         view.autoPin(toAspectRatio: aspectRatio)
 | |
|         view.autoMatch(.width, to: .width, of: self, withMultiplier: 1.0, relation: .lessThanOrEqual)
 | |
|         view.autoMatch(.height, to: .height, of: self, withMultiplier: 1.0, relation: .lessThanOrEqual)
 | |
|     }
 | |
| 
 | |
|     private func createImagePreview() {
 | |
|         guard attachment.isValidImage else {
 | |
|             createGenericPreview()
 | |
|             return
 | |
|         }
 | |
|         guard let image = attachment.image() else {
 | |
|             createGenericPreview()
 | |
|             return
 | |
|         }
 | |
|         guard image.size.width > 0 && image.size.height > 0 else {
 | |
|             createGenericPreview()
 | |
|             return
 | |
|         }
 | |
| 
 | |
|         let imageView = UIImageView(image: image)
 | |
|         imageView.layer.minificationFilter = kCAFilterTrilinear
 | |
|         imageView.layer.magnificationFilter = kCAFilterTrilinear
 | |
|         let aspectRatio = image.size.width / image.size.height
 | |
|         addSubviewWithScaleAspectFitLayout(view: imageView, aspectRatio: aspectRatio)
 | |
|         contentView = imageView
 | |
|     }
 | |
| 
 | |
|     private func createVideoPreview() {
 | |
|         guard attachment.isValidVideo else {
 | |
|             createGenericPreview()
 | |
|             return
 | |
|         }
 | |
|         guard let image = attachment.videoPreview() else {
 | |
|             createGenericPreview()
 | |
|             return
 | |
|         }
 | |
|         guard image.size.width > 0 && image.size.height > 0 else {
 | |
|             createGenericPreview()
 | |
|             return
 | |
|         }
 | |
| 
 | |
|         let imageView = UIImageView(image: image)
 | |
|         imageView.layer.minificationFilter = kCAFilterTrilinear
 | |
|         imageView.layer.magnificationFilter = kCAFilterTrilinear
 | |
|         let aspectRatio = image.size.width / image.size.height
 | |
|         addSubviewWithScaleAspectFitLayout(view: imageView, aspectRatio: aspectRatio)
 | |
|         contentView = imageView
 | |
| 
 | |
|         // attachment approval provides it's own play button to keep it
 | |
|         // at the proper zoom scale.
 | |
|         if mode != .attachmentApproval {
 | |
|             let videoPlayIcon = UIImage(named: "play_button")!
 | |
|             let videoPlayButton = UIImageView(image: videoPlayIcon)
 | |
|             self.videoPlayButton = videoPlayButton
 | |
|             videoPlayButton.contentMode = .scaleAspectFit
 | |
|             self.addSubview(videoPlayButton)
 | |
|             videoPlayButton.autoCenterInSuperview()
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     private func createGenericPreview() {
 | |
|         var subviews = [UIView]()
 | |
| 
 | |
|         let imageView = createHeroImageView(imageName: "file-thin-black-filled-large")
 | |
|         subviews.append(imageView)
 | |
| 
 | |
|         let fileNameLabel = createFileNameLabel()
 | |
|         if let fileNameLabel = fileNameLabel {
 | |
|             subviews.append(fileNameLabel)
 | |
|         }
 | |
| 
 | |
|         let fileSizeLabel = createFileSizeLabel()
 | |
|         subviews.append(fileSizeLabel)
 | |
| 
 | |
|         let stackView = wrapViewsInVerticalStack(subviews: subviews)
 | |
|         self.addSubview(stackView)
 | |
|         fileNameLabel?.autoPinWidthToSuperview(withMargin: 32)
 | |
| 
 | |
|         // We want to center the stackView in it's superview while also ensuring
 | |
|         // it's superview is big enough to contain it.
 | |
|         stackView.autoPinWidthToSuperview()
 | |
|         stackView.autoVCenterInSuperview()
 | |
|         NSLayoutConstraint.autoSetPriority(UILayoutPriority.defaultLow) {
 | |
|             stackView.autoPinHeightToSuperview()
 | |
|         }
 | |
|         stackView.autoPinEdge(toSuperviewEdge: .top, withInset: 0, relation: .greaterThanOrEqual)
 | |
|         stackView.autoPinEdge(toSuperviewEdge: .bottom, withInset: 0, relation: .greaterThanOrEqual)
 | |
|     }
 | |
| 
 | |
|     private func createHeroViewSize() -> CGFloat {
 | |
|         switch mode {
 | |
|         case .large:
 | |
|             return ScaleFromIPhone5To7Plus(175, 225)
 | |
|         case .attachmentApproval:
 | |
|             return ScaleFromIPhone5(100)
 | |
|         case .small:
 | |
|             return ScaleFromIPhone5To7Plus(80, 80)
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     private func createHeroImageView(imageName: String) -> UIView {
 | |
|         let imageSize = createHeroViewSize()
 | |
| 
 | |
|         let image = UIImage(named: imageName)
 | |
|         assert(image != nil)
 | |
|         let imageView = UIImageView(image: image)
 | |
|         imageView.layer.minificationFilter = kCAFilterTrilinear
 | |
|         imageView.layer.magnificationFilter = kCAFilterTrilinear
 | |
|         imageView.layer.shadowColor = UIColor.black.cgColor
 | |
|         let shadowScaling = 5.0
 | |
|         imageView.layer.shadowRadius = CGFloat(2.0 * shadowScaling)
 | |
|         imageView.layer.shadowOpacity = 0.25
 | |
|         imageView.layer.shadowOffset = CGSize(width: 0.75 * shadowScaling, height: 0.75 * shadowScaling)
 | |
|         imageView.autoSetDimension(.width, toSize: imageSize)
 | |
|         imageView.autoSetDimension(.height, toSize: imageSize)
 | |
| 
 | |
|         return imageView
 | |
|     }
 | |
| 
 | |
|     private func labelFont() -> UIFont {
 | |
|         switch mode {
 | |
|         case .large, .attachmentApproval:
 | |
|             return UIFont.ows_regularFont(withSize: ScaleFromIPhone5To7Plus(18, 24))
 | |
|         case .small:
 | |
|             return UIFont.ows_regularFont(withSize: ScaleFromIPhone5To7Plus(14, 14))
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     private var controlTintColor: UIColor {
 | |
|         switch mode {
 | |
|         case .small, .large:
 | |
|             return UIColor.ows_materialBlue
 | |
|         case .attachmentApproval:
 | |
|             return UIColor.white
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     private func formattedFileExtension() -> String? {
 | |
|         guard let fileExtension = attachment.fileExtension else {
 | |
|             return nil
 | |
|         }
 | |
| 
 | |
|         return String(format: NSLocalizedString("ATTACHMENT_APPROVAL_FILE_EXTENSION_FORMAT",
 | |
|                                                comment: "Format string for file extension label in call interstitial view"),
 | |
|                       fileExtension.uppercased())
 | |
|     }
 | |
| 
 | |
|     public func formattedFileName() -> String? {
 | |
|         guard let sourceFilename = attachment.sourceFilename else {
 | |
|             return nil
 | |
|         }
 | |
|         let filename = sourceFilename.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines)
 | |
|         guard filename.count > 0 else {
 | |
|             return nil
 | |
|         }
 | |
|         return filename
 | |
|     }
 | |
| 
 | |
|     private func createFileNameLabel() -> UIView? {
 | |
|         let filename = formattedFileName() ?? formattedFileExtension()
 | |
| 
 | |
|         guard filename != nil else {
 | |
|             return nil
 | |
|         }
 | |
| 
 | |
|         let label = UILabel()
 | |
|         label.text = filename
 | |
|         label.textColor = controlTintColor
 | |
|         label.font = labelFont()
 | |
|         label.textAlignment = .center
 | |
|         label.lineBreakMode = .byTruncatingMiddle
 | |
|         return label
 | |
|     }
 | |
| 
 | |
|     private func createFileSizeLabel() -> UIView {
 | |
|         let label = UILabel()
 | |
|         let fileSize = attachment.dataLength
 | |
|         label.text = String(format: NSLocalizedString("ATTACHMENT_APPROVAL_FILE_SIZE_FORMAT",
 | |
|                                                      comment: "Format string for file size label in call interstitial view. Embeds: {{file size as 'N mb' or 'N kb'}}."),
 | |
|                             OWSFormat.formatFileSize(UInt(fileSize)))
 | |
| 
 | |
|         label.textColor = controlTintColor
 | |
|         label.font = labelFont()
 | |
|         label.textAlignment = .center
 | |
| 
 | |
|         return label
 | |
|     }
 | |
| 
 | |
|     // MARK: - Event Handlers
 | |
| 
 | |
|     @objc
 | |
|     func audioPlayButtonPressed(sender: UIButton) {
 | |
|         audioPlayer?.togglePlayState()
 | |
|     }
 | |
| 
 | |
|     // MARK: - OWSAudioPlayerDelegate
 | |
| 
 | |
|     public func audioPlaybackState() -> AudioPlaybackState {
 | |
|         return playbackState
 | |
|     }
 | |
| 
 | |
|     public func setAudioPlaybackState(_ value: AudioPlaybackState) {
 | |
|         playbackState = value
 | |
|     }
 | |
| 
 | |
|     private func ensureButtonState() {
 | |
|         if playbackState == .playing {
 | |
|             setAudioIconToPause()
 | |
|         } else {
 | |
|             setAudioIconToPlay()
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     public func setAudioProgress(_ progress: CGFloat, duration: CGFloat) {
 | |
|         audioProgressSeconds = progress
 | |
|         audioDurationSeconds = duration
 | |
|     }
 | |
| 
 | |
|     private func setAudioIconToPlay() {
 | |
|         let image = UIImage(named: "audio_play_black_large")?.withRenderingMode(.alwaysTemplate)
 | |
|         assert(image != nil)
 | |
|         audioPlayButton?.setImage(image, for: .normal)
 | |
|         audioPlayButton?.imageView?.tintColor = controlTintColor
 | |
|     }
 | |
| 
 | |
|     private func setAudioIconToPause() {
 | |
|         let image = UIImage(named: "audio_pause_black_large")?.withRenderingMode(.alwaysTemplate)
 | |
|         assert(image != nil)
 | |
|         audioPlayButton?.setImage(image, for: .normal)
 | |
|         audioPlayButton?.imageView?.tintColor = controlTintColor
 | |
|     }
 | |
| }
 |