//
// 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 UIKit
import PromiseKit
// 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 ! {
return currentViewController . galleryItemBox . value
}
public func setCurrentItem ( _ item : MediaGalleryItem , direction : UIPageViewController . NavigationDirection , animated isAnimated : Bool ) {
guard let galleryPage = self . buildGalleryPage ( galleryItem : item ) else {
owsFailDebug ( " unexpectedly unable to build new gallery page " )
return
}
updateTitle ( item : item )
updateCaption ( item : item )
setViewControllers ( [ galleryPage ] , direction : direction , animated : isAnimated )
updateFooterBarButtonItems ( isPlayingVideo : false )
updateMediaRail ( )
}
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
let options : [ UIPageViewController . OptionsKey : Any ] = [ . interPageSpacing : kSpacingBetweenItems ]
super . init ( transitionStyle : . scroll ,
navigationOrientation : . horizontal ,
options : options )
self . dataSource = self
self . delegate = self
guard let initialPage = self . buildGalleryPage ( galleryItem : initialItem ) else {
owsFailDebug ( " unexpectedly 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 " )
}
// MARK: - S u b v i e w
// MARK: B o t t o m B a r
var bottomContainer : UIView !
var footerBar : UIToolbar !
let captionContainerView : CaptionContainerView = CaptionContainerView ( )
var galleryRailView : GalleryRailView = GalleryRailView ( )
var pagerScrollView : UIScrollView !
// MARK: U I V i e w C o n t r o l l e r o v e r r i d e s
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
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 ] _ , change in
guard let strongSelf = self else { return }
strongSelf . pagerScrollView ( strongSelf . pagerScrollView , contentOffsetDidChange : change )
}
// V i e w s
pagerScrollView . backgroundColor = Colors . navigationBarBackground
view . backgroundColor = Colors . navigationBarBackground
captionContainerView . delegate = self
updateCaptionContainerVisibility ( )
galleryRailView . delegate = self
galleryRailView . autoSetDimension ( . height , toSize : 72 )
let footerBar = self . makeClearToolbar ( )
self . footerBar = footerBar
footerBar . tintColor = Colors . text
footerBar . setBackgroundImage ( UIImage ( ) , forToolbarPosition : . any , barMetrics : UIBarMetrics . default )
footerBar . setShadowImage ( UIImage ( ) , forToolbarPosition : . any )
footerBar . isTranslucent = false
footerBar . barTintColor = Colors . navigationBarBackground
let bottomContainer = UIView ( )
self . bottomContainer = bottomContainer
bottomContainer . backgroundColor = Colors . navigationBarBackground
let bottomStack = UIStackView ( arrangedSubviews : [ captionContainerView , galleryRailView , footerBar ] )
bottomStack . axis = . vertical
bottomContainer . addSubview ( bottomStack )
bottomStack . autoPinEdgesToSuperviewEdges ( )
self . view . addSubview ( bottomContainer )
bottomContainer . autoPinWidthToSuperview ( )
bottomContainer . autoPinEdge ( . bottom , to : . bottom , of : view )
footerBar . bottomAnchor . constraint ( equalTo : view . safeAreaLayoutGuide . bottomAnchor ) . isActive = true
footerBar . autoSetDimension ( . height , toSize : 44 )
updateTitle ( )
updateCaption ( item : currentItem )
updateMediaRail ( )
updateFooterBarButtonItems ( isPlayingVideo : true )
// G e s t u r e s
let verticalSwipe = UISwipeGestureRecognizer ( target : self , action : #selector ( didSwipeView ) )
verticalSwipe . direction = [ . up , . down ]
view . addGestureRecognizer ( verticalSwipe )
let navigationBar = navigationController ! . navigationBar
navigationBar . setBackgroundImage ( UIImage ( ) , for : UIBarMetrics . default )
navigationBar . shadowImage = UIImage ( )
navigationBar . isTranslucent = false
navigationBar . barTintColor = Colors . navigationBarBackground
}
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: 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 )
captionContainerView . updatePagerTransition ( ratioComplete : ratioComplete )
}
// 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 func makeClearToolbar ( ) -> UIToolbar {
let toolbar = UIToolbar ( )
toolbar . backgroundColor = Colors . navigationBarBackground
// h i d e 1 p x t o p - b o r d e r
toolbar . clipsToBounds = true
return toolbar
}
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 )
UIView . animate ( withDuration : 0.1 ) {
self . currentViewController . setShouldHideToolbars ( self . shouldHideToolbars )
self . bottomContainer . isHidden = self . shouldHideToolbars
}
}
}
// MARK: B a r B u t t o n s
lazy var shareBarButton : UIBarButtonItem = {
let shareBarButton = UIBarButtonItem ( barButtonSystemItem : . action , target : self , action : #selector ( didPressShare ) )
shareBarButton . tintColor = Theme . darkThemePrimaryColor
return shareBarButton
} ( )
lazy var deleteBarButton : UIBarButtonItem = {
let deleteBarButton = UIBarButtonItem ( barButtonSystemItem : . trash ,
target : self ,
action : #selector ( didPressDelete ) )
deleteBarButton . tintColor = Theme . darkThemePrimaryColor
return deleteBarButton
} ( )
func buildFlexibleSpace ( ) -> UIBarButtonItem {
return UIBarButtonItem ( barButtonSystemItem : . flexibleSpace , target : nil , action : nil )
}
lazy var videoPlayBarButton : UIBarButtonItem = {
let videoPlayBarButton = UIBarButtonItem ( barButtonSystemItem : . play , target : self , action : #selector ( didPressPlayBarButton ) )
videoPlayBarButton . tintColor = Theme . darkThemePrimaryColor
return videoPlayBarButton
} ( )
lazy var videoPauseBarButton : UIBarButtonItem = {
let videoPauseBarButton = UIBarButtonItem ( barButtonSystemItem : . pause , target : self , action :
#selector ( didPressPauseBarButton ) )
videoPauseBarButton . tintColor = Theme . darkThemePrimaryColor
return videoPauseBarButton
} ( )
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 ] = [
shareBarButton ,
buildFlexibleSpace ( )
]
if ( self . currentItem . isVideo ) {
toolbarItems += [
isPlayingVideo ? self . videoPauseBarButton : self . videoPlayBarButton ,
buildFlexibleSpace ( )
]
}
toolbarItems . append ( deleteBarButton )
self . footerBar . setItems ( toolbarItems , animated : false )
}
func updateMediaRail ( ) {
guard let currentItem = self . currentItem else {
owsFailDebug ( " currentItem was unexpectedly nil " )
return
}
galleryRailView . configureCellViews ( itemProvider : currentItem . album ,
focusedItem : currentItem ,
cellViewBuilder : { _ in return GalleryRailCellView ( ) } )
}
// 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 . presentAlert ( actionSheet )
}
// 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 : AnyObject ) {
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
if let pendingCaptionText = pendingViewController . galleryItem . captionForDisplay , pendingCaptionText . count > 0 {
self . captionContainerView . pendingText = pendingCaptionText
} else {
self . captionContainerView . pendingText = nil
}
// 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 ) {
captionContainerView . completePagerTransition ( )
}
updateTitle ( )
updateMediaRail ( )
previousPage . zoomOut ( animated : false )
previousPage . stopAnyVideo ( )
updateFooterBarButtonItems ( isPlayingVideo : false )
} else {
captionContainerView . pendingText = nil
}
}
}
// 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 )
fetchedItem = ConversationInteractionViewItem ( interaction : message ,
isGroupThread : thread . isGroupThread ( ) ,
transaction : transaction )
}
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
}
if IsLandscapeOrientationEnabled ( ) {
mediaGalleryDataSource . dismissMediaDetailViewController ( self ,
animated : isAnimated ,
completion : completion )
} else {
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 func senderName ( message : TSMessage ) -> String {
switch message {
case let incomingMessage as TSIncomingMessage :
let hexEncodedPublicKey = incomingMessage . authorId
if incomingMessage . thread . isGroupThread ( ) {
let publicChat = Storage . shared . getOpenGroup ( for : incomingMessage . thread . uniqueId ! )
if let publicChat = publicChat {
return UserDisplayNameUtilities . getPublicChatDisplayName ( for : hexEncodedPublicKey , in : publicChat . channel , on : publicChat . server ) ? ? hexEncodedPublicKey
} else {
return hexEncodedPublicKey
}
} else {
return UserDisplayNameUtilities . getPrivateChatDisplayName ( for : hexEncodedPublicKey ) ? ? hexEncodedPublicKey
}
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 = Colors . text
label . font = . systemFont ( ofSize : Values . mediumFontSize )
label . textAlignment = . center
label . adjustsFontSizeToFitWidth = true
label . minimumScaleFactor = 0.8
return label
} ( )
lazy private var portraitHeaderDateLabel : UILabel = {
let label = UILabel ( )
label . textColor = Colors . text
label . font = . systemFont ( ofSize : Values . verySmallFontSize )
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 updateCaption ( item : MediaGalleryItem ) {
captionContainerView . currentText = item . captionForDisplay
}
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
}
}
}
extension MediaGalleryItem : GalleryRailItem {
public func buildRailItemView ( ) -> UIView {
let imageView = UIImageView ( )
imageView . contentMode = . scaleAspectFill
getRailImage ( ) . map { [ weak imageView ] image in
guard let imageView = imageView else { return }
imageView . image = image
} . retainUntilComplete ( )
return imageView
}
public func getRailImage ( ) -> Guarantee < UIImage > {
return Guarantee < UIImage > { fulfill in
if let image = self . thumbnailImage ( async : { fulfill ( $0 ) } ) {
fulfill ( image )
}
}
}
}
extension MediaGalleryAlbum : GalleryRailItemProvider {
var railItems : [ GalleryRailItem ] {
return self . items
}
}
extension MediaPageViewController : GalleryRailViewDelegate {
func galleryRailView ( _ galleryRailView : GalleryRailView , didTapItem imageRailItem : GalleryRailItem ) {
guard let targetItem = imageRailItem as ? MediaGalleryItem else {
owsFailDebug ( " unexpected imageRailItem: \( imageRailItem ) " )
return
}
let direction : UIPageViewController . NavigationDirection
direction = currentItem . albumIndex < targetItem . albumIndex ? . forward : . reverse
self . setCurrentItem ( targetItem , direction : direction , animated : true )
}
}
extension MediaPageViewController : CaptionContainerViewDelegate {
func captionContainerViewDidUpdateText ( _ captionContainerView : CaptionContainerView ) {
updateCaptionContainerVisibility ( )
}
// MARK: H e l p e r s
func updateCaptionContainerVisibility ( ) {
if let currentText = captionContainerView . currentText , currentText . count > 0 {
captionContainerView . isHidden = false
return
}
if let pendingText = captionContainerView . pendingText , pendingText . count > 0 {
captionContainerView . isHidden = false
return
}
captionContainerView . isHidden = true
}
}