//
// 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 Foundation
public enum GalleryDirection {
case before , after , around
}
public struct MediaGalleryItem : Equatable , Hashable {
let logTag = " [MediaGalleryItem] "
let message : TSMessage
let attachmentStream : TSAttachmentStream
let galleryDate : GalleryDate
init ( message : TSMessage , attachmentStream : TSAttachmentStream ) {
self . message = message
self . attachmentStream = attachmentStream
self . galleryDate = GalleryDate ( message : message )
}
var isVideo : Bool {
return attachmentStream . isVideo ( )
}
var isAnimated : Bool {
return attachmentStream . isAnimated ( )
}
var isImage : Bool {
return attachmentStream . isImage ( )
}
var thumbnailImage : UIImage {
guard let image = attachmentStream . thumbnailImage ( ) else {
owsFail ( " \( logTag ) in \( #function ) unexpectedly unable to build attachment thumbnail " )
return UIImage ( )
}
return image
}
var fullSizedImage : UIImage {
guard let image = attachmentStream . image ( ) else {
owsFail ( " \( logTag ) in \( #function ) unexpectedly unable to build attachment image " )
return UIImage ( )
}
return image
}
// MARK: E q u a t a b l e
public static func = = ( lhs : MediaGalleryItem , rhs : MediaGalleryItem ) -> Bool {
return lhs . message . uniqueId = = rhs . message . uniqueId
}
// MARK: H a s h a b l e
public var hashValue : Int {
return message . hashValue
}
}
public struct GalleryDate : Hashable , Comparable , Equatable {
let year : Int
let month : Int
init ( message : TSMessage ) {
let date = message . dateForSorting ( )
self . year = Calendar . current . component ( . year , from : date )
self . month = Calendar . current . component ( . month , from : date )
}
init ( year : Int , month : Int ) {
assert ( month >= 1 && month <= 12 )
self . year = year
self . month = month
}
private var isThisMonth : Bool {
let now = Date ( )
let year = Calendar . current . component ( . year , from : now )
let month = Calendar . current . component ( . month , from : now )
let thisMonth = GalleryDate ( year : year , month : month )
return self = = thisMonth
}
public var date : Date {
var components = DateComponents ( )
components . month = self . month
components . year = self . year
return Calendar . current . date ( from : components ) !
}
private var isThisYear : Bool {
let now = Date ( )
let thisYear = Calendar . current . component ( . year , from : now )
return self . year = = thisYear
}
static let thisYearFormatter : DateFormatter = {
let formatter = DateFormatter ( )
formatter . dateFormat = " MMMM "
return formatter
} ( )
static let olderFormatter : DateFormatter = {
let formatter = DateFormatter ( )
// F I X M E l o c a l i z e f o r R T L , o r i s t h e r e a b u i l t i n w a y t o d o t h i s ?
formatter . dateFormat = " MMMM yyyy "
return formatter
} ( )
var localizedString : String {
if isThisMonth {
return NSLocalizedString ( " MEDIA_GALLERY_THIS_MONTH_HEADER " , comment : " Section header in media gallery collection view " )
} else if isThisYear {
return type ( of : self ) . thisYearFormatter . string ( from : self . date )
} else {
return type ( of : self ) . olderFormatter . string ( from : self . date )
}
}
// MARK: H a s h a b l e
public var hashValue : Int {
return month . hashValue ^ year . hashValue
}
// M a r k : C o m p a r a b l e
public static func < ( lhs : GalleryDate , rhs : GalleryDate ) -> Bool {
if lhs . year != rhs . year {
return lhs . year < rhs . year
} else if lhs . month != rhs . month {
return lhs . month < rhs . month
} else {
return false
}
}
// MARK: E q u a t a b l e
public static func = = ( lhs : GalleryDate , rhs : GalleryDate ) -> Bool {
return lhs . month = = rhs . month && lhs . year = = rhs . year
}
}
protocol MediaGalleryDataSource : class {
var hasFetchedOldest : Bool { get }
var hasFetchedMostRecent : Bool { get }
var galleryItems : [ MediaGalleryItem ] { get }
var galleryItemCount : Int { get }
var sections : [ GalleryDate : [ MediaGalleryItem ] ] { get }
var sectionDates : [ GalleryDate ] { get }
func ensureGalleryItemsLoaded ( _ direction : GalleryDirection , item : MediaGalleryItem , amount : UInt , completion : ( ( IndexSet , [ IndexPath ] ) -> Void ) ? )
func galleryItem ( before currentItem : MediaGalleryItem ) -> MediaGalleryItem ?
func galleryItem ( after currentItem : MediaGalleryItem ) -> MediaGalleryItem ?
func showAllMedia ( focusedItem : MediaGalleryItem )
func dismissMediaDetailViewController ( _ mediaDetailViewController : MediaPageViewController , animated isAnimated : Bool , completion : ( ( ) -> Void ) ? )
func delete ( message : TSMessage )
}
class MediaGalleryViewController : UINavigationController , MediaGalleryDataSource , MediaTileViewControllerDelegate {
private var pageViewController : MediaPageViewController ?
private let uiDatabaseConnection : YapDatabaseConnection
private let editingDatabaseConnection : YapDatabaseConnection
private let mediaGalleryFinder : OWSMediaGalleryFinder
private var initialDetailItem : MediaGalleryItem ?
private let thread : TSThread
private let options : MediaGalleryOption
// w e s t a r t w i t h a s m a l l r a n g e s i z e f o r q u i c k l o a d i n g .
private let fetchRangeSize : UInt = 10
deinit {
Logger . debug ( " \( logTag ) deinit " )
}
init ( thread : TSThread , uiDatabaseConnection : YapDatabaseConnection , options : MediaGalleryOption = [ ] ) {
self . thread = thread
assert ( uiDatabaseConnection . isInLongLivedReadTransaction ( ) )
self . uiDatabaseConnection = uiDatabaseConnection
self . editingDatabaseConnection = OWSPrimaryStorage . shared ( ) . newDatabaseConnection ( )
self . options = options
self . mediaGalleryFinder = OWSMediaGalleryFinder ( thread : thread )
super . init ( nibName : nil , bundle : nil )
}
required init ? ( coder aDecoder : NSCoder ) {
fatalError ( " init(coder:) has not been implemented " )
}
// MARK: V i e w L i f e c y l e
override func viewDidLoad ( ) {
super . viewDidLoad ( )
// U I M o d a l P r e s e n t a t i o n C u s t o m r e t a i n s t h e c u r r e n t v i e w c o n t e x t b e h i n d o u r V C , a l l o w i n g u s t o m a n u a l l y
// a n i m a t e i n o u r v i e w , o v e r t h e e x i s t i n g c o n t e x t , s i m i l a r t o a c r o s s d i s o l v e , b u t a l l o w i n g u s t o h a v e
// m o r e f i n e g r a i n e d c o n t r o l
self . modalPresentationStyle = . custom
self . navigationBar . barTintColor = UIColor . ows_materialBlue
self . navigationBar . isTranslucent = false
self . navigationBar . isOpaque = true
// T h e p r e s e n t a t i o n V i e w i s o n l y u s e d d u r i n g p r e s e n t / d i s m i s s a n i m a t i o n s .
// I t ' s a s t a t i c i m a g e o f t h e m e d i a c o n t e n t .
let presentationView = UIImageView ( )
self . presentationView = presentationView
self . view . addSubview ( presentationView )
presentationView . isHidden = true
presentationView . clipsToBounds = true
presentationView . layer . allowsEdgeAntialiasing = true
presentationView . layer . minificationFilter = kCAFilterTrilinear
presentationView . layer . magnificationFilter = kCAFilterTrilinear
presentationView . contentMode = . scaleAspectFit
}
// MARK: P r e s e n t / D i s m i s s
private var currentItem : MediaGalleryItem {
return self . pageViewController ! . currentItem
}
private var replacingView : UIView ?
private var presentationView : UIImageView !
private var presentationViewConstraints : [ NSLayoutConstraint ] = [ ]
// T O D O r e n a m e t o r e p l a c i n g O r i g i n R e c t
private var originRect : CGRect ?
public func presentDetailView ( fromViewController : UIViewController , mediaMessage : TSMessage , replacingView : UIView ) {
var galleryItem : MediaGalleryItem ?
uiDatabaseConnection . read { transaction in
galleryItem = self . buildGalleryItem ( message : mediaMessage , transaction : transaction ) !
}
guard let initialDetailItem = galleryItem else {
owsFail ( " \( logTag ) in \( #function ) unexpectedly failed to build initialDetailItem. " )
return
}
presentDetailView ( fromViewController : fromViewController , initialDetailItem : initialDetailItem , replacingView : replacingView )
}
public func presentDetailView ( fromViewController : UIViewController , initialDetailItem : MediaGalleryItem , replacingView : UIView ) {
// F o r a s p e e d y l o a d , w e o n l y f e t c h a f e w i t e m s o n e i t h e r s i d e o f
// t h e i n i t i a l m e s s a g e
ensureGalleryItemsLoaded ( . around , item : initialDetailItem , amount : 10 )
self . initialDetailItem = initialDetailItem
let pageViewController = MediaPageViewController ( initialItem : initialDetailItem , mediaGalleryDataSource : self , uiDatabaseConnection : self . uiDatabaseConnection , options : self . options )
self . pageViewController = pageViewController
self . setViewControllers ( [ pageViewController ] , animated : false )
self . replacingView = replacingView
let convertedRect : CGRect = replacingView . convert ( replacingView . bounds , to : UIApplication . shared . keyWindow )
self . originRect = convertedRect
// l o a d V i e w h a s n ' t n e c e s s a r i l y b e e n c a l l e d y e t .
self . loadViewIfNeeded ( )
self . presentationView . image = initialDetailItem . fullSizedImage
self . applyInitialMediaViewConstraints ( )
// R e s t o r e p r e s e n t a t i o n V i e w . a l p h a i n c a s e a p r e v i o u s d i s m i s s l e f t u s i n a b a d s t a t e .
pageViewController . navigationController ? . setNavigationBarHidden ( false , animated : false )
self . presentationView . alpha = 1
// W e w a n t t o a n i m a t e t h e t a p p e d m e d i a f r o m i t ' s p o s i t i o n i n t h e p r e v i o u s V C
// t o i t ' s r e s t i n g p l a c e i n t h e c e n t e r o f t h i s v i e w c o n t r o l l e r .
//
// R a t h e r t h a n a n i m a t i n g t h e a c t u a l m e d i a v i e w i n p l a c e , w e a n i m a t e t h e p r e s e n t a t i o n V i e w , w h i c h i s a s t a t i c
// i m a g e o f t h e m e d i a c o n t e n t . A n i m a t i n g t h e a c t u a l m e d i a v i e w i s p r o b l e m a t i c f o r a c o u p l e r e a s o n s :
// 1 . T h e m e d i a v i e w u l t i m a t e l y l i v e s i n a z o o m a b l e s c r o l l V i e w . G e t t i n g b o t h o r i g i n a l p o s i t i o n i n g a n d t h e f i n a l p o s i t i o n i n g
// c o r r e c t , i n v o l v e s m a n i p u l a t i n g t h e z o o m S c a l e a n d p o s i t i o n s i m u l t a n e o u s l y , w h i c h r e s u l t s i n n o n - l i n e a r m o v e m e n t ,
// e s p e c i a l l y n o t i c e a b l e o n h i g h r e s o l u t i o n i m a g e s .
// 2 . F o r V i d e o v i e w s , t h e A V P l a y e r L a y e r c o n t e n t d o e s n o t s c a l e w i t h t h e p r e s e n t a t i o n a n i m a t i o n . S o y o u i n s t e a d g e t a f u l l s c a l e
// v i d e o , w h e r e i n o n l y t h e c r o p p i n g i s a n i m a t e d .
// U s i n g a s i m p l e i m a g e v i e w a l l o w s u s t o a d d r e s s b o t h t h e s e p r o b l e m s r e l a t i v e l y e a s i l y .
self . view . alpha = 0.0
guard let detailView = pageViewController . view else {
owsFail ( " \( logTag ) in \( #function ) detailView was unexpectedly nil " )
return
}
detailView . isHidden = true
self . presentationView . isHidden = false
self . presentationView . layer . cornerRadius = OWSMessageCellCornerRadius
fromViewController . present ( self , animated : false ) {
// 1 . F a d e i n t h e e n t i r e v i e w .
UIView . animate ( withDuration : 0.1 ) {
self . replacingView ? . alpha = 0.0
self . view . alpha = 1.0
}
self . presentationView . superview ? . layoutIfNeeded ( )
self . applyFinalMediaViewConstraints ( )
// 2 . A n i m a t e i m a g e V i e w f r o m i t ' s i n i t i a l p o s i t i o n , w h i c h s h o u l d m a t c h w h e r e i t w a s
// i n t h e p r e s e n t i n g v i e w t o i t ' s f i n a l p o s i t i o n , f r o n t a n d c e n t e r i n t h i s v i e w . T h i s
// a n i m a t i o n d u r a t i o n i n t e n t i o n a l l y o v e r l a p s t h e p r e v i o u s
UIView . animate ( withDuration : 0.2 ,
delay : 0.08 ,
options : . curveEaseOut ,
animations : {
self . presentationView . layer . cornerRadius = 0
self . presentationView . superview ? . layoutIfNeeded ( )
self . view . backgroundColor = UIColor . white
} ,
completion : { ( _ : Bool ) in
// A t t h i s p o i n t o u r p r e s e n t a t i o n v i e w s h o u l d b e o v e r l a y e d p e r f e c t l y
// w i t h o u r m e d i a v i e w . S w a p p i n g t h e m o u t s h o u l d b e i m p e r c e p t i b l e .
detailView . isHidden = false
self . presentationView . isHidden = true
self . view . isUserInteractionEnabled = true
pageViewController . wasPresented ( )
} )
}
}
// I f w e ' r e u s i n g a n a v i g a t i o n C o n t r o l l e r o t h e r t h a n s e l f t o p r e s e n t t h e v i e w s
// e . g . t h e c o n v e r s a t i o n s e t t i n g s v i e w c o n t r o l l e r
var fromNavController : UINavigationController ?
func pushTileView ( fromNavController : UINavigationController ) {
var mostRecentItem : MediaGalleryItem ?
self . uiDatabaseConnection . read { transaction in
if let message = self . mediaGalleryFinder . mostRecentMediaMessage ( transaction : transaction ) {
mostRecentItem = self . buildGalleryItem ( message : message , transaction : transaction )
}
}
if let mostRecentItem = mostRecentItem {
mediaTileViewController . focusedItem = mostRecentItem
ensureGalleryItemsLoaded ( . around , item : mostRecentItem , amount : 100 )
}
self . fromNavController = fromNavController
fromNavController . pushViewController ( mediaTileViewController , animated : true )
}
func showAllMedia ( focusedItem : MediaGalleryItem ) {
// T O D O f a n c y a n i m a t i o n - z o o m m e d i a i t e m i n t o i t ' s t i l e i n t h e a l l m e d i a g r i d
ensureGalleryItemsLoaded ( . around , item : focusedItem , amount : 100 )
if let fromNavController = self . fromNavController {
// I f f r o m c o n v e r s a t i o n s e t t i n g s v i e w , w e ' v e a l r e a d y p u s h e d
fromNavController . popViewController ( animated : true )
} else {
// I f f r o m c o n v e r s a t i o n v i e w
mediaTileViewController . focusedItem = focusedItem
self . pushViewController ( mediaTileViewController , animated : true )
}
}
// MARK: M e d i a T i l e V i e w C o n t r o l l e r D e l e g a t e
func mediaTileViewController ( _ viewController : MediaTileViewController , didTapView tappedView : UIView , mediaGalleryItem : MediaGalleryItem ) {
if self . fromNavController != nil {
// I f w e g o t t o t h e g a l l e r y v i a c o n v e r s a t i o n s e t t i n g s , p r e s e n t t h e d e t a i l v i e w
// o n t o p o f t h e t i l e v i e w
//
// = = V i e w C o n t r o l l e r S c h e m a t i c = =
//
// [ D e t a i l V i e w ] < - - ,
// [ T i l e V i e w ] - - - - - '
// [ C o n v e r s a t i o n S e t t i n g s V i e w ]
// [ C o n v e r s a t i o n V i e w ]
//
self . presentDetailView ( fromViewController : mediaTileViewController , initialDetailItem : mediaGalleryItem , replacingView : tappedView )
} else {
// I f w e g o t t o t h e g a l l e r y v i a t h e c o n v e r s a t i o n v i e w , p o p t h e t i l e v i e w
// t o r e t u r n t o t h e d e t a i l v i e w
//
// = = V i e w C o n t r o l l e r S c h e m a t i c = =
//
// [ T i l e V i e w ] - - - - - ,
// [ D e t a i l V i e w ] < - - '
// [ C o n v e r s a t i o n V i e w ]
//
guard let pageViewController = self . pageViewController else {
owsFail ( " \( logTag ) in \( #function ) pageViewController was unexpectedly nil " )
self . dismiss ( animated : true )
return
}
pageViewController . currentItem = mediaGalleryItem
pageViewController . willBePresentedAgain ( )
// T O D O f a n c y z o o m a n i m a t i o n
self . popViewController ( animated : true )
}
}
public func dismissMediaDetailViewController ( _ mediaDetailViewController : MediaPageViewController , animated isAnimated : Bool , completion : ( ( ) -> Void ) ? ) {
self . view . isUserInteractionEnabled = false
UIApplication . shared . isStatusBarHidden = false
guard let detailView = mediaDetailViewController . view else {
owsFail ( " \( logTag ) in \( #function ) detailView was unexpectedly nil " )
self . presentingViewController ? . dismiss ( animated : false , completion : completion )
return
}
detailView . isHidden = true
self . presentationView . isHidden = false
// M o v e t h e p r e s e n t a t i o n V i e w b a c k t o i t ' s i n i t i a l p o s i t i o n , i . e . w h e r e
// i t s i t s o n t h e s c r e e n i n t h e c o n v e r s a t i o n v i e w .
let changedItems = currentItem != self . initialDetailItem
if changedItems {
self . presentationView . image = currentItem . fullSizedImage
self . applyOffscreenMediaViewConstraints ( )
} else {
self . applyInitialMediaViewConstraints ( )
}
if isAnimated {
UIView . animate ( withDuration : changedItems ? 0.25 : 0.18 ,
delay : 0.0 ,
options : . curveEaseOut ,
animations : {
self . presentationView . superview ? . layoutIfNeeded ( )
// I n c a s e u s e r h a s h i d d e n b a r s , w h i c h c h a n g e s b a c k g r o u n d t o b l a c k .
self . view . backgroundColor = UIColor . white
if changedItems {
self . presentationView . alpha = 0
} else {
self . presentationView . layer . cornerRadius = OWSMessageCellCornerRadius
}
} ,
completion : nil )
// T h i s i n t e n t i o n a l l y o v e r l a p s t h e p r e v i o u s a n i m a t i o n a b i t
UIView . animate ( withDuration : 0.1 ,
delay : 0.15 ,
options : . curveEaseInOut ,
animations : {
guard let replacingView = self . replacingView else {
owsFail ( " \( self . logTag ) in \( #function ) replacingView was unexpectedly nil " )
self . presentingViewController ? . dismiss ( animated : false , completion : completion )
return
}
replacingView . alpha = 1.0
// f a d e o u t c o n t e n t a n d t o o l b a r s
self . navigationController ? . view . alpha = 0.0
} ,
completion : { ( _ : Bool ) in
self . presentingViewController ? . dismiss ( animated : false , completion : completion )
} )
} else {
guard let replacingView = self . replacingView else {
owsFail ( " \( self . logTag ) in \( #function ) replacingView was unexpectedly nil " )
self . presentingViewController ? . dismiss ( animated : false , completion : completion )
return
}
replacingView . alpha = 1.0
self . presentingViewController ? . dismiss ( animated : false , completion : completion )
}
}
private func applyInitialMediaViewConstraints ( ) {
if ( self . presentationViewConstraints . count > 0 ) {
NSLayoutConstraint . deactivate ( self . presentationViewConstraints )
self . presentationViewConstraints = [ ]
}
guard let originRect = self . originRect else {
owsFail ( " \( logTag ) in \( #function ) originRect was unexpectedly nil " )
return
}
guard let presentationSuperview = self . presentationView . superview else {
owsFail ( " \( logTag ) in \( #function ) presentationView.superview was unexpectedly nil " )
return
}
let convertedRect : CGRect = presentationSuperview . convert ( originRect , from : UIApplication . shared . keyWindow )
self . presentationViewConstraints += self . presentationView . autoSetDimensions ( to : convertedRect . size )
self . presentationViewConstraints += [
self . presentationView . autoPinEdge ( toSuperviewEdge : . top , withInset : convertedRect . origin . y ) ,
self . presentationView . autoPinEdge ( toSuperviewEdge : . left , withInset : convertedRect . origin . x )
]
}
private func applyFinalMediaViewConstraints ( ) {
if ( self . presentationViewConstraints . count > 0 ) {
NSLayoutConstraint . deactivate ( self . presentationViewConstraints )
self . presentationViewConstraints = [ ]
}
self . presentationViewConstraints = [
self . presentationView . autoPinEdge ( toSuperviewEdge : . leading ) ,
self . presentationView . autoPinEdge ( toSuperviewEdge : . top ) ,
self . presentationView . autoPinEdge ( toSuperviewEdge : . trailing ) ,
self . presentationView . autoPinEdge ( toSuperviewEdge : . bottom )
]
}
private func applyOffscreenMediaViewConstraints ( ) {
if ( self . presentationViewConstraints . count > 0 ) {
NSLayoutConstraint . deactivate ( self . presentationViewConstraints )
self . presentationViewConstraints = [ ]
}
self . presentationViewConstraints += [
self . presentationView . autoPinEdge ( toSuperviewEdge : . leading ) ,
self . presentationView . autoPinEdge ( toSuperviewEdge : . trailing ) ,
self . presentationView . autoPinEdge ( . top , to : . bottom , of : self . view )
]
}
// MARK: M e d i a G a l l e r y D a t a S o u r c e
lazy var mediaTileViewController : MediaTileViewController = {
let vc = MediaTileViewController ( mediaGalleryDataSource : self , uiDatabaseConnection : self . uiDatabaseConnection )
vc . delegate = self
return vc
} ( )
var galleryItems : [ MediaGalleryItem ] = [ ]
var sections : [ GalleryDate : [ MediaGalleryItem ] ] = [ : ]
var sectionDates : [ GalleryDate ] = [ ]
var hasFetchedOldest = false
var hasFetchedMostRecent = false
func buildGalleryItem ( message : TSMessage , transaction : YapDatabaseReadTransaction ) -> MediaGalleryItem ? {
guard let attachmentStream = message . attachment ( with : transaction ) as ? TSAttachmentStream else {
owsFail ( " \( self . logTag ) in \( #function ) attachment was unexpectedly empty " )
return nil
}
return MediaGalleryItem ( message : message , attachmentStream : attachmentStream )
}
// R a n g e i n s t e a d o f i n d e x S e t s i n c e i t ' s c o n t i g u o u s ?
var fetchedIndexSet = IndexSet ( ) {
didSet {
Logger . debug ( " \( logTag ) in \( #function ) \( oldValue ) -> \( fetchedIndexSet ) " )
}
}
func ensureGalleryItemsLoaded ( _ direction : GalleryDirection , item : MediaGalleryItem , amount : UInt , completion : ( ( IndexSet , [ IndexPath ] ) -> Void ) ? = nil ) {
var galleryItems : [ MediaGalleryItem ] = self . galleryItems
var sections : [ GalleryDate : [ MediaGalleryItem ] ] = self . sections
var sectionDates : [ GalleryDate ] = self . sectionDates
var newGalleryItems : [ MediaGalleryItem ] = [ ]
var newDates : [ GalleryDate ] = [ ]
Bench ( title : " fetching gallery items " ) {
self . uiDatabaseConnection . read { transaction in
let initialIndex : Int = Int ( self . mediaGalleryFinder . mediaIndex ( message : item . message , transaction : transaction ) )
let mediaCount : Int = Int ( self . mediaGalleryFinder . mediaCount ( transaction : transaction ) )
let requestRange : Range < Int > = { ( ) -> Range < Int > in
let range : Range < Int > = { ( ) -> Range < Int > in
switch direction {
case . around :
// T o k e e p i t s i m p l e , t h i s i s n ' t e x a c t l y * a m o u n t * s i z e d i f ` m e s s a g e ` w i n d o w o v e r l a p s t h e e n d o r
// b e g i n n i n g o f t h e v i e w . S t i l l , w e h a v e s u f f i c i e n t b u f f e r t o f e t c h m o r e a s t h e u s e r s w i p e s .
let start : Int = initialIndex - Int ( amount ) / 2
let end : Int = initialIndex + Int ( amount ) / 2
return start . . < end
case . before :
let start : Int = initialIndex - Int ( amount )
let end : Int = initialIndex
return start . . < end
case . after :
let start : Int = initialIndex
let end : Int = initialIndex + Int ( amount )
return start . . < end
}
} ( )
return range . clamped ( to : 0. . < mediaCount )
} ( )
let requestSet = IndexSet ( integersIn : requestRange )
guard ! self . fetchedIndexSet . contains ( integersIn : requestSet ) else {
Logger . debug ( " \( self . logTag ) in \( #function ) all requested messages have already been loaded. " )
return
}
let unfetchedSet = requestSet . subtracting ( self . fetchedIndexSet )
guard unfetchedSet . count > ( requestSet . count / 2 ) else {
// F o r p e r f w e o n l y w a n t t o f e t c h a r e l a t i v e l y f u l l b a t c h , u n l e s s t h e r e q u e s t S e t i s v e r y s m a l l .
Logger . debug ( " \( self . logTag ) in \( #function ) ignoring small fetch request: \( unfetchedSet . count ) " )
return
}
Logger . debug ( " \( self . logTag ) in \( #function ) fetching set: \( unfetchedSet ) " )
let nsRange : NSRange = NSRange ( location : unfetchedSet . min ( ) ! , length : unfetchedSet . count )
self . mediaGalleryFinder . enumerateMediaMessages ( range : nsRange , transaction : transaction ) { ( message : TSMessage ) in
guard ! self . deletedMessages . contains ( message ) else {
Logger . debug ( " \( self . logTag ) skipping \( message ) which has been deleted. " )
return
}
guard let item : MediaGalleryItem = self . buildGalleryItem ( message : message , transaction : transaction ) else {
owsFail ( " \( self . logTag ) in \( #function ) unexpectedly failed to buildGalleryItem " )
return
}
let date = item . galleryDate
galleryItems . append ( item )
if sections [ date ] != nil {
sections [ date ] ! . append ( item )
// s o w e c a n u p d a t e c o l l e c t i o n V i e w
newGalleryItems . append ( item )
} else {
sectionDates . append ( date )
sections [ date ] = [ item ]
// s o w e c a n u p d a t e c o l l e c t i o n V i e w
newDates . append ( date )
newGalleryItems . append ( item )
}
}
self . fetchedIndexSet = self . fetchedIndexSet . union ( unfetchedSet )
self . hasFetchedOldest = self . fetchedIndexSet . min ( ) = = 0
self . hasFetchedMostRecent = self . fetchedIndexSet . max ( ) = = mediaCount - 1
}
}
// T O D O o n l y s o r t i f c h a n g e d
var sortedSections : [ GalleryDate : [ MediaGalleryItem ] ] = [ : ]
Bench ( title : " sorting gallery items " ) {
galleryItems . sort { lhs , rhs -> Bool in
return lhs . message . timestampForSorting ( ) < rhs . message . timestampForSorting ( )
}
sectionDates . sort ( )
for ( date , galleryItems ) in sections {
sortedSections [ date ] = galleryItems . sorted { lhs , rhs -> Bool in
return lhs . message . timestampForSorting ( ) < rhs . message . timestampForSorting ( )
}
}
}
self . galleryItems = galleryItems
self . sections = sortedSections
self . sectionDates = sectionDates
if let completionBlock = completion {
Bench ( title : " calculating changes for collectionView " ) {
// F I X M E c a n w e a v o i d t h i s i n d e x o f f s e t ?
let dateIndices = newDates . map { sectionDates . index ( of : $0 ) ! + 1 }
let addedSections : IndexSet = IndexSet ( dateIndices )
let addedItems : [ IndexPath ] = newGalleryItems . map { galleryItem in
let sectionIdx = sectionDates . index ( of : galleryItem . galleryDate ) !
let section = sections [ galleryItem . galleryDate ] !
let itemIdx = section . index ( of : galleryItem ) !
// F I X M E c a n w e a v o i d t h i s i n d e x o f f s e t ?
return IndexPath ( item : itemIdx , section : sectionIdx + 1 )
}
completionBlock ( addedSections , addedItems )
}
}
}
var deletedMessages : Set < TSMessage > = Set ( )
func delete ( message : TSMessage ) {
Logger . info ( " \( logTag ) in \( #function ) with message: \( String ( describing : message . uniqueId ) ) attachmentId: \( String ( describing : message . attachmentIds . firstObject ) ) " )
// T O D O p u t t h i s s o m e w h e r e r e a s o n a b l e . . .
self . mediaTileViewController . collectionView ! . layoutIfNeeded ( )
self . editingDatabaseConnection . asyncReadWrite { transaction in
message . remove ( with : transaction )
}
self . deletedMessages . insert ( message )
var deletedSections : IndexSet = IndexSet ( )
var deletedIndexPaths : [ IndexPath ] = [ ]
guard let itemIndex = galleryItems . index ( where : { $0 . message = = message } ) else {
owsFail ( " \( logTag ) in \( #function ) removing unknown item. " )
return
}
let item : MediaGalleryItem = galleryItems [ itemIndex ]
self . galleryItems . remove ( at : itemIndex )
guard let sectionIndex = sectionDates . index ( where : { $0 = = item . galleryDate } ) else {
owsFail ( " \( logTag ) in \( #function ) item with unknown date. " )
return
}
guard var sectionItems = self . sections [ item . galleryDate ] else {
owsFail ( " \( logTag ) in \( #function ) item with unknown section " )
return
}
if sectionItems = = [ item ] {
// L a s t i t e m i n s e c t i o n . D e l e t e s e c t i o n .
self . sections [ item . galleryDate ] = nil
self . sectionDates . remove ( at : sectionIndex )
deletedSections . insert ( sectionIndex + 1 )
deletedIndexPaths . append ( IndexPath ( row : 0 , section : sectionIndex + 1 ) )
} else {
guard let sectionRowIndex = sectionItems . index ( of : item ) else {
owsFail ( " \( logTag ) in \( #function ) item with unknown sectionRowIndex " )
return
}
sectionItems . remove ( at : sectionRowIndex )
self . sections [ item . galleryDate ] = sectionItems
deletedIndexPaths . append ( IndexPath ( row : sectionRowIndex , section : sectionIndex + 1 ) )
}
// T O D O ? n o t i f y p a g e r v i e w
// n o t i f y t i l e v i e w
self . mediaTileViewController . updatedDataSource ( deletedSections : deletedSections , deletedItems : deletedIndexPaths )
}
let kGallerySwipeLoadBatchSize : UInt = 5
internal func galleryItem ( after currentItem : MediaGalleryItem ) -> MediaGalleryItem ? {
Logger . debug ( " \( logTag ) in \( #function ) " )
self . ensureGalleryItemsLoaded ( . after , item : currentItem , amount : kGallerySwipeLoadBatchSize )
guard let currentIndex = galleryItems . index ( of : currentItem ) else {
owsFail ( " currentIndex was unexpectedly nil in \( #function ) " )
return nil
}
let index : Int = galleryItems . index ( after : currentIndex )
return galleryItems [ safe : index ]
}
internal func galleryItem ( before currentItem : MediaGalleryItem ) -> MediaGalleryItem ? {
Logger . debug ( " \( logTag ) in \( #function ) " )
self . ensureGalleryItemsLoaded ( . before , item : currentItem , amount : kGallerySwipeLoadBatchSize )
guard let currentIndex = galleryItems . index ( of : currentItem ) else {
owsFail ( " currentIndex was unexpectedly nil in \( #function ) " )
return nil
}
let index : Int = galleryItems . index ( before : currentIndex )
return galleryItems [ safe : index ]
}
var galleryItemCount : Int {
var count : UInt = 0
self . uiDatabaseConnection . read { ( transaction : YapDatabaseReadTransaction ) in
count = self . mediaGalleryFinder . mediaCount ( transaction : transaction )
}
return Int ( count )
}
}