2017-09-19 16:36:23 +02:00
//
2019-03-30 14:24:40 +01:00
// 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 .
2017-09-19 16:36:23 +02:00
//
import Foundation
import MediaPlayer
2017-11-28 00:17:46 +01:00
import YYImage
2020-11-11 01:50:01 +01:00
2020-11-09 06:03:59 +01:00
import SessionUIKit
2017-09-19 16:36:23 +02:00
2017-10-24 18:58:24 +02:00
@objc
2017-12-04 23:17:44 +01:00
public enum MediaMessageViewMode : UInt {
2017-10-24 18:58:24 +02:00
case large
case small
2017-12-08 20:13:52 +01:00
case attachmentApproval
2017-10-24 18:58:24 +02:00
}
2017-12-04 23:17:44 +01:00
@objc
2018-02-23 21:44:46 +01:00
public class MediaMessageView : UIView , OWSAudioPlayerDelegate {
2017-09-19 16:36:23 +02:00
// MARK: P r o p e r t i e s
2017-12-04 23:17:44 +01:00
@objc
public let mode : MediaMessageViewMode
@objc
public let attachment : SignalAttachment
@objc
2018-02-23 21:44:46 +01:00
public var audioPlayer : OWSAudioPlayer ?
2017-09-19 16:36:23 +02:00
2017-12-04 23:17:44 +01:00
@objc
public var audioPlayButton : UIButton ?
2017-09-19 16:36:23 +02:00
2017-12-08 20:13:52 +01:00
@objc
public var videoPlayButton : UIImageView ?
2017-12-04 23:17:44 +01:00
@objc
public var playbackState = AudioPlaybackState . stopped {
2017-10-10 22:13:54 +02:00
didSet {
2018-08-22 19:44:22 +02:00
AssertIsOnMainThread ( )
2017-10-10 22:13:54 +02:00
ensureButtonState ( )
}
}
2017-12-04 23:17:44 +01:00
@objc
public var audioProgressSeconds : CGFloat = 0
2017-09-19 16:36:23 +02:00
2017-12-04 23:17:44 +01:00
@objc
public var audioDurationSeconds : CGFloat = 0
@objc
public var contentView : UIView ?
2018-01-16 21:27:53 +01:00
2017-09-19 16:36:23 +02:00
// MARK: I n i t i a l i z e r s
2017-10-24 23:16:44 +02:00
@ available ( * , unavailable , message : " use other constructor instead. " )
2017-12-04 23:17:44 +01:00
required public init ? ( coder aDecoder : NSCoder ) {
2018-08-27 16:21:03 +02:00
notImplemented ( )
2017-09-19 16:36:23 +02:00
}
2018-04-11 17:50:30 +02:00
// 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 .
2018-04-11 17:31:34 +02:00
@objc
2018-04-11 17:50:30 +02:00
public required init ( attachment : SignalAttachment , mode : MediaMessageViewMode ) {
2020-09-23 02:18:41 +02:00
if attachment . hasError {
owsFailDebug ( attachment . error . debugDescription )
}
2017-09-19 16:36:23 +02:00
self . attachment = attachment
2018-01-07 23:35:52 +01:00
self . mode = mode
2017-09-19 16:36:23 +02:00
super . init ( frame : CGRect . zero )
createViews ( )
}
2017-10-23 16:18:36 +02:00
deinit {
NotificationCenter . default . removeObserver ( self )
}
2017-09-19 16:36:23 +02:00
// 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 {
2017-10-16 17:55:19 +02:00
subview . autoPinEdge ( toSuperviewEdge : . top )
2017-09-19 16:36:23 +02:00
} else {
2017-10-24 18:58:24 +02:00
subview . autoPinEdge ( . top , to : . bottom , of : lastView ! , withOffset : stackSpacing ( ) )
2017-09-19 16:36:23 +02:00
}
lastView = subview
}
2017-10-16 17:55:19 +02:00
lastView ? . autoPinEdge ( toSuperviewEdge : . bottom )
2017-09-19 16:36:23 +02:00
return stackView
}
2017-10-24 18:58:24 +02:00
private func stackSpacing ( ) -> CGFloat {
switch mode {
2017-12-08 20:13:52 +01:00
case . large , . attachmentApproval :
2017-10-24 18:58:24 +02:00
return CGFloat ( 10 )
case . small :
return CGFloat ( 5 )
}
}
2017-09-19 16:36:23 +02:00
private func createAudioPreview ( ) {
guard let dataUrl = attachment . dataUrl else {
createGenericPreview ( )
return
}
2018-10-23 16:40:09 +02:00
audioPlayer = OWSAudioPlayer ( mediaUrl : dataUrl , audioBehavior : . playback , delegate : self )
2017-09-19 16:36:23 +02:00
var subviews = [ UIView ] ( )
let audioPlayButton = UIButton ( )
self . audioPlayButton = audioPlayButton
setAudioIconToPlay ( )
2019-03-30 15:11:08 +01:00
audioPlayButton . imageView ? . layer . minificationFilter = . trilinear
audioPlayButton . imageView ? . layer . magnificationFilter = . trilinear
2017-10-16 17:55:19 +02:00
audioPlayButton . addTarget ( self , action : #selector ( audioPlayButtonPressed ) , for : . touchUpInside )
2017-09-19 16:36:23 +02:00
let buttonSize = createHeroViewSize ( )
2017-10-16 17:55:19 +02:00
audioPlayButton . autoSetDimension ( . width , toSize : buttonSize )
audioPlayButton . autoSetDimension ( . height , toSize : buttonSize )
2017-09-19 16:36:23 +02:00
subviews . append ( audioPlayButton )
let fileNameLabel = createFileNameLabel ( )
if let fileNameLabel = fileNameLabel {
subviews . append ( fileNameLabel )
}
let fileSizeLabel = createFileSizeLabel ( )
subviews . append ( fileSizeLabel )
2017-10-16 17:55:19 +02:00
let stackView = wrapViewsInVerticalStack ( subviews : subviews )
2017-09-19 16:36:23 +02:00
self . addSubview ( stackView )
fileNameLabel ? . autoPinWidthToSuperview ( withMargin : 32 )
2017-12-20 00:59:34 +01:00
2017-12-20 04:26:27 +01:00
// 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
2017-12-20 00:59:34 +01:00
// 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 .
2017-09-19 16:36:23 +02:00
stackView . autoPinWidthToSuperview ( )
stackView . autoVCenterInSuperview ( )
2018-05-25 18:54:25 +02:00
NSLayoutConstraint . autoSetPriority ( UILayoutPriority . defaultLow ) {
2017-12-20 00:59:34 +01:00
stackView . autoPinHeightToSuperview ( )
}
stackView . autoPinEdge ( toSuperviewEdge : . top , withInset : 0 , relation : . greaterThanOrEqual )
stackView . autoPinEdge ( toSuperviewEdge : . bottom , withInset : 0 , relation : . greaterThanOrEqual )
2017-09-19 16:36:23 +02:00
}
private func createAnimatedPreview ( ) {
guard attachment . isValidImage else {
2017-09-30 03:54:06 +02:00
createGenericPreview ( )
return
}
guard let dataUrl = attachment . dataUrl else {
createGenericPreview ( )
2017-09-19 16:36:23 +02:00
return
}
2017-10-16 17:55:19 +02:00
guard let image = YYImage ( contentsOfFile : dataUrl . path ) else {
2017-09-19 16:36:23 +02:00
createGenericPreview ( )
return
}
2017-10-23 16:18:36 +02:00
guard image . size . width > 0 && image . size . height > 0 else {
createGenericPreview ( )
return
}
2017-09-30 03:54:06 +02:00
let animatedImageView = YYAnimatedImageView ( )
animatedImageView . image = image
2017-10-23 16:18:36 +02:00
let aspectRatio = image . size . width / image . size . height
2018-02-23 21:44:46 +01:00
addSubviewWithScaleAspectFitLayout ( view : animatedImageView , aspectRatio : aspectRatio )
2017-10-23 16:18:36 +02:00
contentView = animatedImageView
}
private func addSubviewWithScaleAspectFitLayout ( view : UIView , aspectRatio : CGFloat ) {
self . addSubview ( view )
2017-10-24 15:41:03 +02:00
// 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 .
2017-10-23 16:18:36 +02:00
view . autoCenterInSuperview ( )
2018-02-23 21:44:46 +01:00
view . autoPin ( toAspectRatio : aspectRatio )
2017-10-23 16:18:36 +02:00
view . autoMatch ( . width , to : . width , of : self , withMultiplier : 1.0 , relation : . lessThanOrEqual )
view . autoMatch ( . height , to : . height , of : self , withMultiplier : 1.0 , relation : . lessThanOrEqual )
2017-09-19 16:36:23 +02:00
}
private func createImagePreview ( ) {
2018-08-31 02:59:26 +02:00
guard attachment . isValidImage else {
createGenericPreview ( )
return
}
2017-10-23 16:18:36 +02:00
guard let image = attachment . image ( ) else {
createGenericPreview ( )
return
2017-09-19 16:36:23 +02:00
}
2017-10-23 16:18:36 +02:00
guard image . size . width > 0 && image . size . height > 0 else {
2017-09-19 16:36:23 +02:00
createGenericPreview ( )
return
}
2017-10-16 17:55:19 +02:00
let imageView = UIImageView ( image : image )
2019-03-30 15:11:08 +01:00
imageView . layer . minificationFilter = . trilinear
imageView . layer . magnificationFilter = . trilinear
2017-10-23 16:18:36 +02:00
let aspectRatio = image . size . width / image . size . height
2018-02-23 21:44:46 +01:00
addSubviewWithScaleAspectFitLayout ( view : imageView , aspectRatio : aspectRatio )
2017-10-23 16:18:36 +02:00
contentView = imageView
2017-10-24 23:16:44 +02:00
}
2017-09-19 16:36:23 +02:00
private func createVideoPreview ( ) {
2018-08-31 02:59:26 +02:00
guard attachment . isValidVideo else {
createGenericPreview ( )
return
}
2017-10-23 16:18:36 +02:00
guard let image = attachment . videoPreview ( ) else {
2017-09-19 16:36:23 +02:00
createGenericPreview ( )
return
}
2017-10-23 16:18:36 +02:00
guard image . size . width > 0 && image . size . height > 0 else {
2017-09-19 16:36:23 +02:00
createGenericPreview ( )
return
}
2017-10-23 16:18:36 +02:00
let imageView = UIImageView ( image : image )
2019-03-30 15:11:08 +01:00
imageView . layer . minificationFilter = . trilinear
imageView . layer . magnificationFilter = . trilinear
2017-10-23 16:18:36 +02:00
let aspectRatio = image . size . width / image . size . height
2018-02-23 21:44:46 +01:00
addSubviewWithScaleAspectFitLayout ( view : imageView , aspectRatio : aspectRatio )
2017-10-23 16:18:36 +02:00
contentView = imageView
2017-09-19 16:36:23 +02:00
2017-12-08 20:13:52 +01:00
// 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 {
2019-12-12 04:26:23 +01:00
let videoPlayIcon = UIImage ( named : " CirclePlay " ) !
2017-12-08 20:13:52 +01:00
let videoPlayButton = UIImageView ( image : videoPlayIcon )
self . videoPlayButton = videoPlayButton
videoPlayButton . contentMode = . scaleAspectFit
self . addSubview ( videoPlayButton )
videoPlayButton . autoCenterInSuperview ( )
2019-12-12 04:26:23 +01:00
videoPlayButton . autoSetDimension ( . width , toSize : 72 )
videoPlayButton . autoSetDimension ( . height , toSize : 72 )
2017-12-08 20:13:52 +01:00
}
2017-09-19 16:36:23 +02:00
}
private func createGenericPreview ( ) {
var subviews = [ UIView ] ( )
2021-12-07 05:53:58 +01:00
let imageView = createHeroImageView ( imageName : " FileLarge " )
imageView . contentMode = . center
2017-09-19 16:36:23 +02:00
subviews . append ( imageView )
let fileNameLabel = createFileNameLabel ( )
if let fileNameLabel = fileNameLabel {
subviews . append ( fileNameLabel )
}
let fileSizeLabel = createFileSizeLabel ( )
subviews . append ( fileSizeLabel )
2017-10-16 17:55:19 +02:00
let stackView = wrapViewsInVerticalStack ( subviews : subviews )
2017-09-19 16:36:23 +02:00
self . addSubview ( stackView )
fileNameLabel ? . autoPinWidthToSuperview ( withMargin : 32 )
2017-12-20 00:59:34 +01:00
2017-12-20 04:26:27 +01:00
// 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
2017-12-20 00:59:34 +01:00
// 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 .
2017-09-19 16:36:23 +02:00
stackView . autoPinWidthToSuperview ( )
stackView . autoVCenterInSuperview ( )
2018-05-25 18:54:25 +02:00
NSLayoutConstraint . autoSetPriority ( UILayoutPriority . defaultLow ) {
2017-12-20 00:59:34 +01:00
stackView . autoPinHeightToSuperview ( )
}
stackView . autoPinEdge ( toSuperviewEdge : . top , withInset : 0 , relation : . greaterThanOrEqual )
stackView . autoPinEdge ( toSuperviewEdge : . bottom , withInset : 0 , relation : . greaterThanOrEqual )
2017-09-19 16:36:23 +02:00
}
private func createHeroViewSize ( ) -> CGFloat {
2017-10-24 18:58:24 +02:00
switch mode {
2017-12-12 15:56:43 +01:00
case . large :
2017-10-24 18:58:24 +02:00
return ScaleFromIPhone5To7Plus ( 175 , 225 )
2017-12-12 15:56:43 +01:00
case . attachmentApproval :
return ScaleFromIPhone5 ( 100 )
2017-10-24 18:58:24 +02:00
case . small :
return ScaleFromIPhone5To7Plus ( 80 , 80 )
}
2017-09-19 16:36:23 +02:00
}
private func createHeroImageView ( imageName : String ) -> UIView {
let imageSize = createHeroViewSize ( )
2017-12-04 23:17:44 +01:00
2017-12-06 19:44:08 +01:00
let image = UIImage ( named : imageName )
2017-09-19 16:36:23 +02:00
assert ( image != nil )
2017-10-16 17:55:19 +02:00
let imageView = UIImageView ( image : image )
2019-03-30 15:11:08 +01:00
imageView . layer . minificationFilter = . trilinear
imageView . layer . magnificationFilter = . trilinear
2017-09-19 16:36:23 +02:00
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 )
2017-10-16 17:55:19 +02:00
imageView . autoSetDimension ( . width , toSize : imageSize )
imageView . autoSetDimension ( . height , toSize : imageSize )
2017-09-19 16:36:23 +02:00
return imageView
}
private func labelFont ( ) -> UIFont {
2017-10-24 18:58:24 +02:00
switch mode {
2017-12-08 20:13:52 +01:00
case . large , . attachmentApproval :
2017-10-24 18:58:24 +02:00
return UIFont . ows_regularFont ( withSize : ScaleFromIPhone5To7Plus ( 18 , 24 ) )
case . small :
return UIFont . ows_regularFont ( withSize : ScaleFromIPhone5To7Plus ( 14 , 14 ) )
}
2017-09-19 16:36:23 +02:00
}
2017-12-12 15:56:43 +01:00
private var controlTintColor : UIColor {
switch mode {
case . small , . large :
2020-03-17 06:18:53 +01:00
return Colors . accent
2017-12-12 15:56:43 +01:00
case . attachmentApproval :
2020-09-23 02:18:41 +02:00
return Colors . text
2017-12-12 15:56:43 +01:00
}
}
2017-09-19 16:36:23 +02:00
private func formattedFileExtension ( ) -> String ? {
guard let fileExtension = attachment . fileExtension else {
return nil
}
2017-10-16 17:55:19 +02:00
return String ( format : NSLocalizedString ( " ATTACHMENT_APPROVAL_FILE_EXTENSION_FORMAT " ,
2017-09-19 16:36:23 +02:00
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 )
2017-12-04 16:35:47 +01:00
guard filename . count > 0 else {
2017-09-19 16:36:23 +02:00
return nil
}
return filename
}
private func createFileNameLabel ( ) -> UIView ? {
let filename = formattedFileName ( ) ? ? formattedFileExtension ( )
guard filename != nil else {
return nil
}
let label = UILabel ( )
label . text = filename
2017-12-12 15:56:43 +01:00
label . textColor = controlTintColor
2017-09-19 16:36:23 +02:00
label . font = labelFont ( )
label . textAlignment = . center
label . lineBreakMode = . byTruncatingMiddle
return label
}
private func createFileSizeLabel ( ) -> UIView {
let label = UILabel ( )
let fileSize = attachment . dataLength
2017-10-16 17:55:19 +02:00
label . text = String ( format : NSLocalizedString ( " ATTACHMENT_APPROVAL_FILE_SIZE_FORMAT " ,
2017-09-19 16:36:23 +02:00
comment : " Format string for file size label in call interstitial view. Embeds: {{file size as 'N mb' or 'N kb'}}. " ) ,
2017-12-04 16:35:47 +01:00
OWSFormat . formatFileSize ( UInt ( fileSize ) ) )
2017-09-19 16:36:23 +02:00
2017-12-12 15:56:43 +01:00
label . textColor = controlTintColor
2017-09-19 16:36:23 +02:00
label . font = labelFont ( )
label . textAlignment = . center
return label
}
// MARK: - E v e n t H a n d l e r s
2017-12-04 23:17:44 +01:00
@objc
2017-09-19 16:36:23 +02:00
func audioPlayButtonPressed ( sender : UIButton ) {
2018-10-23 16:40:09 +02:00
audioPlayer ? . togglePlayState ( )
2017-09-19 16:36:23 +02:00
}
2018-02-23 21:44:46 +01:00
// MARK: - O W S A u d i o P l a y e r D e l e g a t e
2017-09-19 16:36:23 +02:00
2017-10-10 22:13:54 +02:00
public func audioPlaybackState ( ) -> AudioPlaybackState {
return playbackState
2017-09-19 16:36:23 +02:00
}
2017-10-10 22:13:54 +02:00
public func setAudioPlaybackState ( _ value : AudioPlaybackState ) {
playbackState = value
2017-09-19 16:36:23 +02:00
}
2020-11-26 00:37:56 +01:00
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. " ) )
}
2017-09-19 16:36:23 +02:00
2021-01-29 01:46:32 +01:00
public func audioPlayerDidFinishPlaying ( _ player : OWSAudioPlayer , successfully flag : Bool ) {
// D o n o t h i n g
}
2017-10-10 22:13:54 +02:00
private func ensureButtonState ( ) {
if playbackState = = . playing {
setAudioIconToPause ( )
} else {
setAudioIconToPlay ( )
}
2017-09-19 16:36:23 +02:00
}
public func setAudioProgress ( _ progress : CGFloat , duration : CGFloat ) {
audioProgressSeconds = progress
audioDurationSeconds = duration
}
2017-10-10 22:13:54 +02:00
private func setAudioIconToPlay ( ) {
2017-10-16 17:55:19 +02:00
let image = UIImage ( named : " audio_play_black_large " ) ? . withRenderingMode ( . alwaysTemplate )
2017-09-19 16:36:23 +02:00
assert ( image != nil )
2017-10-16 17:55:19 +02:00
audioPlayButton ? . setImage ( image , for : . normal )
2017-12-12 15:56:43 +01:00
audioPlayButton ? . imageView ? . tintColor = controlTintColor
2017-09-19 16:36:23 +02:00
}
2017-10-10 22:13:54 +02:00
private func setAudioIconToPause ( ) {
2017-10-16 17:55:19 +02:00
let image = UIImage ( named : " audio_pause_black_large " ) ? . withRenderingMode ( . alwaysTemplate )
2017-09-19 16:36:23 +02:00
assert ( image != nil )
2017-10-16 17:55:19 +02:00
audioPlayButton ? . setImage ( image , for : . normal )
2017-12-12 15:56:43 +01:00
audioPlayButton ? . imageView ? . tintColor = controlTintColor
2017-09-19 16:36:23 +02:00
}
2017-12-04 23:17:44 +01:00
}