//
// C o p y r i g h t ( c ) 2 0 1 9 O p e n W h i s p e r S y s t e m s . A l l r i g h t s r e s e r v e d .
//
import Foundation
import MediaPlayer
import YYImage
import SessionUIKit
@objc
public enum MediaMessageViewMode : UInt {
case large
case small
case attachmentApproval
}
@objc
public class MediaMessageView : UIView , OWSAudioPlayerDelegate {
// MARK: P r o p e r t i e s
@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: I n i t i a l i z e r s
@ available ( * , unavailable , message : " use other constructor instead. " )
required public init ? ( coder aDecoder : NSCoder ) {
notImplemented ( )
}
// C u r r e n t l y w e o n l y u s e o n e m o d e ( A t t a c h m e n t A p p r o v a l ) , s o w e c o u l d s i m p l i f y t h i s c l a s s , b u t i t ' s k i n d
// o f n i c e t h a t i t ' s w r i t t e n i n a f l e x i b l e w a y i n c a s e w e ' d w a n t t o u s e i t e l s e w h e r e a g a i n i n t h e f u t u r e .
@objc
public required init ( attachment : SignalAttachment , mode : MediaMessageViewMode ) {
if attachment . hasError {
owsFailDebug ( attachment . error . debugDescription )
}
self . attachment = attachment
self . mode = mode
super . init ( frame : CGRect . zero )
createViews ( )
}
deinit {
NotificationCenter . default . removeObserver ( self )
}
// MARK: - C r e a t e V i e w s
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 = . trilinear
audioPlayButton . imageView ? . layer . magnificationFilter = . trilinear
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 )
// W e w a n t t o c e n t e r t h e s t a c k V i e w i n i t ' s s u p e r v i e w w h i l e a l s o e n s u r i n g
// i t ' s s u p e r v i e w i s b i g e n o u g h t o c o n t a i n i t .
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 )
// T h i s e m u l a t e s t h e b e h a v i o r o f c o n t e n t M o d e = . s c a l e A s p e c t F i t u s i n g
// i O S a u t o l a y o u t c o n s t r a i n t s .
//
// T h i s a l l o w s C o n v e r s a t i o n I n p u t T o o l b a r t o p l a c e t h e " c a n c e l " b u t t o n
// i n t h e u p p e r - r i g h t h a n d c o r n e r o f t h e p r e v i e w c o n t e n t .
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 = . trilinear
imageView . layer . magnificationFilter = . trilinear
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 = . trilinear
imageView . layer . magnificationFilter = . trilinear
let aspectRatio = image . size . width / image . size . height
addSubviewWithScaleAspectFitLayout ( view : imageView , aspectRatio : aspectRatio )
contentView = imageView
// a t t a c h m e n t a p p r o v a l p r o v i d e s i t ' s o w n p l a y b u t t o n t o k e e p i t
// a t t h e p r o p e r z o o m s c a l e .
if mode != . attachmentApproval {
let videoPlayIcon = UIImage ( named : " CirclePlay " ) !
let videoPlayButton = UIImageView ( image : videoPlayIcon )
self . videoPlayButton = videoPlayButton
videoPlayButton . contentMode = . scaleAspectFit
self . addSubview ( videoPlayButton )
videoPlayButton . autoCenterInSuperview ( )
videoPlayButton . autoSetDimension ( . width , toSize : 72 )
videoPlayButton . autoSetDimension ( . height , toSize : 72 )
}
}
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 )
// W e w a n t t o c e n t e r t h e s t a c k V i e w i n i t ' s s u p e r v i e w w h i l e a l s o e n s u r i n g
// i t ' s s u p e r v i e w i s b i g e n o u g h t o c o n t a i n i t .
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 = . trilinear
imageView . layer . magnificationFilter = . trilinear
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 Colors . accent
case . attachmentApproval :
return Colors . text
}
}
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: - E v e n t H a n d l e r s
@objc
func audioPlayButtonPressed ( sender : UIButton ) {
audioPlayer ? . togglePlayState ( )
}
// MARK: - O W S A u d i o P l a y e r D e l e g a t e
public func audioPlaybackState ( ) -> AudioPlaybackState {
return playbackState
}
public func setAudioPlaybackState ( _ value : AudioPlaybackState ) {
playbackState = value
}
public func showInvalidAudioFileAlert ( ) {
OWSAlerts . showErrorAlert ( message : NSLocalizedString ( " INVALID_AUDIO_FILE_ALERT_ERROR_MESSAGE " , comment : " Message for the alert indicating that an audio file is invalid. " ) )
}
public func audioPlayerDidFinishPlaying ( _ player : OWSAudioPlayer , successfully flag : Bool ) {
// D o n o t h i n g
}
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
}
}