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.
		
		
		
		
		
			
		
			
				
	
	
		
			693 lines
		
	
	
		
			26 KiB
		
	
	
	
		
			Swift
		
	
			
		
		
	
	
			693 lines
		
	
	
		
			26 KiB
		
	
	
	
		
			Swift
		
	
| //
 | |
| //  Copyright (c) 2019 Open Whisper Systems. All rights reserved.
 | |
| //
 | |
| 
 | |
| import Foundation
 | |
| import MediaPlayer
 | |
| import YYImage
 | |
| import NVActivityIndicatorView
 | |
| import SessionUIKit
 | |
| import SessionMessagingKit
 | |
| 
 | |
| public protocol MediaMessageViewAudioDelegate: AnyObject {
 | |
|     func progressChanged(_ progressSeconds: CGFloat, durationSeconds: CGFloat)
 | |
| }
 | |
| 
 | |
| public class MediaMessageView: UIView, OWSAudioPlayerDelegate {
 | |
|     public enum Mode: UInt {
 | |
|         case large
 | |
|         case small
 | |
|         case attachmentApproval
 | |
|     }
 | |
| 
 | |
|     // MARK: Properties
 | |
| 
 | |
|     public let mode: Mode
 | |
|     public let attachment: SignalAttachment
 | |
| 
 | |
|     public lazy var audioPlayer: OWSAudioPlayer? = {
 | |
|         guard let dataUrl = attachment.dataUrl else { return nil }
 | |
|         
 | |
|         return OWSAudioPlayer(mediaUrl: dataUrl, audioBehavior: .playback, delegate: self)
 | |
|     }()
 | |
|     
 | |
|     public var wasPlayingAudio: Bool = false
 | |
|     public var audioProgressSeconds: CGFloat = 0
 | |
|     public var audioDurationSeconds: CGFloat = 0
 | |
|     public weak var audioDelegate: MediaMessageViewAudioDelegate?
 | |
|     
 | |
|     public var playbackState = AudioPlaybackState.stopped {
 | |
|         didSet {
 | |
|             AssertIsOnMainThread()
 | |
| 
 | |
|             ensureButtonState()
 | |
|         }
 | |
|     }
 | |
|     
 | |
|     private lazy var validImage: UIImage? = {
 | |
|         if attachment.isImage {
 | |
|             guard
 | |
|                 attachment.isValidImage,
 | |
|                 let image: UIImage = attachment.image(),
 | |
|                 image.size.width > 0,
 | |
|                 image.size.height > 0
 | |
|             else {
 | |
|                 return nil
 | |
|             }
 | |
|             
 | |
|             return image
 | |
|         }
 | |
|         else if attachment.isVideo {
 | |
|             guard
 | |
|                 attachment.isValidVideo,
 | |
|                 let image: UIImage = attachment.videoPreview(),
 | |
|                 image.size.width > 0,
 | |
|                 image.size.height > 0
 | |
|             else {
 | |
|                 return nil
 | |
|             }
 | |
|             
 | |
|             return image
 | |
|         }
 | |
|         
 | |
|         return nil
 | |
|     }()
 | |
|     private lazy var validAnimatedImage: YYImage? = {
 | |
|         guard
 | |
|             attachment.isAnimatedImage,
 | |
|             attachment.isValidImage,
 | |
|             let dataUrl: URL = attachment.dataUrl,
 | |
|             let image: YYImage = YYImage(contentsOfFile: dataUrl.path),
 | |
|             image.size.width > 0,
 | |
|             image.size.height > 0
 | |
|         else {
 | |
|             return nil
 | |
|         }
 | |
|         
 | |
|         return image
 | |
|     }()
 | |
|     private var linkPreviewInfo: (url: String, draft: LinkPreviewDraft?)?
 | |
| 
 | |
|     // 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.
 | |
|     public required init(attachment: SignalAttachment, mode: MediaMessageView.Mode) {
 | |
|         if attachment.hasError { owsFailDebug(attachment.error.debugDescription) }
 | |
|         
 | |
|         self.attachment = attachment
 | |
|         self.mode = mode
 | |
|         
 | |
|         // Set the linkPreviewUrl if it's a url
 | |
|         if attachment.isUrl, let linkPreviewURL: String = LinkPreview.previewUrl(for: attachment.text()) {
 | |
|             self.linkPreviewInfo = (url: linkPreviewURL, draft: nil)
 | |
|         }
 | |
|         
 | |
|         super.init(frame: CGRect.zero)
 | |
| 
 | |
|         setupViews()
 | |
|         setupLayout()
 | |
|     }
 | |
| 
 | |
|     deinit {
 | |
|         NotificationCenter.default.removeObserver(self)
 | |
|     }
 | |
|     
 | |
|     // MARK: - UI
 | |
|     
 | |
|     private lazy var stackView: UIStackView = {
 | |
|         let stackView: UIStackView = UIStackView()
 | |
|         stackView.translatesAutoresizingMaskIntoConstraints = false
 | |
|         stackView.axis = .vertical
 | |
|         stackView.alignment = .center
 | |
|         stackView.distribution = .fill
 | |
|         
 | |
|         switch mode {
 | |
|             case .attachmentApproval: stackView.spacing = 2
 | |
|             case .large: stackView.spacing = 10
 | |
|             case .small: stackView.spacing = 5
 | |
|         }
 | |
|         
 | |
|         return stackView
 | |
|     }()
 | |
|     
 | |
|     private let loadingView: NVActivityIndicatorView = {
 | |
|         let result: NVActivityIndicatorView = NVActivityIndicatorView(
 | |
|             frame: CGRect.zero,
 | |
|             type: .circleStrokeSpin,
 | |
|             color: .black,
 | |
|             padding: nil
 | |
|         )
 | |
|         result.translatesAutoresizingMaskIntoConstraints = false
 | |
|         result.isHidden = true
 | |
|         
 | |
|         ThemeManager.onThemeChange(observer: result) { [weak result] theme, _ in
 | |
|             guard let textPrimary: UIColor = theme.color(for: .textPrimary) else { return }
 | |
|             
 | |
|             result?.color = textPrimary
 | |
|         }
 | |
|         
 | |
|         return result
 | |
|     }()
 | |
|     
 | |
|     private lazy var imageView: UIImageView = {
 | |
|         let view: UIImageView = UIImageView()
 | |
|         view.translatesAutoresizingMaskIntoConstraints = false
 | |
|         view.contentMode = .scaleAspectFit
 | |
|         view.image = UIImage(named: "FileLarge")?.withRenderingMode(.alwaysTemplate)
 | |
|         view.themeTintColor = .textPrimary
 | |
|         view.isHidden = true
 | |
|         
 | |
|         // Override the image to the correct one
 | |
|         if attachment.isImage || attachment.isVideo {
 | |
|             if let validImage: UIImage = validImage {
 | |
|                 view.layer.minificationFilter = .trilinear
 | |
|                 view.layer.magnificationFilter = .trilinear
 | |
|                 view.image = validImage
 | |
|             }
 | |
|         }
 | |
|         else if attachment.isUrl {
 | |
|             view.clipsToBounds = true
 | |
|             view.image = UIImage(named: "Link")?.withRenderingMode(.alwaysTemplate)
 | |
|             view.themeTintColor = .messageBubble_outgoingText
 | |
|             view.contentMode = .center
 | |
|             view.themeBackgroundColor = .messageBubble_overlay
 | |
|             view.layer.cornerRadius = 8
 | |
|         }
 | |
|         
 | |
|         return view
 | |
|     }()
 | |
|     
 | |
|     private lazy var fileTypeImageView: UIImageView = {
 | |
|         let view: UIImageView = UIImageView()
 | |
|         view.translatesAutoresizingMaskIntoConstraints = false
 | |
|         view.isHidden = true
 | |
|         
 | |
|         return view
 | |
|     }()
 | |
|     
 | |
|     private lazy var animatedImageView: YYAnimatedImageView = {
 | |
|         let view: YYAnimatedImageView = YYAnimatedImageView()
 | |
|         view.translatesAutoresizingMaskIntoConstraints = false
 | |
|         view.isHidden = true
 | |
|         
 | |
|         if let image: YYImage = validAnimatedImage {
 | |
|             view.image = image
 | |
|         }
 | |
|         else {
 | |
|             view.contentMode = .scaleAspectFit
 | |
|             view.image = UIImage(named: "FileLarge")?.withRenderingMode(.alwaysTemplate)
 | |
|             view.themeTintColor = .textPrimary
 | |
|         }
 | |
|         
 | |
|         return view
 | |
|     }()
 | |
|     
 | |
|     lazy var videoPlayButton: UIImageView = {
 | |
|         let view: UIImageView = UIImageView(image: UIImage(named: "CirclePlay"))
 | |
|         view.translatesAutoresizingMaskIntoConstraints = false
 | |
|         view.contentMode = .scaleAspectFit
 | |
|         view.isHidden = true
 | |
|         
 | |
|         return view
 | |
|     }()
 | |
|     
 | |
|     /// Note: This uses different assets from the `videoPlayButton` and has a 'Pause' state
 | |
|     private lazy var audioPlayPauseButton: UIButton = {
 | |
|         let button: UIButton = UIButton()
 | |
|         button.translatesAutoresizingMaskIntoConstraints = false
 | |
|         button.clipsToBounds = true
 | |
|         button.setThemeBackgroundColorForced(
 | |
|             .theme(.classicLight, color: .settings_tabBackground),
 | |
|             for: .normal
 | |
|         )
 | |
|         button.setThemeBackgroundColorForced(
 | |
|             .theme(.classicLight, color: .highlighted(.settings_tabBackground)),
 | |
|             for: .highlighted
 | |
|         )
 | |
|         button.addTarget(self, action: #selector(audioPlayPauseButtonPressed), for: .touchUpInside)
 | |
|         button.isHidden = true
 | |
|         
 | |
|         return button
 | |
|     }()
 | |
|     
 | |
|     private lazy var titleStackView: UIStackView = {
 | |
|         let stackView: UIStackView = UIStackView()
 | |
|         stackView.translatesAutoresizingMaskIntoConstraints = false
 | |
|         stackView.axis = .vertical
 | |
|         stackView.alignment = (attachment.isUrl && linkPreviewInfo?.url != nil ? .leading : .center)
 | |
|         stackView.distribution = .fill
 | |
|         
 | |
|         switch mode {
 | |
|             case .attachmentApproval: stackView.spacing = 2
 | |
|             case .large: stackView.spacing = 10
 | |
|             case .small: stackView.spacing = 5
 | |
|         }
 | |
|         
 | |
|         return stackView
 | |
|     }()
 | |
|     
 | |
|     private lazy var titleLabel: UILabel = {
 | |
|         let label: UILabel = UILabel()
 | |
|         label.translatesAutoresizingMaskIntoConstraints = false
 | |
|         
 | |
|         // Styling
 | |
|         switch mode {
 | |
|             case .attachmentApproval:
 | |
|                 label.font = UIFont.ows_boldFont(withSize: ScaleFromIPhone5To7Plus(16, 22))
 | |
|                 label.themeTextColor = .textPrimary
 | |
|                 
 | |
|             case .large:
 | |
|                 label.font = UIFont.ows_regularFont(withSize: ScaleFromIPhone5To7Plus(18, 24))
 | |
|                 label.themeTextColor = .primary
 | |
|                 
 | |
|             case .small:
 | |
|                 label.font = UIFont.ows_regularFont(withSize: ScaleFromIPhone5To7Plus(14, 14))
 | |
|                 label.themeTextColor = .primary
 | |
|         }
 | |
|         
 | |
|         // Content
 | |
|         if attachment.isUrl {
 | |
|             // If we have no link preview info at this point then assume link previews are disabled
 | |
|             if let linkPreviewURL: String = linkPreviewInfo?.url {
 | |
|                 label.font = .boldSystemFont(ofSize: Values.smallFontSize)
 | |
|                 label.text = linkPreviewURL
 | |
|                 label.textAlignment = .left
 | |
|                 label.lineBreakMode = .byTruncatingTail
 | |
|                 label.numberOfLines = 2
 | |
|             }
 | |
|             else {
 | |
|                 label.text = "vc_share_link_previews_disabled_title".localized()
 | |
|             }
 | |
|         }
 | |
|         // Title for everything except these types
 | |
|         else if !attachment.isImage && !attachment.isAnimatedImage && !attachment.isVideo {
 | |
|             if let fileName: String = attachment.sourceFilename?.trimmingCharacters(in: .whitespacesAndNewlines), fileName.count > 0 {
 | |
|                 label.text = fileName
 | |
|             }
 | |
|             else if let fileExtension: String = attachment.fileExtension {
 | |
|                 label.text = String(
 | |
|                     format: "ATTACHMENT_APPROVAL_FILE_EXTENSION_FORMAT".localized(),
 | |
|                     fileExtension.uppercased()
 | |
|                 )
 | |
|             }
 | |
|             
 | |
|             label.textAlignment = .center
 | |
|             label.lineBreakMode = .byTruncatingMiddle
 | |
|         }
 | |
|         
 | |
|         // Hide the label if it has no content
 | |
|         label.isHidden = ((label.text?.count ?? 0) == 0)
 | |
|         
 | |
|         return label
 | |
|     }()
 | |
|     
 | |
|     private lazy var subtitleLabel: UILabel = {
 | |
|         let label: UILabel = UILabel()
 | |
|         label.translatesAutoresizingMaskIntoConstraints = false
 | |
|         
 | |
|         // Styling
 | |
|         switch mode {
 | |
|             case .attachmentApproval:
 | |
|                 label.font = UIFont.ows_regularFont(withSize: ScaleFromIPhone5To7Plus(12, 18))
 | |
|                 label.themeTextColor = .textSecondary
 | |
|                 
 | |
|             case .large:
 | |
|                 label.font = UIFont.ows_regularFont(withSize: ScaleFromIPhone5To7Plus(18, 24))
 | |
|                 label.themeTextColor = .primary
 | |
|                 
 | |
|             case .small:
 | |
|                 label.font = UIFont.ows_regularFont(withSize: ScaleFromIPhone5To7Plus(14, 14))
 | |
|                 label.themeTextColor = .primary
 | |
|         }
 | |
|         
 | |
|         // Content
 | |
|         if attachment.isUrl {
 | |
|             // We only load Link Previews for HTTPS urls so append an explanation for not
 | |
|             if let linkPreviewURL: String = linkPreviewInfo?.url {
 | |
|                 if let targetUrl: URL = URL(string: linkPreviewURL), targetUrl.scheme?.lowercased() != "https" {
 | |
|                     label.font = UIFont.ows_regularFont(withSize: Values.verySmallFontSize)
 | |
|                     label.text = "vc_share_link_previews_unsecure".localized()
 | |
|                     label.themeTextColor = (mode == .attachmentApproval ?
 | |
|                         .textSecondary :
 | |
|                         .primary
 | |
|                     )
 | |
|                 }
 | |
|             }
 | |
|             // If we have no link preview info at this point then assume link previews are disabled
 | |
|             else {
 | |
|                 label.text = "vc_share_link_previews_disabled_explanation".localized()
 | |
|                 label.themeTextColor = .textPrimary
 | |
|                 label.textAlignment = .center
 | |
|                 label.numberOfLines = 0
 | |
|             }
 | |
|         }
 | |
|         // Subtitle for everything else except these types
 | |
|         else if !attachment.isImage && !attachment.isAnimatedImage && !attachment.isVideo {
 | |
|             // Format string for file size label in call interstitial view.
 | |
|             // Embeds: {{file size as 'N mb' or 'N kb'}}.
 | |
|             let fileSize: UInt = attachment.dataLength
 | |
|             label.text = String(format: "ATTACHMENT_APPROVAL_FILE_SIZE_FORMAT".localized(), OWSFormat.formatFileSize(UInt(fileSize)))
 | |
|             label.textAlignment = .center
 | |
|         }
 | |
|         
 | |
|         // Hide the label if it has no content
 | |
|         label.isHidden = ((label.text?.count ?? 0) == 0)
 | |
|         
 | |
|         return label
 | |
|     }()
 | |
|     
 | |
|     // MARK: - Layout
 | |
| 
 | |
|     private func setupViews() {
 | |
|         // Plain text will just be put in the 'message' input so do nothing
 | |
|         guard !attachment.isText else { return }
 | |
|         
 | |
|         // Setup the view hierarchy
 | |
|         addSubview(stackView)
 | |
|         addSubview(loadingView)
 | |
|         addSubview(videoPlayButton)
 | |
|         
 | |
|         stackView.addArrangedSubview(imageView)
 | |
|         stackView.addArrangedSubview(animatedImageView)
 | |
|         if !titleLabel.isHidden { stackView.addArrangedSubview(UIView.vhSpacer(10, 10)) }
 | |
|         stackView.addArrangedSubview(titleStackView)
 | |
|         
 | |
|         titleStackView.addArrangedSubview(titleLabel)
 | |
|         titleStackView.addArrangedSubview(subtitleLabel)
 | |
|         
 | |
|         imageView.addSubview(fileTypeImageView)
 | |
|         
 | |
|         // Type-specific configurations
 | |
|         if attachment.isAnimatedImage {
 | |
|             animatedImageView.isHidden = false
 | |
|         }
 | |
|         else if attachment.isImage {
 | |
|             imageView.isHidden = false
 | |
|         }
 | |
|         else if attachment.isVideo {
 | |
|             // Note: The 'attachmentApproval' mode provides it's own play button to keep
 | |
|             // it at the proper scale when zooming
 | |
|             imageView.isHidden = false
 | |
|             videoPlayButton.isHidden = (mode == .attachmentApproval)
 | |
|         }
 | |
|         else if attachment.isAudio {
 | |
|             // Hide the 'audioPlayPauseButton' if the 'audioPlayer' failed to get created
 | |
|             imageView.isHidden = false
 | |
|             audioPlayPauseButton.isHidden = (audioPlayer == nil)
 | |
|             setAudioIconToPlay()
 | |
|             setAudioProgress(0, duration: (audioPlayer?.duration ?? 0))
 | |
|             
 | |
|             fileTypeImageView.image = UIImage(named: "table_ic_notification_sound")?
 | |
|                 .withRenderingMode(.alwaysTemplate)
 | |
|             fileTypeImageView.themeTintColor = .textPrimary
 | |
|             fileTypeImageView.isHidden = false
 | |
|             
 | |
|             // Note: There is an annoying bug where the MediaMessageView will fill the screen if the
 | |
|             // 'audioPlayPauseButton' is added anywhere within the view hierarchy causing issues with
 | |
|             // the min scale on 'image' and 'animatedImage' file types (assume it's actually any UIButton)
 | |
|             addSubview(audioPlayPauseButton)
 | |
|         }
 | |
|         else if attachment.isUrl {
 | |
|             imageView.isHidden = false
 | |
|             imageView.alpha = 0 // Not 'isHidden' because we want it to take up space in the UIStackView
 | |
|             loadingView.isHidden = false
 | |
|             
 | |
|             if let linkPreviewUrl: String = linkPreviewInfo?.url {
 | |
|                 // Don't want to change the axis until we have a URL to start loading, otherwise the
 | |
|                 // error message will be broken
 | |
|                 stackView.axis = .horizontal
 | |
|                 
 | |
|                 loadLinkPreview(linkPreviewURL: linkPreviewUrl)
 | |
|             }
 | |
|         }
 | |
|         else {
 | |
|             imageView.isHidden = false
 | |
|         }
 | |
|     }
 | |
|     
 | |
|     private func setupLayout() {
 | |
|         // Plain text will just be put in the 'message' input so do nothing
 | |
|         guard !attachment.isText else { return }
 | |
|         
 | |
|         // Sizing calculations
 | |
|         let clampedRatio: CGFloat = {
 | |
|             if attachment.isUrl {
 | |
|                 return 1
 | |
|             }
 | |
|             
 | |
|             if attachment.isAnimatedImage {
 | |
|                 let imageSize: CGSize = (animatedImageView.image?.size ?? CGSize(width: 1, height: 1))
 | |
|                 let aspectRatio: CGFloat = (imageSize.width / imageSize.height)
 | |
|             
 | |
|                 return CGFloatClamp(aspectRatio, 0.05, 95.0)
 | |
|             }
 | |
|             
 | |
|             // All other types should maintain the ratio of the image in the 'imageView'
 | |
|             let imageSize: CGSize = (imageView.image?.size ?? CGSize(width: 1, height: 1))
 | |
|             let aspectRatio: CGFloat = (imageSize.width / imageSize.height)
 | |
|         
 | |
|             return CGFloatClamp(aspectRatio, 0.05, 95.0)
 | |
|         }()
 | |
|         
 | |
|         let maybeImageSize: CGFloat? = {
 | |
|             if attachment.isImage || attachment.isVideo {
 | |
|                 if validImage != nil { return nil }
 | |
|                 
 | |
|                 // If we don't have a valid image then use the 'generic' case
 | |
|             }
 | |
|             else if attachment.isAnimatedImage {
 | |
|                 if validAnimatedImage != nil { return nil }
 | |
|                 
 | |
|                 // If we don't have a valid image then use the 'generic' case
 | |
|             }
 | |
|             else if attachment.isUrl {
 | |
|                 return 80
 | |
|             }
 | |
|             
 | |
|             // Generic file size
 | |
|             switch mode {
 | |
|                 case .large: return 200
 | |
|                 case .attachmentApproval: return 120
 | |
|                 case .small: return 80
 | |
|             }
 | |
|         }()
 | |
|         
 | |
|         let imageSize: CGFloat = (maybeImageSize ?? 0)
 | |
|         let audioButtonSize: CGFloat = (imageSize / 2.5)
 | |
|         audioPlayPauseButton.layer.cornerRadius = (audioButtonSize / 2)
 | |
|         
 | |
|         // Actual layout
 | |
|         NSLayoutConstraint.activate([
 | |
|             stackView.centerXAnchor.constraint(equalTo: centerXAnchor),
 | |
|             stackView.centerYAnchor.constraint(equalTo: centerYAnchor),
 | |
|             stackView.heightAnchor.constraint(lessThanOrEqualTo: heightAnchor),
 | |
|             
 | |
|             (maybeImageSize != nil ?
 | |
|                 stackView.widthAnchor.constraint(
 | |
|                     equalTo: widthAnchor,
 | |
|                     constant: (attachment.isUrl ? -(32 * 2) : 0)    // Inset stackView for urls
 | |
|                 ) :
 | |
|                 stackView.widthAnchor.constraint(lessThanOrEqualTo: widthAnchor)
 | |
|             ),
 | |
|             
 | |
|             imageView.widthAnchor.constraint(
 | |
|                 equalTo: imageView.heightAnchor,
 | |
|                 multiplier: clampedRatio
 | |
|             ),
 | |
|             animatedImageView.widthAnchor.constraint(
 | |
|                 equalTo: animatedImageView.heightAnchor,
 | |
|                 multiplier: clampedRatio
 | |
|             ),
 | |
|             
 | |
|             // Note: AnimatedImage, Image and Video types should allow zooming so be lessThanOrEqualTo
 | |
|             // the view size but some other types should have specific sizes
 | |
|             animatedImageView.widthAnchor.constraint(lessThanOrEqualTo: widthAnchor),
 | |
|             animatedImageView.heightAnchor.constraint(lessThanOrEqualTo: heightAnchor),
 | |
|             
 | |
|             (maybeImageSize != nil ?
 | |
|                 imageView.widthAnchor.constraint(equalToConstant: imageSize) :
 | |
|                 imageView.widthAnchor.constraint(lessThanOrEqualTo: widthAnchor)
 | |
|             ),
 | |
|             (maybeImageSize != nil ?
 | |
|                 imageView.heightAnchor.constraint(equalToConstant: imageSize) :
 | |
|                 imageView.heightAnchor.constraint(lessThanOrEqualTo: heightAnchor)
 | |
|             ),
 | |
|             
 | |
|             fileTypeImageView.centerXAnchor.constraint(equalTo: imageView.centerXAnchor),
 | |
|             fileTypeImageView.centerYAnchor.constraint(
 | |
|                 equalTo: imageView.centerYAnchor,
 | |
|                 constant: ceil(imageSize * 0.15)
 | |
|             ),
 | |
|             fileTypeImageView.widthAnchor.constraint(
 | |
|                 equalTo: fileTypeImageView.heightAnchor,
 | |
|                 multiplier: ((fileTypeImageView.image?.size.width ?? 1) / (fileTypeImageView.image?.size.height ?? 1))
 | |
|             ),
 | |
|             fileTypeImageView.widthAnchor.constraint(equalTo: imageView.widthAnchor, multiplier: 0.5),
 | |
|             
 | |
|             videoPlayButton.centerXAnchor.constraint(equalTo: centerXAnchor),
 | |
|             videoPlayButton.centerYAnchor.constraint(equalTo: centerYAnchor),
 | |
| 
 | |
|             loadingView.centerXAnchor.constraint(equalTo: imageView.centerXAnchor),
 | |
|             loadingView.centerYAnchor.constraint(equalTo: imageView.centerYAnchor),
 | |
|             loadingView.widthAnchor.constraint(equalToConstant: ceil(imageSize / 3)),
 | |
|             loadingView.heightAnchor.constraint(equalToConstant: ceil(imageSize / 3))
 | |
|         ])
 | |
|         
 | |
|         // No inset for the text for URLs but there is for all other layouts
 | |
|         if !attachment.isUrl {
 | |
|             NSLayoutConstraint.activate([
 | |
|                 titleLabel.widthAnchor.constraint(equalTo: stackView.widthAnchor, constant: -(32 * 2)),
 | |
|                 subtitleLabel.widthAnchor.constraint(equalTo: stackView.widthAnchor, constant: -(32 * 2))
 | |
|             ])
 | |
|         }
 | |
|         
 | |
|         // Note: There is an annoying bug where the MediaMessageView will fill the screen if the
 | |
|         // 'audioPlayPauseButton' is added anywhere within the view hierarchy causing issues with
 | |
|         // the min scale on 'image' and 'animatedImage' file types (assume it's actually any UIButton)
 | |
|         if attachment.isAudio {
 | |
|             NSLayoutConstraint.activate([
 | |
|                 audioPlayPauseButton.centerXAnchor.constraint(equalTo: imageView.centerXAnchor),
 | |
|                 audioPlayPauseButton.centerYAnchor.constraint(equalTo: imageView.centerYAnchor),
 | |
|                 audioPlayPauseButton.widthAnchor.constraint(equalToConstant: audioButtonSize),
 | |
|                 audioPlayPauseButton.heightAnchor.constraint(equalToConstant: audioButtonSize),
 | |
|             ])
 | |
|         }
 | |
|     }
 | |
|     
 | |
|     // MARK: - Link Loading
 | |
|     
 | |
|     private func loadLinkPreview(linkPreviewURL: String) {
 | |
|         loadingView.startAnimating()
 | |
|         
 | |
|         LinkPreview.tryToBuildPreviewInfo(previewUrl: linkPreviewURL)
 | |
|             .done { [weak self] draft in
 | |
|                 // TODO: Look at refactoring this behaviour to consolidate attachment mutations
 | |
|                 self?.attachment.linkPreviewDraft = draft
 | |
|                 self?.linkPreviewInfo = (url: linkPreviewURL, draft: draft)
 | |
|                 
 | |
|                 // Update the UI
 | |
|                 self?.titleLabel.text = (draft.title ?? self?.titleLabel.text)
 | |
|                 self?.loadingView.alpha = 0
 | |
|                 self?.loadingView.stopAnimating()
 | |
|                 self?.imageView.alpha = 1
 | |
|                 
 | |
|                 if let jpegImageData: Data = draft.jpegImageData, let loadedImage: UIImage = UIImage(data: jpegImageData) {
 | |
|                     self?.imageView.image = loadedImage
 | |
|                     self?.imageView.contentMode = .scaleAspectFill
 | |
|                 }
 | |
|             }
 | |
|             .catch { [weak self] _ in
 | |
|                 self?.loadingView.alpha = 0
 | |
|                 self?.loadingView.stopAnimating()
 | |
|                 self?.imageView.alpha = 1
 | |
|                 self?.titleLabel.numberOfLines = 1  // Truncates the URL at 1 line so the error is more readable
 | |
|                 self?.subtitleLabel.isHidden = false
 | |
|                 
 | |
|                 // Set the error text appropriately
 | |
|                 if let targetUrl: URL = URL(string: linkPreviewURL), targetUrl.scheme?.lowercased() != "https" {
 | |
|                     // This error case is handled already in the 'subtitleLabel' creation
 | |
|                 }
 | |
|                 else {
 | |
|                     self?.subtitleLabel.font = UIFont.ows_regularFont(withSize: Values.verySmallFontSize)
 | |
|                     self?.subtitleLabel.text = "vc_share_link_previews_error".localized()
 | |
|                     self?.subtitleLabel.themeTextColor = (self?.mode == .attachmentApproval ?
 | |
|                         .textSecondary :
 | |
|                         .primary
 | |
|                     )
 | |
|                     self?.subtitleLabel.textAlignment = .left
 | |
|                 }
 | |
|             }
 | |
|             .retainUntilComplete()
 | |
|     }
 | |
|     
 | |
|     // MARK: - Functions
 | |
|     
 | |
|     public func playAudio() {
 | |
|         audioPlayer?.play()
 | |
|         ensureButtonState()
 | |
|     }
 | |
|     
 | |
|     public func pauseAudio() {
 | |
|         wasPlayingAudio = (audioPlayer?.isPlaying == true)
 | |
|         
 | |
|         // If the 'audioPlayer' has a duration of 0 then we probably haven't played previously which
 | |
|         // will result in the audioPlayer having a 'duration' of 0 breaking the progressBar. We play
 | |
|         // the audio to get it to properly load the file right before pausing it so the data is
 | |
|         // loaded correctly
 | |
|         if audioPlayer?.duration == 0 {
 | |
|             audioPlayer?.play()
 | |
|         }
 | |
|         
 | |
|         audioPlayer?.pause()
 | |
|         ensureButtonState()
 | |
|     }
 | |
|     
 | |
|     public func setAudioTime(currentTime: TimeInterval) {
 | |
|         audioPlayer?.setCurrentTime(currentTime)
 | |
|     }
 | |
| 
 | |
|     // MARK: - Event Handlers
 | |
| 
 | |
|     @objc func audioPlayPauseButtonPressed(sender: UIButton) {
 | |
|         audioPlayer?.togglePlayState()
 | |
|     }
 | |
| 
 | |
|     // MARK: - OWSAudioPlayerDelegate
 | |
| 
 | |
|     public func audioPlaybackState() -> AudioPlaybackState {
 | |
|         return playbackState
 | |
|     }
 | |
| 
 | |
|     public func setAudioPlaybackState(_ value: AudioPlaybackState) {
 | |
|         playbackState = value
 | |
|     }
 | |
|     
 | |
|     public func showInvalidAudioFileAlert() {
 | |
|         let modal: ConfirmationModal = ConfirmationModal(
 | |
|             targetView: CurrentAppContext().frontmostViewController()?.view,
 | |
|             info: ConfirmationModal.Info(
 | |
|                 title: CommonStrings.errorAlertTitle,
 | |
|                 explanation: "INVALID_AUDIO_FILE_ALERT_ERROR_MESSAGE".localized(),
 | |
|                 cancelTitle: "BUTTON_OK".localized(),
 | |
|                 cancelStyle: .alert_text
 | |
|             )
 | |
|         )
 | |
|         CurrentAppContext().frontmostViewController()?.present(modal, animated: true)
 | |
|     }
 | |
| 
 | |
|     public func audioPlayerDidFinishPlaying(_ player: OWSAudioPlayer, successfully flag: Bool) {
 | |
|         // Do nothing
 | |
|     }
 | |
| 
 | |
|     private func ensureButtonState() {
 | |
|         switch playbackState {
 | |
|             case .playing: setAudioIconToPause()
 | |
|             default: setAudioIconToPlay()
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     public func setAudioProgress(_ progress: CGFloat, duration: CGFloat) {
 | |
|         // Note: When the OWSAudioPlayer stops it sets the duration to 0 (which we want to ignore so
 | |
|         // the UI doesn't look buggy)
 | |
|         let finalDuration: CGFloat = (duration > 0 ? duration : audioDurationSeconds)
 | |
|         audioProgressSeconds = progress
 | |
|         audioDurationSeconds = finalDuration
 | |
|         
 | |
|         audioDelegate?.progressChanged(progress, durationSeconds: finalDuration)
 | |
|     }
 | |
| 
 | |
|     private func setAudioIconToPlay() {
 | |
|         audioPlayPauseButton.setImage(UIImage(named: "Play"), for: .normal)
 | |
|     }
 | |
| 
 | |
|     private func setAudioIconToPause() {
 | |
|         audioPlayPauseButton.setImage(UIImage(named: "Pause"), for: .normal)
 | |
|     }
 | |
| }
 |