//
// C o p y r i g h t ( c ) 2 0 1 8 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 UIKit
// O b j c w r a p p e r f o r t h e M e d i a G a l l e r y I t e m s t r u c t
@objc
public class GalleryItemBox : NSObject {
public let value : MediaGalleryItem
init ( _ value : MediaGalleryItem ) {
self . value = value
}
@objc
public var attachmentStream : TSAttachmentStream {
return value . attachmentStream
}
}
private class Box < A > {
var value : A
init ( _ val : A ) {
self . value = val
}
}
fileprivate extension MediaDetailViewController {
fileprivate var galleryItem : MediaGalleryItem {
return self . galleryItemBox . value
}
}
class MediaPageViewController : UIPageViewController , UIPageViewControllerDataSource , UIPageViewControllerDelegate , MediaDetailViewControllerDelegate , MediaGalleryDataSourceDelegate {
private weak var mediaGalleryDataSource : MediaGalleryDataSource ?
private var cachedPages : [ MediaGalleryItem : MediaDetailViewController ] = [ : ]
private var initialPage : MediaDetailViewController !
public var currentViewController : MediaDetailViewController {
return viewControllers ! . first as ! MediaDetailViewController
}
public var currentItem : MediaGalleryItem ! {
get {
return currentViewController . galleryItemBox . value
}
set {
setCurrentItem ( newValue , direction : . forward , animated : false )
}
}
private func setCurrentItem ( _ item : MediaGalleryItem , direction : UIPageViewControllerNavigationDirection , animated isAnimated : Bool ) {
guard let galleryPage = self . buildGalleryPage ( galleryItem : item ) else {
owsFailDebug ( " unexpetedly unable to build new gallery page " )
return
}
self . updateTitle ( item : item )
self . setViewControllers ( [ galleryPage ] , direction : direction , animated : isAnimated )
self . updateFooterBarButtonItems ( isPlayingVideo : false )
}
private let uiDatabaseConnection : YapDatabaseConnection
private let showAllMediaButton : Bool
private let sliderEnabled : Bool
init ( initialItem : MediaGalleryItem , mediaGalleryDataSource : MediaGalleryDataSource , uiDatabaseConnection : YapDatabaseConnection , options : MediaGalleryOption ) {
assert ( uiDatabaseConnection . isInLongLivedReadTransaction ( ) )
self . uiDatabaseConnection = uiDatabaseConnection
self . showAllMediaButton = options . contains ( . showAllMediaButton )
self . sliderEnabled = options . contains ( . sliderEnabled )
self . mediaGalleryDataSource = mediaGalleryDataSource
let kSpacingBetweenItems : CGFloat = 20
super . init ( transitionStyle : . scroll ,
navigationOrientation : . horizontal ,
options : [ UIPageViewControllerOptionInterPageSpacingKey : kSpacingBetweenItems ] )
self . dataSource = self
self . delegate = self
guard let initialPage = self . buildGalleryPage ( galleryItem : initialItem ) else {
owsFailDebug ( " unexpetedly unable to build initial gallery item " )
return
}
self . initialPage = initialPage
self . setViewControllers ( [ initialPage ] , direction : . forward , animated : false , completion : nil )
}
@ available ( * , unavailable , message : " Unimplemented " )
required init ? ( coder : NSCoder ) {
notImplemented ( )
}
deinit {
Logger . debug ( " deinit " )
}
var bottomContainer : UIView !
var footerBar : UIToolbar !
var videoPlayBarButton : UIBarButtonItem !
var videoPauseBarButton : UIBarButtonItem !
var pagerScrollView : UIScrollView !
// MARK: C a p t i o n
var currentCaptionView : CaptionView !
var pendingCaptionView : CaptionView !
override func viewDidLoad ( ) {
super . viewDidLoad ( )
// N a v i g a t i o n
// N o t e : u s i n g a c u s t o m l e f t B a r B u t t o n I t e m b r e a k s t h e i n t e r a c t i v e p o p g e s t u r e , b u t w e d o n ' t w a n t t o b e a b l e
// t o s w i p e t o g o b a c k i n t h e p a g e r v i e w a n y w a y , i n s t e a d s w i p i n g b a c k s h o u l d s h o w t h e n e x t p a g e .
let backButton = OWSViewController . createOWSBackButton ( withTarget : self , selector : #selector ( didPressDismissButton ) )
self . navigationItem . leftBarButtonItem = backButton
self . navigationItem . titleView = portraitHeaderView
self . updateTitle ( )
if showAllMediaButton {
self . navigationItem . rightBarButtonItem = UIBarButtonItem ( title : MediaStrings . allMedia , style : . plain , target : self , action : #selector ( didPressAllMediaButton ) )
}
// E v e n t h o u g h b a r s a r e o p a q u e , w e w a n t c o n t e n t t o b e l a y e d o u t b e h i n d t h e m .
// T h e b a r s m i g h t o b s c u r e p a r t o f t h e c o n t e n t , b u t t h e y c a n e a s i l y b e h i d d e n b y t a p p i n g
// T h e a l t e r n a t i v e w o u l d b e t h a t c o n t e n t w o u l d s h i f t w h e n t h e n a v b a r s h i d e .
self . extendedLayoutIncludesOpaqueBars = true
self . automaticallyAdjustsScrollViewInsets = false
// G e t r e f e r e n c e t o p a g e d c o n t e n t w h i c h l i v e s i n a s c r o l l V i e w c r e a t e d b y t h e s u p e r c l a s s
// W e s h o w / h i d e t h i s c o n t e n t d u r i n g p r e s e n t a t i o n
for view in self . view . subviews {
if let pagerScrollView = view as ? UIScrollView {
self . pagerScrollView = pagerScrollView
}
}
// H a c k t o a v o i d " p a g e " b o u n c i n g w h e n n o t i n g a l l e r y v i e w .
// e . g . w h e n g e t t i n g t o m e d i a d e t a i l s v i a m e s s a g e d e t a i l s s c r e e n , t h e r e ' s o n l y
// o n e " P a g e " s o t h e b o u n c e d o e s n ' t m a k e s e n s e .
pagerScrollView . isScrollEnabled = sliderEnabled
pagerScrollViewContentOffsetObservation = pagerScrollView . observe ( \ . contentOffset , options : [ . new ] ) { [ weak self ] object , change in
guard let strongSelf = self else { return }
strongSelf . pagerScrollView ( strongSelf . pagerScrollView , contentOffsetDidChange : change )
}
// V i e w s
let kFooterHeight : CGFloat = 44
view . backgroundColor = Theme . backgroundColor
let footerBar = UIToolbar ( )
self . footerBar = footerBar
let captionViewsContainer = UIView ( )
let kMaxCaptionHeight : CGFloat = ScaleFromIPhone5 ( 300 )
captionViewsContainer . autoSetDimension ( . height , toSize : kMaxCaptionHeight , relation : . lessThanOrEqual )
captionViewsContainer . setContentHuggingHigh ( )
captionViewsContainer . setCompressionResistanceHigh ( )
let currentCaptionView = CaptionView ( )
self . currentCaptionView = currentCaptionView
captionViewsContainer . addSubview ( currentCaptionView )
currentCaptionView . autoPinEdgesToSuperviewEdges ( with : . zero , excludingEdge : . top )
currentCaptionView . autoPinEdge ( toSuperviewEdge : . top , withInset : 0 , relation : . greaterThanOrEqual )
currentCaptionView . setContentHuggingHigh ( )
currentCaptionView . setCompressionResistanceHigh ( )
currentCaptionView . text = currentItem . caption
let pendingCaptionView = CaptionView ( )
self . pendingCaptionView = pendingCaptionView
pendingCaptionView . alpha = 0
captionViewsContainer . addSubview ( pendingCaptionView )
pendingCaptionView . autoPinEdgesToSuperviewEdges ( with : . zero , excludingEdge : . top )
pendingCaptionView . autoPinEdge ( toSuperviewEdge : . top , withInset : 0 , relation : . greaterThanOrEqual )
pendingCaptionView . setContentHuggingHigh ( )
pendingCaptionView . setCompressionResistanceHigh ( )
let bottomContainer = UIView ( )
self . bottomContainer = bottomContainer
let bottomStack = UIStackView ( arrangedSubviews : [ captionViewsContainer , footerBar ] )
bottomStack . axis = . vertical
bottomContainer . addSubview ( bottomStack )
bottomStack . autoPinEdgesToSuperviewEdges ( )
self . videoPlayBarButton = UIBarButtonItem ( barButtonSystemItem : . play , target : self , action : #selector ( didPressPlayBarButton ) )
self . videoPauseBarButton = UIBarButtonItem ( barButtonSystemItem : . pause , target : self , action : #selector ( didPressPauseBarButton ) )
self . updateFooterBarButtonItems ( isPlayingVideo : true )
self . view . addSubview ( bottomContainer )
bottomContainer . autoPinWidthToSuperview ( )
bottomContainer . autoPinEdge ( toSuperviewEdge : . bottom )
footerBar . autoPin ( toBottomLayoutGuideOf : self , withInset : 0 )
footerBar . autoSetDimension ( . height , toSize : kFooterHeight )
// G e s t u r e s
let verticalSwipe = UISwipeGestureRecognizer ( target : self , action : #selector ( didSwipeView ) )
verticalSwipe . direction = [ . up , . down ]
view . addGestureRecognizer ( verticalSwipe )
}
// MARK: K V O
var pagerScrollViewContentOffsetObservation : NSKeyValueObservation ?
func pagerScrollView ( _ pagerScrollView : UIScrollView , contentOffsetDidChange change : NSKeyValueObservedChange < CGPoint > ) {
guard let newValue = change . newValue else {
owsFailDebug ( " newValue was unexpectedly nil " )
return
}
let width = pagerScrollView . frame . size . width
guard width > 0 else {
return
}
let ratioComplete = abs ( ( newValue . x - width ) / width )
updatePagerTransition ( ratioComplete : ratioComplete )
}
func updatePagerTransition ( ratioComplete : CGFloat ) {
if currentCaptionView . text != nil {
currentCaptionView . alpha = 1 - ratioComplete
} else {
currentCaptionView . alpha = 0
}
if pendingCaptionView . text != nil {
pendingCaptionView . alpha = ratioComplete
} else {
pendingCaptionView . alpha = 0
}
}
override func viewWillTransition ( to size : CGSize , with coordinator : UIViewControllerTransitionCoordinator ) {
super . viewWillTransition ( to : size , with : coordinator )
let isLandscape = size . width > size . height
self . navigationItem . titleView = isLandscape ? nil : self . portraitHeaderView
}
override func didReceiveMemoryWarning ( ) {
Logger . info ( " " )
super . didReceiveMemoryWarning ( )
self . cachedPages = [ : ]
}
// MARK: V i e w H e l p e r s
public func willBePresentedAgain ( ) {
updateFooterBarButtonItems ( isPlayingVideo : false )
}
public func wasPresented ( ) {
let currentViewController = self . currentViewController
if currentViewController . galleryItem . isVideo {
currentViewController . playVideo ( )
}
}
private var shouldHideToolbars : Bool = false {
didSet {
if ( oldValue = = shouldHideToolbars ) {
return
}
// H i d i n g t h e s t a t u s b a r a f f e c t s t h e p o s i t i o n i n g o f t h e n a v b a r . W e d o n ' t w a n t t o s h o w t h a t i n a n a n i m a t i o n , i t ' s
// b e t t e r t o j u s t h a v e e v e r y t h i g n " f l i t " i n / o u t .
UIApplication . shared . setStatusBarHidden ( shouldHideToolbars , with : . none )
self . navigationController ? . setNavigationBarHidden ( shouldHideToolbars , animated : false )
// W e d o n ' t a n i m a t e t h e b a c k g r o u n d c o l o r c h a n g e b e c a u s e t h e o l d c o l o r s h o w s t h r o u g h m o m e n t a r i l y
// b e h i n d w h e r e t h e s t a t u s b a r " u s e d t o b e " .
self . view . backgroundColor = ( shouldHideToolbars ? UIColor . black : Theme . backgroundColor )
UIView . animate ( withDuration : 0.1 ) {
self . currentViewController . setShouldHideToolbars ( self . shouldHideToolbars )
self . bottomContainer . isHidden = self . shouldHideToolbars
}
}
}
private func updateFooterBarButtonItems ( isPlayingVideo : Bool ) {
// T O D O d o w e s t i l l n e e d t h i s ? s e e m s l i k e a v e s t i g e
// f r o m w h e n m e d i a d e t a i l v i e w w a s u s e d f o r a t t a c h m e n t a p p r o v a l
if self . footerBar = = nil {
owsFailDebug ( " No footer bar visible. " )
return
}
var toolbarItems : [ UIBarButtonItem ] = [
UIBarButtonItem ( barButtonSystemItem : . action , target : self , action : #selector ( didPressShare ) ) ,
UIBarButtonItem ( barButtonSystemItem : . flexibleSpace , target : nil , action : nil )
]
if ( self . currentItem . isVideo ) {
toolbarItems += [
isPlayingVideo ? self . videoPauseBarButton : self . videoPlayBarButton ,
UIBarButtonItem ( barButtonSystemItem : . flexibleSpace , target : nil , action : nil )
]
}
toolbarItems . append ( UIBarButtonItem ( barButtonSystemItem : . trash ,
target : self ,
action : #selector ( didPressDelete ) ) )
self . footerBar . setItems ( toolbarItems , animated : false )
}
// MARK: A c t i o n s
@objc
public func didPressAllMediaButton ( sender : Any ) {
Logger . debug ( " " )
currentViewController . stopAnyVideo ( )
guard let mediaGalleryDataSource = self . mediaGalleryDataSource else {
owsFailDebug ( " mediaGalleryDataSource was unexpectedly nil " )
return
}
mediaGalleryDataSource . showAllMedia ( focusedItem : currentItem )
}
@objc
public func didSwipeView ( sender : Any ) {
Logger . debug ( " " )
self . dismissSelf ( animated : true )
}
@objc
public func didPressDismissButton ( _ sender : Any ) {
dismissSelf ( animated : true )
}
@objc
public func didPressShare ( _ sender : Any ) {
guard let currentViewController = self . viewControllers ? [ 0 ] as ? MediaDetailViewController else {
owsFailDebug ( " currentViewController was unexpectedly nil " )
return
}
let attachmentStream = currentViewController . galleryItem . attachmentStream
AttachmentSharing . showShareUI ( forAttachment : attachmentStream )
}
@objc
public func didPressDelete ( _ sender : Any ) {
guard let currentViewController = self . viewControllers ? [ 0 ] as ? MediaDetailViewController else {
owsFailDebug ( " currentViewController was unexpectedly nil " )
return
}
guard let mediaGalleryDataSource = self . mediaGalleryDataSource else {
owsFailDebug ( " mediaGalleryDataSource was unexpectedly nil " )
return
}
let actionSheet = UIAlertController ( title : nil , message : nil , preferredStyle : . actionSheet )
let deleteAction = UIAlertAction ( title : NSLocalizedString ( " TXT_DELETE_TITLE " , comment : " " ) ,
style : . destructive ) { _ in
let deletedItem = currentViewController . galleryItem
mediaGalleryDataSource . delete ( items : [ deletedItem ] , initiatedBy : self )
}
actionSheet . addAction ( OWSAlerts . cancelAction )
actionSheet . addAction ( deleteAction )
self . present ( actionSheet , animated : true )
}
// MARK: M e d i a G a l l e r y D a t a S o u r c e D e l e g a t e
func mediaGalleryDataSource ( _ mediaGalleryDataSource : MediaGalleryDataSource , willDelete items : [ MediaGalleryItem ] , initiatedBy : MediaGalleryDataSourceDelegate ) {
Logger . debug ( " " )
guard let currentItem = self . currentItem else {
owsFailDebug ( " currentItem was unexpectedly nil " )
return
}
guard items . contains ( currentItem ) else {
Logger . debug ( " irrelevant item " )
return
}
// I f w e s e t C u r r e n t I t e m w i t h ( a n i m a t e d : t r u e ) w h i l e t h i s V C i s i n t h e b a c k g r o u n d , t h e n
// t h e n e x t / p r e v i o u s c a c h e i s n ' t e x p i r e d , a n d w e ' r e a b l e t o s w i p e b a c k t o t h e j u s t - d e l e t e d v c .
// S o t o g e t t h e c o r r e c t b e h a v i o r , w e s h o u l d o n l y a n i m a t e t h e s e t r a n s i t i o n s w h e n t h i s
// v c i s i n t h e f o r e g r o u n d
let isAnimated = initiatedBy = = = self
if ! self . sliderEnabled {
// I n m e s s a g e d e t a i l s , w h i c h d o e s n ' t u s e t h e s l i d e r , s o d o n ' t s w a p p a g e s .
} else if let nextItem = mediaGalleryDataSource . galleryItem ( after : currentItem ) {
self . setCurrentItem ( nextItem , direction : . forward , animated : isAnimated )
} else if let previousItem = mediaGalleryDataSource . galleryItem ( before : currentItem ) {
self . setCurrentItem ( previousItem , direction : . reverse , animated : isAnimated )
} else {
// e l s e w e d e l e t e d t h e l a s t p i e c e o f m e d i a , r e t u r n t o t h e c o n v e r s a t i o n v i e w
self . dismissSelf ( animated : true )
}
}
func mediaGalleryDataSource ( _ mediaGalleryDataSource : MediaGalleryDataSource , deletedSections : IndexSet , deletedItems : [ IndexPath ] ) {
// n o - o p
}
@objc
public func didPressPlayBarButton ( _ sender : Any ) {
guard let currentViewController = self . viewControllers ? [ 0 ] as ? MediaDetailViewController else {
owsFailDebug ( " currentViewController was unexpectedly nil " )
return
}
currentViewController . didPressPlayBarButton ( sender )
}
@objc
public func didPressPauseBarButton ( _ sender : Any ) {
guard let currentViewController = self . viewControllers ? [ 0 ] as ? MediaDetailViewController else {
owsFailDebug ( " currentViewController was unexpectedly nil " )
return
}
currentViewController . didPressPauseBarButton ( sender )
}
// MARK: U I P a g e V i e w C o n t r o l l e r D e l e g a t e
var pendingViewController : MediaDetailViewController ?
public func pageViewController ( _ pageViewController : UIPageViewController , willTransitionTo pendingViewControllers : [ UIViewController ] ) {
Logger . debug ( " " )
assert ( pendingViewControllers . count = = 1 )
pendingViewControllers . forEach { viewController in
guard let pendingViewController = viewController as ? MediaDetailViewController else {
owsFailDebug ( " unexpected mediaDetailViewController: \( viewController ) " )
return
}
self . pendingViewController = pendingViewController
CATransaction . begin ( )
CATransaction . disableActions ( )
if let pendingCaptionText = pendingViewController . galleryItem . caption , pendingCaptionText . count > 0 {
self . pendingCaptionView . text = pendingCaptionText
} else {
self . pendingCaptionView . text = nil
}
self . pendingCaptionView . sizeToFit ( )
self . pendingCaptionView . superview ? . layoutIfNeeded ( )
CATransaction . commit ( )
// E n s u r e u p c o m i n g p a g e r e s p e c t s c u r r e n t t o o l b a r s t a t u s
pendingViewController . setShouldHideToolbars ( self . shouldHideToolbars )
}
}
public func pageViewController ( _ pageViewController : UIPageViewController , didFinishAnimating finished : Bool , previousViewControllers : [ UIViewController ] , transitionCompleted : Bool ) {
Logger . debug ( " " )
assert ( previousViewControllers . count = = 1 )
previousViewControllers . forEach { viewController in
guard let previousPage = viewController as ? MediaDetailViewController else {
owsFailDebug ( " unexpected mediaDetailViewController: \( viewController ) " )
return
}
// D o a n y c l e a n u p f o r t h e n o - l o n g e r v i s i b l e v i e w c o n t r o l l e r
if transitionCompleted {
pendingViewController = nil
// T h i s c a n h a p p e n w h e n t r y i n g t o p a g e p a s t t h e l a s t ( o r f i r s t ) v i e w c o n t r o l l e r
// I n t h a t c a s e , w e d o n ' t w a n t t o c h a n g e t h e c a p t i o n V i e w .
if ( previousPage != currentViewController ) {
updatePagerTransition ( ratioComplete : 1 )
// p r o m o t e " p e n d i n g " t o " c u r r e n t " c a p t i o n v i e w .
let oldCaptionView = self . currentCaptionView
self . currentCaptionView = self . pendingCaptionView
self . pendingCaptionView = oldCaptionView
self . pendingCaptionView . text = nil
}
updateTitle ( )
previousPage . zoomOut ( animated : false )
previousPage . stopAnyVideo ( )
updateFooterBarButtonItems ( isPlayingVideo : false )
}
}
}
// MARK: U I P a g e V i e w C o n t r o l l e r D a t a S o u r c e
public func pageViewController ( _ pageViewController : UIPageViewController , viewControllerBefore viewController : UIViewController ) -> UIViewController ? {
Logger . debug ( " " )
guard let previousDetailViewController = viewController as ? MediaDetailViewController else {
owsFailDebug ( " unexpected viewController: \( viewController ) " )
return nil
}
guard let mediaGalleryDataSource = self . mediaGalleryDataSource else {
owsFailDebug ( " mediaGalleryDataSource was unexpectedly nil " )
return nil
}
let previousItem = previousDetailViewController . galleryItem
guard let nextItem : MediaGalleryItem = mediaGalleryDataSource . galleryItem ( before : previousItem ) else {
return nil
}
guard let nextPage : MediaDetailViewController = buildGalleryPage ( galleryItem : nextItem ) else {
return nil
}
return nextPage
}
public func pageViewController ( _ pageViewController : UIPageViewController , viewControllerAfter viewController : UIViewController ) -> UIViewController ? {
Logger . debug ( " " )
guard let previousDetailViewController = viewController as ? MediaDetailViewController else {
owsFailDebug ( " unexpected viewController: \( viewController ) " )
return nil
}
guard let mediaGalleryDataSource = self . mediaGalleryDataSource else {
owsFailDebug ( " mediaGalleryDataSource was unexpectedly nil " )
return nil
}
let previousItem = previousDetailViewController . galleryItem
guard let nextItem = mediaGalleryDataSource . galleryItem ( after : previousItem ) else {
// n o m o r e p a g e s
return nil
}
guard let nextPage : MediaDetailViewController = buildGalleryPage ( galleryItem : nextItem ) else {
return nil
}
return nextPage
}
private func buildGalleryPage ( galleryItem : MediaGalleryItem ) -> MediaDetailViewController ? {
if let cachedPage = cachedPages [ galleryItem ] {
Logger . debug ( " cache hit. " )
return cachedPage
}
Logger . debug ( " cache miss. " )
var fetchedItem : ConversationViewItem ?
self . uiDatabaseConnection . read { transaction in
let message = galleryItem . message
let thread = message . thread ( with : transaction )
let conversationStyle = ConversationStyle ( thread : thread )
fetchedItem = ConversationInteractionViewItem ( interaction : message ,
isGroupThread : thread . isGroupThread ( ) ,
transaction : transaction ,
conversationStyle : conversationStyle )
}
guard let viewItem = fetchedItem else {
owsFailDebug ( " viewItem was unexpectedly nil " )
return nil
}
let viewController = MediaDetailViewController ( galleryItemBox : GalleryItemBox ( galleryItem ) , viewItem : viewItem )
viewController . delegate = self
cachedPages [ galleryItem ] = viewController
return viewController
}
public func dismissSelf ( animated isAnimated : Bool , completion : ( ( ) -> Void ) ? = nil ) {
// S w a p p i n g m e d i a V i e w f o r p r e s e n t a t i o n V i e w w i l l b e p e r c e p t i b l e i f w e ' r e n o t z o o m e d o u t a l l t h e w a y .
// c u r r e n t V C
currentViewController . zoomOut ( animated : true )
currentViewController . stopAnyVideo ( )
guard let mediaGalleryDataSource = self . mediaGalleryDataSource else {
owsFailDebug ( " mediaGalleryDataSource was unexpectedly nil " )
self . presentingViewController ? . dismiss ( animated : true )
return
}
mediaGalleryDataSource . dismissMediaDetailViewController ( self , animated : isAnimated ) {
UIDevice . current . ows_setOrientation ( . portrait )
completion ? ( )
}
}
// MARK: M e d i a D e t a i l V i e w C o n t r o l l e r D e l e g a t e
@objc
public func mediaDetailViewControllerDidTapMedia ( _ mediaDetailViewController : MediaDetailViewController ) {
Logger . debug ( " " )
self . shouldHideToolbars = ! self . shouldHideToolbars
}
public func mediaDetailViewController ( _ mediaDetailViewController : MediaDetailViewController , requestDelete attachment : TSAttachment ) {
guard let mediaGalleryDataSource = self . mediaGalleryDataSource else {
owsFailDebug ( " mediaGalleryDataSource was unexpectedly nil " )
self . presentingViewController ? . dismiss ( animated : true )
return
}
guard let galleryItem = self . mediaGalleryDataSource ? . galleryItems . first ( where : { $0 . attachmentStream = = attachment } ) else {
owsFailDebug ( " galleryItem was unexpectedly nil " )
self . presentingViewController ? . dismiss ( animated : true )
return
}
dismissSelf ( animated : true ) {
mediaGalleryDataSource . delete ( items : [ galleryItem ] , initiatedBy : self )
}
}
public func mediaDetailViewController ( _ mediaDetailViewController : MediaDetailViewController , isPlayingVideo : Bool ) {
guard mediaDetailViewController = = currentViewController else {
Logger . verbose ( " ignoring stale delegate. " )
return
}
self . shouldHideToolbars = isPlayingVideo
self . updateFooterBarButtonItems ( isPlayingVideo : isPlayingVideo )
}
// MARK: D y n a m i c H e a d e r
private var contactsManager : OWSContactsManager {
return Environment . shared . contactsManager
}
private func senderName ( message : TSMessage ) -> String {
switch message {
case let incomingMessage as TSIncomingMessage :
return self . contactsManager . displayName ( forPhoneIdentifier : incomingMessage . authorId )
case is TSOutgoingMessage :
return NSLocalizedString ( " MEDIA_GALLERY_SENDER_NAME_YOU " , comment : " Short sender label for media sent by you " )
default :
owsFailDebug ( " Unknown message type: \( type ( of : message ) ) " )
return " "
}
}
private lazy var dateFormatter : DateFormatter = {
let formatter = DateFormatter ( )
formatter . dateStyle = . short
formatter . timeStyle = . short
return formatter
} ( )
lazy private var portraitHeaderNameLabel : UILabel = {
let label = UILabel ( )
label . textColor = Theme . navbarTitleColor
label . font = UIFont . ows_regularFont ( withSize : 17 )
label . textAlignment = . center
label . adjustsFontSizeToFitWidth = true
label . minimumScaleFactor = 0.8
return label
} ( )
lazy private var portraitHeaderDateLabel : UILabel = {
let label = UILabel ( )
label . textColor = Theme . navbarTitleColor
label . font = UIFont . ows_regularFont ( withSize : 12 )
label . textAlignment = . center
label . adjustsFontSizeToFitWidth = true
label . minimumScaleFactor = 0.8
return label
} ( )
private lazy var portraitHeaderView : UIView = {
let stackView = UIStackView ( )
stackView . axis = . vertical
stackView . alignment = . center
stackView . spacing = 0
stackView . distribution = . fillProportionally
stackView . addArrangedSubview ( portraitHeaderNameLabel )
stackView . addArrangedSubview ( portraitHeaderDateLabel )
let containerView = UIView ( )
containerView . layoutMargins = UIEdgeInsets ( top : 2 , left : 8 , bottom : 4 , right : 8 )
containerView . addSubview ( stackView )
stackView . autoPinEdge ( toSuperviewMargin : . top , relation : . greaterThanOrEqual )
stackView . autoPinEdge ( toSuperviewMargin : . trailing , relation : . greaterThanOrEqual )
stackView . autoPinEdge ( toSuperviewMargin : . bottom , relation : . greaterThanOrEqual )
stackView . autoPinEdge ( toSuperviewMargin : . leading , relation : . greaterThanOrEqual )
stackView . setContentHuggingHigh ( )
stackView . autoCenterInSuperview ( )
return containerView
} ( )
private func updateTitle ( ) {
guard let currentItem = self . currentItem else {
owsFailDebug ( " currentItem was unexpectedly nil " )
return
}
updateTitle ( item : currentItem )
}
private func updateTitle ( item : MediaGalleryItem ) {
let name = senderName ( message : item . message )
portraitHeaderNameLabel . text = name
// u s e s e n t d a t e
let date = Date ( timeIntervalSince1970 : Double ( item . message . timestamp ) / 1000 )
let formattedDate = dateFormatter . string ( from : date )
portraitHeaderDateLabel . text = formattedDate
let landscapeHeaderFormat = NSLocalizedString ( " MEDIA_GALLERY_LANDSCAPE_TITLE_FORMAT " , comment : " embeds {{sender name}} and {{sent datetime}}, e.g. 'Sarah on 10/30/18, 3:29' " )
let landscapeHeaderText = String ( format : landscapeHeaderFormat , name , formattedDate )
self . title = landscapeHeaderText
self . navigationItem . title = landscapeHeaderText
if #available ( iOS 11 , * ) {
// D o n o t h i n g , o n i O S 1 1 + , a u t o l a y o u t g r o w s t h e s t a c k v i e w a s n e c e s s a r y .
} else {
// S i z e t h e t i t l e V i e w t o b e l a r g e e n o u g h t o f i t t h e w i d e s t l a b e l ,
// b u t n o l a r g e r . I f w e g o f o r a " f u l l w i d t h " l a b e l , o u r t i t l e v i e w
// w i l l n o t b e c e n t e r e d ( s i n c e t h e l e f t a n d r i g h t b a r b u t t o n s h a v e d i f f e r e n t w i d t h s )
portraitHeaderNameLabel . sizeToFit ( )
portraitHeaderDateLabel . sizeToFit ( )
let width = max ( portraitHeaderNameLabel . frame . width , portraitHeaderDateLabel . frame . width )
let headerFrame : CGRect = CGRect ( x : 0 , y : 0 , width : width , height : 44 )
portraitHeaderView . frame = headerFrame
}
}
}
class CaptionView : UIView {
var label : UILabel = UILabel ( )
var text : String ? {
get { return label . text }
set { label . text = newValue }
}
override init ( frame : CGRect ) {
super . init ( frame : frame )
let gradientView = GradientView ( from : . clear , to : . black )
addSubview ( gradientView )
gradientView . autoPinEdgesToSuperviewEdges ( )
addSubview ( label )
label . font = UIFont . ows_dynamicTypeBody
label . textColor = . white
// U s u a l l y c a p t i o n s a r e s h o r t , b u t t h e y c a n b e a s l o n g a s 2 k .
// W e d o n ' t h a v e U I f o r v i e w i n g i n f i n i t e l y l a r g e c a p t i o n s , s o
// w e d o s o m e n o t - i d e a l t h i n g s t o b r o a d e n t h e l e n g h t o f t h e
// c a p t i o n s w e c a n s u p p o r t .
label . numberOfLines = 0
label . adjustsFontSizeToFitWidth = true
label . minimumScaleFactor = 0.5
label . lineBreakMode = . byTruncatingTail
label . autoPinEdgesToSuperviewMargins ( )
}
required init ? ( coder aDecoder : NSCoder ) {
fatalError ( " init(coder:) has not been implemented " )
}
}