@ -8,7 +8,7 @@ public protocol MediaTileViewControllerDelegate: class {
func mediaTileViewController ( _ viewController : MediaTileViewController , didTapView tappedView : UIView , mediaGalleryItem : MediaGalleryItem )
}
public class MediaTileViewController : UICollectionViewController , MediaGallery CellDelegate, MediaGallery DataSourceDelegate {
public class MediaTileViewController : UICollectionViewController , MediaGallery DataSourceDelegate {
private weak var mediaGalleryDataSource : MediaGalleryDataSource ?
@ -88,10 +88,30 @@ public class MediaTileViewController: UICollectionViewController, MediaGalleryCe
collectionView . delegate = self
// T O D O i P h o n e X
// f e e l s a b i t w e i r d t o h a v e c o n t e n t s m a s h e d a l l t h e w a y t o t h e b o t t o m e d g e .
collectionView . contentInset = UIEdgeInsets ( top : 0 , left : 0 , bottom : 20 , right : 0 )
let footerBar = UIToolbar ( )
self . footerBar = footerBar
let deleteButton = UIBarButtonItem ( barButtonSystemItem : . trash ,
target : self ,
action : #selector ( didPressDelete ) )
self . deleteButton = deleteButton
let footerItems = [
UIBarButtonItem ( barButtonSystemItem : . flexibleSpace , target : nil , action : nil ) ,
deleteButton ,
UIBarButtonItem ( barButtonSystemItem : . flexibleSpace , target : nil , action : nil ) ,
]
footerBar . setItems ( footerItems , animated : false )
self . view . addSubview ( self . footerBar )
footerBar . barTintColor = UIColor . ows_signalBrandBlue
footerBar . autoPinWidthToSuperview ( )
footerBar . autoSetDimension ( . height , toSize : kFooterBarHeight )
self . footerBarBottomConstraint = footerBar . autoPinEdge ( toSuperviewEdge : . bottom , withInset : - kFooterBarHeight )
updateSelectButton ( )
self . view . layoutIfNeeded ( )
scrollToBottom ( animated : false )
}
@ -123,7 +143,7 @@ public class MediaTileViewController: UICollectionViewController, MediaGalleryCe
self . collectionView ? . scrollToItem ( at : indexPath , at : . centeredVertically , animated : false )
}
// MARK: U I C o l l e ti o n V i e w D e l e g a t e
// MARK: U I C o l l e c ti o n V i e w D e l e g a t e
override public func scrollViewDidScroll ( _ scrollView : UIScrollView ) {
self . autoLoadMoreIfNecessary ( )
@ -137,42 +157,87 @@ public class MediaTileViewController: UICollectionViewController, MediaGalleryCe
self . isUserScrolling = false
}
private var isUserScrolling : Bool = false {
didSet {
autoLoadMoreIfNecessary ( )
override public func collectionView ( _ collectionView : UICollectionView , shouldSelectItemAt indexPath : IndexPath ) -> Bool {
Logger . debug ( " \( self . logTag ) in \( #function ) " )
guard galleryDates . count > 0 else {
return false
}
switch indexPath . section {
case kLoadOlderSectionIdx , loadNewerSectionIdx :
return false
default :
return 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
override public func collectionView ( _ collectionView : UICollectionView , shouldDeselectItemAt indexPath : IndexPath ) -> Bool {
func mediaGalleryDataSource ( _ mediaGalleryDataSource : MediaGalleryDataSource , willDelete message : TSMessage ) {
guard let collectionView = self . collectionView else {
owsFail ( " \( logTag ) in \( #function ) collectionView was unexpectedly nil " )
return
Logger . debug ( " \( self . logTag ) in \( #function ) " )
guard galleryDates . count > 0 else {
return false
}
// W e ' v e g o t t o l a y o u t t h e c o l l e c t i o n V i e w b e f o r e a n y c h a n g e s a r e m a d e t o t h e d a t e s o u r c e
// o t h e r w i s e w e ' l l f a i l w h e n w e t r y t o r e m o v e t h e d e l e t e d s e c t i o n s / r o w s
collectionView . layoutIfNeeded ( )
switch indexPath . section {
case kLoadOlderSectionIdx , loadNewerSectionIdx :
return false
default :
return true
}
}
func mediaGalleryDataSource ( _ mediaGalleryDataSource : MediaGalleryDataSource , deletedSections : IndexSet , deletedItems : [ IndexPath ] ) {
guard let collectionView = self . collectionView else {
owsFail ( " \( logTag ) in \( #function ) collectionView was unexpetedly nil " )
public override func collectionView ( _ collectionView : UICollectionView , shouldHighlightItemAt indexPath : IndexPath ) -> Bool {
Logger . debug ( " \( self . logTag ) in \( #function ) " )
guard galleryDates . count > 0 else {
return false
}
switch indexPath . section {
case kLoadOlderSectionIdx , loadNewerSectionIdx :
return false
default :
return true
}
}
override public func collectionView ( _ collectionView : UICollectionView , didSelectItemAt indexPath : IndexPath ) {
Logger . debug ( " \( self . logTag ) in \( #function ) " )
guard let galleryCell = self . collectionView ( collectionView , cellForItemAt : indexPath ) as ? MediaGalleryCell else {
owsFail ( " \( logTag ) in \( #function ) galleryCell was unexpectedly nil " )
return
}
guard mediaGalleryDataSource . galleryItemCount > 0 else {
// S h o w E m p t y
self . collectionView ? . reloadData ( )
guard let galleryItem = galleryCell . item else {
owsFail ( " \( logTag ) in \( #function ) galleryItem was unexpectedly nil " )
return
}
// I f c o l l e c t i o n V i e w h a s n ' t b e e n l a i d o u t y e t , i t w o n ' t h a v e t h e s e c t i o n s / r o w s t o r e m o v e .
collectionView . performBatchUpdates ( {
collectionView . deleteSections ( deletedSections )
collectionView . deleteItems ( at : deletedItems )
} )
if isInBatchSelectMode {
updateDeleteButton ( )
} else {
collectionView . deselectItem ( at : indexPath , animated : true )
self . delegate ? . mediaTileViewController ( self , didTapView : galleryCell . imageView , mediaGalleryItem : galleryItem )
}
}
public override func collectionView ( _ collectionView : UICollectionView , didDeselectItemAt indexPath : IndexPath ) {
Logger . debug ( " \( self . logTag ) in \( #function ) " )
if isInBatchSelectMode {
updateDeleteButton ( )
}
}
private var isUserScrolling : Bool = false {
didSet {
autoLoadMoreIfNecessary ( )
}
}
// MARK: U I C o l l e c t i o n V i e w D a t a S o u r c e
@ -288,18 +353,8 @@ public class MediaTileViewController: UICollectionViewController, MediaGalleryCe
owsFail ( " \( logTag ) in \( #function ) unexpected cell for loadNewerSectionIdx " )
return defaultCell
default :
guard let sectionDate = self . galleryDates [ safe : indexPath . section - 1 ] else {
owsFail ( " \( logTag ) in \( #function ) unknown section: \( indexPath . section ) " )
return defaultCell
}
guard let sectionItems = self . galleryItems [ sectionDate ] else {
owsFail ( " \( logTag ) in \( #function ) no section for date: \( sectionDate ) " )
return defaultCell
}
guard let galleryItem = sectionItems [ safe : indexPath . row ] else {
owsFail ( " \( logTag ) in \( #function ) no message for row: \( indexPath . row ) " )
guard let galleryItem = galleryItem ( at : indexPath ) else {
owsFail ( " \( logTag ) in \( #function ) no message for path: \( indexPath ) " )
return defaultCell
}
@ -308,12 +363,31 @@ public class MediaTileViewController: UICollectionViewController, MediaGalleryCe
return defaultCell
}
cell . configure ( item : galleryItem , delegate : self )
cell . configure ( item : galleryItem )
return cell
}
}
func galleryItem ( at indexPath : IndexPath ) -> MediaGalleryItem ? {
guard let sectionDate = self . galleryDates [ safe : indexPath . section - 1 ] else {
owsFail ( " \( logTag ) in \( #function ) unknown section: \( indexPath . section ) " )
return nil
}
guard let sectionItems = self . galleryItems [ sectionDate ] else {
owsFail ( " \( logTag ) in \( #function ) no section for date: \( sectionDate ) " )
return nil
}
guard let galleryItem = sectionItems [ safe : indexPath . row ] else {
owsFail ( " \( logTag ) in \( #function ) no message for row: \( indexPath . row ) " )
return nil
}
return galleryItem
}
// MARK: U I C o l l e c t i o n V i e w D e l e g a t e F l o w L a y o u t
public func collectionView ( _ collectionView : UICollectionView ,
@ -343,11 +417,173 @@ public class MediaTileViewController: UICollectionViewController, MediaGalleryCe
return kMonthHeaderSize
}
}
// MARK: M e d i a G a l l e r y D e l e g a t e
fileprivate func didTapCell ( _ cell : MediaGalleryCell , item : MediaGalleryItem ) {
Logger . debug ( " \( logTag ) in \( #function ) " )
self . delegate ? . mediaTileViewController ( self , didTapView : cell . imageView , mediaGalleryItem : item )
// MARK: B a t c h S e l e c t i o n
var isInBatchSelectMode = false {
didSet {
collectionView ! . allowsMultipleSelection = isInBatchSelectMode
updateSelectButton ( )
updateDeleteButton ( )
}
}
func updateDeleteButton ( ) {
guard let collectionView = self . collectionView else {
owsFail ( " \( logTag ) in \( #function ) collectionView was unexpectedly nil " )
return
}
if let count = collectionView . indexPathsForSelectedItems ? . count , count > 0 {
self . deleteButton . isEnabled = true
} else {
self . deleteButton . isEnabled = false
}
}
func updateSelectButton ( ) {
if isInBatchSelectMode {
self . navigationItem . rightBarButtonItem = UIBarButtonItem ( barButtonSystemItem : . cancel , target : self , action : #selector ( didCancelSelect ) )
} else {
self . navigationItem . rightBarButtonItem = UIBarButtonItem ( title : NSLocalizedString ( " BUTTON_SELECT " , comment : " Button text to enable batch selection mode " ) ,
style : . plain ,
target : self ,
action : #selector ( didTapSelect ) )
}
}
@objc
func didTapSelect ( _ sender : Any ) {
isInBatchSelectMode = true
guard let collectionView = self . collectionView else {
owsFail ( " \( logTag ) in \( #function ) collectionView was unexpectedly nil " )
return
}
// s h o w t o o l b a r
UIView . animate ( withDuration : 0.1 , delay : 0 , options : . curveEaseInOut , animations : {
NSLayoutConstraint . deactivate ( [ self . footerBarBottomConstraint ] )
self . footerBarBottomConstraint = self . footerBar . autoPin ( toBottomLayoutGuideOf : self , withInset : 0 )
self . footerBar . superview ? . layoutIfNeeded ( )
// e n s u r e t o o l b a r d o e s n ' t c o v e r b o t t o m r o w .
collectionView . contentInset . bottom += self . kFooterBarHeight
} , completion : nil )
// d i s a b l e d u n t i l a t l e a s t o n e i t e m i s s e l e c t e d
self . deleteButton . isEnabled = false
// D o n ' t a l l o w t h e u s e r t o l e a v e m i d - s e l e c t i o n , s o t h e y r e a l i z e d t h e y h a v e
// t o c a n c e l ( l o s e ) t h e i r s e l e c t i o n i f t h e y l e a v e .
self . navigationItem . hidesBackButton = true
}
@objc
func didCancelSelect ( _ sender : Any ) {
isInBatchSelectMode = false
guard let collectionView = self . collectionView else {
owsFail ( " \( logTag ) in \( #function ) collectionView was unexpectedly nil " )
return
}
// h i d e t o o l b a r
UIView . animate ( withDuration : 0.1 , delay : 0 , options : . curveEaseInOut , animations : {
NSLayoutConstraint . deactivate ( [ self . footerBarBottomConstraint ] )
self . footerBarBottomConstraint = self . footerBar . autoPinEdge ( toSuperviewEdge : . bottom , withInset : - self . kFooterBarHeight )
self . footerBar . superview ? . layoutIfNeeded ( )
// u n d o " e n s u r e t o o l b a r d o e s n ' t c o v e r b o t t o m r o w . "
collectionView . contentInset . bottom -= self . kFooterBarHeight
} , completion : nil )
self . navigationItem . hidesBackButton = false
// d e s e l e c t a n y s e l e c t e d
collectionView . indexPathsForSelectedItems ? . forEach { collectionView . deselectItem ( at : $0 , animated : false ) }
}
@objc
func didPressDelete ( _ sender : Any ) {
Logger . debug ( " \( self . logTag ) in \( #function ) " )
guard let collectionView = self . collectionView else {
owsFail ( " \( logTag ) in \( #function ) collectionView was unexpectedly nil " )
return
}
guard let indexPaths = collectionView . indexPathsForSelectedItems else {
owsFail ( " \( logTag ) in \( #function ) indexPaths was unexpectedly nil " )
return
}
let items : [ MediaGalleryItem ] = indexPaths . flatMap { return self . galleryItem ( at : $0 ) }
guard let mediaGalleryDataSource = self . mediaGalleryDataSource else {
owsFail ( " \( logTag ) in \( #function ) mediaGalleryDataSource was unexpectedly nil " )
return
}
let confirmationTitle : String = {
if indexPaths . count = = 1 {
return NSLocalizedString ( " MEDIA_GALLERY_DELETE_SINGLE_MESSAGE " , comment : " Confirmation button text to delete selected media message from the gallery " )
} else {
let format = NSLocalizedString ( " MEDIA_GALLERY_DELETE_MULTIPLE_MESSAGES_FORMAT " , comment : " Confirmation button text to delete selected media from the gallery, embeds {{number of messages}} " )
return String ( format : format , indexPaths . count )
}
} ( )
let deleteAction = UIAlertAction ( title : confirmationTitle , style : . destructive ) { _ in
mediaGalleryDataSource . delete ( items : items )
}
let actionSheet = UIAlertController ( title : nil , message : nil , preferredStyle : . actionSheet )
actionSheet . addAction ( deleteAction )
actionSheet . addAction ( OWSAlerts . cancelAction )
present ( actionSheet , animated : true )
}
var footerBar : UIToolbar !
var deleteButton : UIBarButtonItem !
var footerBarBottomConstraint : NSLayoutConstraint !
let kFooterBarHeight : CGFloat = 40
// 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 ] ) {
Logger . debug ( " \( self . logTag ) in \( #function ) " )
guard let collectionView = self . collectionView else {
owsFail ( " \( logTag ) in \( #function ) collectionView was unexpectedly nil " )
return
}
// W e ' v e g o t t o l a y o u t t h e c o l l e c t i o n V i e w b e f o r e a n y c h a n g e s a r e m a d e t o t h e d a t e s o u r c e
// o t h e r w i s e w e ' l l f a i l w h e n w e t r y t o r e m o v e t h e d e l e t e d s e c t i o n s / r o w s
collectionView . layoutIfNeeded ( )
}
func mediaGalleryDataSource ( _ mediaGalleryDataSource : MediaGalleryDataSource , deletedSections : IndexSet , deletedItems : [ IndexPath ] ) {
Logger . debug ( " \( self . logTag ) in \( #function ) with deletedSections: \( deletedSections ) deletedItems: \( deletedItems ) " )
guard let collectionView = self . collectionView else {
owsFail ( " \( logTag ) in \( #function ) collectionView was unexpetedly nil " )
return
}
guard mediaGalleryDataSource . galleryItemCount > 0 else {
// S h o w E m p t y
self . collectionView ? . reloadData ( )
return
}
collectionView . performBatchUpdates ( {
collectionView . deleteSections ( deletedSections )
collectionView . deleteItems ( at : deletedItems )
} )
}
// MARK: L a z y L o a d i n g
@ -577,10 +813,6 @@ fileprivate class MediaGallerySectionHeader: UICollectionReusableView {
}
}
fileprivate protocol MediaGalleryCellDelegate : class {
func didTapCell ( _ cell : MediaGalleryCell , item : MediaGalleryItem )
}
fileprivate class MediaGalleryStaticHeader : UICollectionViewCell {
static let reuseIdentifier = " MediaGalleryStaticHeader "
@ -616,40 +848,78 @@ fileprivate class MediaGalleryCell: UICollectionViewCell {
static let reuseIdentifier = " MediaGalleryCell "
public let imageView : UIImageView
private var tapGesture : UITapGestureRecognizer !
private let badgeView : UIImageView
private let contentTypeBadgeView : UIImageView
private let selectedBadgeView : UIImageView
private let highlightedView : UIView
private let selectedView : UIView
private var item : MediaGalleryItem ?
public weak var delegate : MediaGalleryCellDelegate ?
fileprivate var item : MediaGalleryItem ?
static let videoBadgeImage = # imageLiteral ( resourceName : " ic_gallery_badge_video " )
static let animatedBadgeImage = # imageLiteral ( resourceName : " ic_gallery_badge_gif " )
static let selectedBadgeImage = # imageLiteral ( resourceName : " selected_blue_circle " )
override var isSelected : Bool {
didSet {
self . selectedBadgeView . isHidden = ! self . isSelected
self . selectedView . isHidden = ! self . isSelected
}
}
override var isHighlighted : Bool {
didSet {
self . highlightedView . isHidden = ! self . isHighlighted
}
}
override init ( frame : CGRect ) {
self . imageView = UIImageView ( )
imageView . contentMode = . scaleAspectFill
self . badgeView = UIImageView ( )
badgeView . isHidden = true
self . contentTypeB adgeView = UIImageView ( )
contentTypeB adgeView. isHidden = true
super . init ( frame : frame )
self . selectedBadgeView = UIImageView ( )
selectedBadgeView . image = MediaGalleryCell . selectedBadgeImage
selectedBadgeView . isHidden = true
self . tapGesture = UITapGestureRecognizer ( target : self , action : #selector ( didTap ) )
self . addGestureRecognizer ( tapGesture )
self . highlightedView = UIView ( )
highlightedView . alpha = 0.2
highlightedView . backgroundColor = . black
highlightedView . isHidden = true
self . selectedView = UIView ( )
selectedView . alpha = 0.3
selectedView . backgroundColor = . white
selectedView . isHidden = true
super . init ( frame : frame )
self . clipsToBounds = true
self . contentView . addSubview ( imageView )
self . contentView . addSubview ( badgeView )
self . contentView . addSubview ( contentTypeBadgeView )
self . contentView . addSubview ( highlightedView )
self . contentView . addSubview ( selectedView )
self . contentView . addSubview ( selectedBadgeView )
imageView . autoPinEdgesToSuperviewEdges ( )
highlightedView . autoPinEdgesToSuperviewEdges ( )
selectedView . autoPinEdgesToSuperviewEdges ( )
// N o t e a s s e t s w e r e r e n d e r e d t o m a t c h e x a c t l y . W e d o n ' t w a n t t o r e - s i z e w i t h
// c o n t e n t m o d e l e s t t h e y b e c o m e l e s s l e g i b l e .
let kBadgeSize = CGSize ( width : 18 , height : 12 )
badgeView . autoPinEdge ( toSuperviewEdge : . leading , withInset : 3 )
badgeView . autoPinEdge ( toSuperviewEdge : . bottom , withInset : 3 )
badgeView . autoSetDimensions ( to : kBadgeSize )
let kContentTypeBadgeSize = CGSize ( width : 18 , height : 12 )
contentTypeBadgeView . autoPinEdge ( toSuperviewEdge : . leading , withInset : 3 )
contentTypeBadgeView . autoPinEdge ( toSuperviewEdge : . bottom , withInset : 3 )
contentTypeBadgeView . autoSetDimensions ( to : kContentTypeBadgeSize )
let kSelectedBadgeSize = CGSize ( width : 31 , height : 31 )
selectedBadgeView . autoPinEdge ( toSuperviewEdge : . trailing , withInset : 0 )
selectedBadgeView . autoPinEdge ( toSuperviewEdge : . bottom , withInset : 0 )
selectedBadgeView . autoSetDimensions ( to : kSelectedBadgeSize )
}
@ available ( * , unavailable , message : " Unimplemented " )
@ -657,21 +927,19 @@ fileprivate class MediaGalleryCell: UICollectionViewCell {
fatalError ( " init(coder:) has not been implemented " )
}
public func configure ( item : MediaGalleryItem , delegate : MediaGalleryCellDelegate ) {
public func configure ( item : MediaGalleryItem ) {
self . item = item
self . imageView . image = item . thumbnailImage
if item . isVideo {
self . b adgeView. isHidden = false
self . b adgeView. image = MediaGalleryCell . videoBadgeImage
self . contentTypeB adgeView. isHidden = false
self . contentTypeB adgeView. image = MediaGalleryCell . videoBadgeImage
} else if item . isAnimated {
self . b adgeView. isHidden = false
self . b adgeView. image = MediaGalleryCell . animatedBadgeImage
self . contentTypeB adgeView. isHidden = false
self . contentTypeB adgeView. image = MediaGalleryCell . animatedBadgeImage
} else {
assert ( item . isImage )
self . b adgeView. isHidden = true
self . contentTypeB adgeView. isHidden = true
}
self . delegate = delegate
}
override public func prepareForReuse ( ) {
@ -679,18 +947,9 @@ fileprivate class MediaGalleryCell: UICollectionViewCell {
self . item = nil
self . imageView . image = nil
self . badgeView . isHidden = true
self . delegate = nil
}
// MARK: E v e n t s
func didTap ( gestureRecognizer : UITapGestureRecognizer ) {
guard let item = self . item else {
owsFail ( " \( logTag ) item was unexpectedly nil " )
return
}
self . delegate ? . didTapCell ( self , item : item )
self . contentTypeBadgeView . isHidden = true
self . highlightedView . isHidden = true
self . selectedView . isHidden = true
self . selectedBadgeView . isHidden = true
}
}