|
|
@ -229,11 +229,11 @@ protocol MediaGalleryDataSource: class {
|
|
|
|
func showAllMedia(focusedItem: MediaGalleryItem)
|
|
|
|
func showAllMedia(focusedItem: MediaGalleryItem)
|
|
|
|
func dismissMediaDetailViewController(_ mediaDetailViewController: MediaPageViewController, animated isAnimated: Bool, completion: (() -> Void)?)
|
|
|
|
func dismissMediaDetailViewController(_ mediaDetailViewController: MediaPageViewController, animated isAnimated: Bool, completion: (() -> Void)?)
|
|
|
|
|
|
|
|
|
|
|
|
func delete(items: [MediaGalleryItem], initiatedBy: MediaGalleryDataSourceDelegate)
|
|
|
|
func delete(items: [MediaGalleryItem], initiatedBy: AnyObject)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
protocol MediaGalleryDataSourceDelegate: class {
|
|
|
|
protocol MediaGalleryDataSourceDelegate: class {
|
|
|
|
func mediaGalleryDataSource(_ mediaGalleryDataSource: MediaGalleryDataSource, willDelete items: [MediaGalleryItem], initiatedBy: MediaGalleryDataSourceDelegate)
|
|
|
|
func mediaGalleryDataSource(_ mediaGalleryDataSource: MediaGalleryDataSource, willDelete items: [MediaGalleryItem], initiatedBy: AnyObject)
|
|
|
|
func mediaGalleryDataSource(_ mediaGalleryDataSource: MediaGalleryDataSource, deletedSections: IndexSet, deletedItems: [IndexPath])
|
|
|
|
func mediaGalleryDataSource(_ mediaGalleryDataSource: MediaGalleryDataSource, deletedSections: IndexSet, deletedItems: [IndexPath])
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
@ -318,7 +318,10 @@ class MediaGallery: NSObject, MediaGalleryDataSource, MediaTileViewControllerDel
|
|
|
|
|
|
|
|
|
|
|
|
private var pageViewController: MediaPageViewController?
|
|
|
|
private var pageViewController: MediaPageViewController?
|
|
|
|
|
|
|
|
|
|
|
|
private let uiDatabaseConnection: YapDatabaseConnection
|
|
|
|
private var uiDatabaseConnection: YapDatabaseConnection {
|
|
|
|
|
|
|
|
return OWSPrimaryStorage.shared().uiDatabaseConnection
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private let editingDatabaseConnection: YapDatabaseConnection
|
|
|
|
private let editingDatabaseConnection: YapDatabaseConnection
|
|
|
|
private let mediaGalleryFinder: OWSMediaGalleryFinder
|
|
|
|
private let mediaGalleryFinder: OWSMediaGalleryFinder
|
|
|
|
|
|
|
|
|
|
|
@ -334,16 +337,19 @@ class MediaGallery: NSObject, MediaGalleryDataSource, MediaTileViewControllerDel
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@objc
|
|
|
|
@objc
|
|
|
|
init(thread: TSThread, uiDatabaseConnection: YapDatabaseConnection, options: MediaGalleryOption = []) {
|
|
|
|
init(thread: TSThread, options: MediaGalleryOption = []) {
|
|
|
|
self.thread = thread
|
|
|
|
self.thread = thread
|
|
|
|
assert(uiDatabaseConnection.isInLongLivedReadTransaction())
|
|
|
|
|
|
|
|
self.uiDatabaseConnection = uiDatabaseConnection
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
self.editingDatabaseConnection = OWSPrimaryStorage.shared().newDatabaseConnection()
|
|
|
|
self.editingDatabaseConnection = OWSPrimaryStorage.shared().newDatabaseConnection()
|
|
|
|
|
|
|
|
|
|
|
|
self.options = options
|
|
|
|
self.options = options
|
|
|
|
self.mediaGalleryFinder = OWSMediaGalleryFinder(thread: thread)
|
|
|
|
self.mediaGalleryFinder = OWSMediaGalleryFinder(thread: thread)
|
|
|
|
super.init()
|
|
|
|
super.init()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
NotificationCenter.default.addObserver(self,
|
|
|
|
|
|
|
|
selector: #selector(uiDatabaseDidUpdate),
|
|
|
|
|
|
|
|
name: .OWSUIDatabaseConnectionDidUpdate,
|
|
|
|
|
|
|
|
object: OWSPrimaryStorage.shared().dbNotificationObject)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// MARK: Present/Dismiss
|
|
|
|
// MARK: Present/Dismiss
|
|
|
@ -709,7 +715,70 @@ class MediaGallery: NSObject, MediaGalleryDataSource, MediaTileViewControllerDel
|
|
|
|
]
|
|
|
|
]
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// MARK: MediaGalleryDataSource
|
|
|
|
// MARK: - Database Notifications
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@objc
|
|
|
|
|
|
|
|
func uiDatabaseDidUpdate(notification: Notification) {
|
|
|
|
|
|
|
|
guard let notifications = notification.userInfo?[OWSUIDatabaseConnectionNotificationsKey] as? [Notification] else {
|
|
|
|
|
|
|
|
owsFailDebug("notifications was unexpectedly nil")
|
|
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
guard mediaGalleryFinder.hasMediaChanges(in: notifications, dbConnection: uiDatabaseConnection) else {
|
|
|
|
|
|
|
|
Logger.verbose("no changes for thread: \(thread)")
|
|
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
let rowChanges = extractRowChanges(notifications: notifications)
|
|
|
|
|
|
|
|
assert(rowChanges.count > 0)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
process(rowChanges: rowChanges)
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func extractRowChanges(notifications: [Notification]) -> [YapDatabaseViewRowChange] {
|
|
|
|
|
|
|
|
return notifications.flatMap { notification -> [YapDatabaseViewRowChange] in
|
|
|
|
|
|
|
|
guard let userInfo = notification.userInfo else {
|
|
|
|
|
|
|
|
owsFailDebug("userInfo was unexpectedly nil")
|
|
|
|
|
|
|
|
return []
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
guard let extensionChanges = userInfo["extensions"] as? [AnyHashable: Any] else {
|
|
|
|
|
|
|
|
owsFailDebug("extensionChanges was unexpectedly nil")
|
|
|
|
|
|
|
|
return []
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
guard let galleryData = extensionChanges[OWSMediaGalleryFinder.databaseExtensionName()] as? [AnyHashable: Any] else {
|
|
|
|
|
|
|
|
owsFailDebug("galleryData was unexpectedly nil")
|
|
|
|
|
|
|
|
return []
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
guard let galleryChanges = galleryData["changes"] as? [Any] else {
|
|
|
|
|
|
|
|
owsFailDebug("gallerlyChanges was unexpectedly nil")
|
|
|
|
|
|
|
|
return []
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return galleryChanges.compactMap { $0 as? YapDatabaseViewRowChange }
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func process(rowChanges: [YapDatabaseViewRowChange]) {
|
|
|
|
|
|
|
|
let deleteChanges = rowChanges.filter { $0.type == .delete }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
let deletedItems: [MediaGalleryItem] = deleteChanges.compactMap { (deleteChange: YapDatabaseViewRowChange) -> MediaGalleryItem? in
|
|
|
|
|
|
|
|
guard let deletedItem = self.galleryItems.first(where: { galleryItem in
|
|
|
|
|
|
|
|
galleryItem.attachmentStream.uniqueId == deleteChange.collectionKey.key
|
|
|
|
|
|
|
|
}) else {
|
|
|
|
|
|
|
|
Logger.debug("deletedItem was never loaded - no need to remove.")
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return deletedItem
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
self.delete(items: deletedItems, initiatedBy: self)
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// MARK: - MediaGalleryDataSource
|
|
|
|
|
|
|
|
|
|
|
|
lazy var mediaTileViewController: MediaTileViewController = {
|
|
|
|
lazy var mediaTileViewController: MediaTileViewController = {
|
|
|
|
let vc = MediaTileViewController(mediaGalleryDataSource: self, uiDatabaseConnection: self.uiDatabaseConnection)
|
|
|
|
let vc = MediaTileViewController(mediaGalleryDataSource: self, uiDatabaseConnection: self.uiDatabaseConnection)
|
|
|
@ -777,6 +846,10 @@ class MediaGallery: NSObject, MediaGalleryDataSource, MediaTileViewControllerDel
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
enum MediaGalleryError: Error {
|
|
|
|
|
|
|
|
case itemNoLongerExists
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func ensureGalleryItemsLoaded(_ direction: GalleryDirection, item: MediaGalleryItem, amount: UInt, completion: ((IndexSet, [IndexPath]) -> Void)? = nil ) {
|
|
|
|
func ensureGalleryItemsLoaded(_ direction: GalleryDirection, item: MediaGalleryItem, amount: UInt, completion: ((IndexSet, [IndexPath]) -> Void)? = nil ) {
|
|
|
|
|
|
|
|
|
|
|
|
var galleryItems: [MediaGalleryItem] = self.galleryItems
|
|
|
|
var galleryItems: [MediaGalleryItem] = self.galleryItems
|
|
|
@ -786,92 +859,102 @@ class MediaGallery: NSObject, MediaGalleryDataSource, MediaTileViewControllerDel
|
|
|
|
var newGalleryItems: [MediaGalleryItem] = []
|
|
|
|
var newGalleryItems: [MediaGalleryItem] = []
|
|
|
|
var newDates: [GalleryDate] = []
|
|
|
|
var newDates: [GalleryDate] = []
|
|
|
|
|
|
|
|
|
|
|
|
Bench(title: "fetching gallery items") {
|
|
|
|
do {
|
|
|
|
self.uiDatabaseConnection.read { transaction in
|
|
|
|
try Bench(title: "fetching gallery items") {
|
|
|
|
|
|
|
|
try self.uiDatabaseConnection.read { transaction in
|
|
|
|
let initialIndex: Int = Int(self.mediaGalleryFinder.mediaIndex(attachment: item.attachmentStream, transaction: transaction))
|
|
|
|
guard let index = self.mediaGalleryFinder.mediaIndex(attachment: item.attachmentStream, transaction: transaction) else {
|
|
|
|
let mediaCount: Int = Int(self.mediaGalleryFinder.mediaCount(transaction: transaction))
|
|
|
|
throw MediaGalleryError.itemNoLongerExists
|
|
|
|
|
|
|
|
}
|
|
|
|
let requestRange: Range<Int> = { () -> Range<Int> in
|
|
|
|
let initialIndex: Int = index.intValue
|
|
|
|
let range: Range<Int> = { () -> Range<Int> in
|
|
|
|
let mediaCount: Int = Int(self.mediaGalleryFinder.mediaCount(transaction: transaction))
|
|
|
|
switch direction {
|
|
|
|
|
|
|
|
case .around:
|
|
|
|
let requestRange: Range<Int> = { () -> Range<Int> in
|
|
|
|
// To keep it simple, this isn't exactly *amount* sized if `message` window overlaps the end or
|
|
|
|
let range: Range<Int> = { () -> Range<Int> in
|
|
|
|
// beginning of the view. Still, we have sufficient buffer to fetch more as the user swipes.
|
|
|
|
switch direction {
|
|
|
|
let start: Int = initialIndex - Int(amount) / 2
|
|
|
|
case .around:
|
|
|
|
let end: Int = initialIndex + Int(amount) / 2
|
|
|
|
// To keep it simple, this isn't exactly *amount* sized if `message` window overlaps the end or
|
|
|
|
|
|
|
|
// beginning of the view. Still, we have sufficient buffer to fetch more as the user swipes.
|
|
|
|
return start..<end
|
|
|
|
let start: Int = initialIndex - Int(amount) / 2
|
|
|
|
case .before:
|
|
|
|
let end: Int = initialIndex + Int(amount) / 2
|
|
|
|
let start: Int = initialIndex - Int(amount)
|
|
|
|
|
|
|
|
let end: Int = initialIndex
|
|
|
|
return start..<end
|
|
|
|
|
|
|
|
case .before:
|
|
|
|
return start..<end
|
|
|
|
let start: Int = initialIndex - Int(amount)
|
|
|
|
case .after:
|
|
|
|
let end: Int = initialIndex
|
|
|
|
let start: Int = initialIndex
|
|
|
|
|
|
|
|
let end: Int = initialIndex + Int(amount)
|
|
|
|
return start..<end
|
|
|
|
|
|
|
|
case .after:
|
|
|
|
|
|
|
|
let start: Int = initialIndex
|
|
|
|
|
|
|
|
let end: Int = initialIndex + Int(amount)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return start..<end
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}()
|
|
|
|
|
|
|
|
|
|
|
|
return start..<end
|
|
|
|
return range.clamped(to: 0..<mediaCount)
|
|
|
|
}
|
|
|
|
|
|
|
|
}()
|
|
|
|
}()
|
|
|
|
|
|
|
|
|
|
|
|
return range.clamped(to: 0..<mediaCount)
|
|
|
|
let requestSet = IndexSet(integersIn: requestRange)
|
|
|
|
}()
|
|
|
|
guard !self.fetchedIndexSet.contains(integersIn: requestSet) else {
|
|
|
|
|
|
|
|
Logger.debug("all requested messages have already been loaded.")
|
|
|
|
let requestSet = IndexSet(integersIn: requestRange)
|
|
|
|
return
|
|
|
|
guard !self.fetchedIndexSet.contains(integersIn: requestSet) else {
|
|
|
|
}
|
|
|
|
Logger.debug("all requested messages have already been loaded.")
|
|
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
let unfetchedSet = requestSet.subtracting(self.fetchedIndexSet)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// For perf we only want to fetch a substantially full batch...
|
|
|
|
|
|
|
|
let isSubstantialRequest = unfetchedSet.count > (requestSet.count / 2)
|
|
|
|
|
|
|
|
// ...but we always fulfill even small requests if we're getting just the tail end of a gallery.
|
|
|
|
|
|
|
|
let isFetchingEdgeOfGallery = (self.fetchedIndexSet.count - unfetchedSet.count) < requestSet.count
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
guard isSubstantialRequest || isFetchingEdgeOfGallery else {
|
|
|
|
let unfetchedSet = requestSet.subtracting(self.fetchedIndexSet)
|
|
|
|
Logger.debug("ignoring small fetch request: \(unfetchedSet.count)")
|
|
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Logger.debug("fetching set: \(unfetchedSet)")
|
|
|
|
// For perf we only want to fetch a substantially full batch...
|
|
|
|
let nsRange: NSRange = NSRange(location: unfetchedSet.min()!, length: unfetchedSet.count)
|
|
|
|
let isSubstantialRequest = unfetchedSet.count > (requestSet.count / 2)
|
|
|
|
self.mediaGalleryFinder.enumerateMediaAttachments(range: nsRange, transaction: transaction) { (attachment: TSAttachment) in
|
|
|
|
// ...but we always fulfill even small requests if we're getting just the tail end of a gallery.
|
|
|
|
|
|
|
|
let isFetchingEdgeOfGallery = (self.fetchedIndexSet.count - unfetchedSet.count) < requestSet.count
|
|
|
|
|
|
|
|
|
|
|
|
guard !self.deletedAttachments.contains(attachment) else {
|
|
|
|
guard isSubstantialRequest || isFetchingEdgeOfGallery else {
|
|
|
|
Logger.debug("skipping \(attachment) which has been deleted.")
|
|
|
|
Logger.debug("ignoring small fetch request: \(unfetchedSet.count)")
|
|
|
|
return
|
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
guard let item: MediaGalleryItem = self.buildGalleryItem(attachment: attachment, transaction: transaction) else {
|
|
|
|
Logger.debug("fetching set: \(unfetchedSet)")
|
|
|
|
owsFailDebug("unexpectedly failed to buildGalleryItem")
|
|
|
|
let nsRange: NSRange = NSRange(location: unfetchedSet.min()!, length: unfetchedSet.count)
|
|
|
|
return
|
|
|
|
self.mediaGalleryFinder.enumerateMediaAttachments(range: nsRange, transaction: transaction) { (attachment: TSAttachment) in
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
let date = item.galleryDate
|
|
|
|
guard !self.deletedAttachments.contains(attachment) else {
|
|
|
|
|
|
|
|
Logger.debug("skipping \(attachment) which has been deleted.")
|
|
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
galleryItems.append(item)
|
|
|
|
guard let item: MediaGalleryItem = self.buildGalleryItem(attachment: attachment, transaction: transaction) else {
|
|
|
|
if sections[date] != nil {
|
|
|
|
owsFailDebug("unexpectedly failed to buildGalleryItem")
|
|
|
|
sections[date]!.append(item)
|
|
|
|
return
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// so we can update collectionView
|
|
|
|
let date = item.galleryDate
|
|
|
|
newGalleryItems.append(item)
|
|
|
|
|
|
|
|
} else {
|
|
|
|
|
|
|
|
sectionDates.append(date)
|
|
|
|
|
|
|
|
sections[date] = [item]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// so we can update collectionView
|
|
|
|
galleryItems.append(item)
|
|
|
|
newDates.append(date)
|
|
|
|
if sections[date] != nil {
|
|
|
|
newGalleryItems.append(item)
|
|
|
|
sections[date]!.append(item)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// so we can update collectionView
|
|
|
|
|
|
|
|
newGalleryItems.append(item)
|
|
|
|
|
|
|
|
} else {
|
|
|
|
|
|
|
|
sectionDates.append(date)
|
|
|
|
|
|
|
|
sections[date] = [item]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// so we can update collectionView
|
|
|
|
|
|
|
|
newDates.append(date)
|
|
|
|
|
|
|
|
newGalleryItems.append(item)
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
self.fetchedIndexSet = self.fetchedIndexSet.union(unfetchedSet)
|
|
|
|
self.fetchedIndexSet = self.fetchedIndexSet.union(unfetchedSet)
|
|
|
|
self.hasFetchedOldest = self.fetchedIndexSet.min() == 0
|
|
|
|
self.hasFetchedOldest = self.fetchedIndexSet.min() == 0
|
|
|
|
self.hasFetchedMostRecent = self.fetchedIndexSet.max() == mediaCount - 1
|
|
|
|
self.hasFetchedMostRecent = self.fetchedIndexSet.max() == mediaCount - 1
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
} catch MediaGalleryError.itemNoLongerExists {
|
|
|
|
|
|
|
|
Logger.debug("Ignoring reload, since item no longer exists.")
|
|
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
|
|
owsFailDebug("unexpected error: \(error)")
|
|
|
|
|
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// TODO only sort if changed
|
|
|
|
// TODO only sort if changed
|
|
|
@ -919,7 +1002,7 @@ class MediaGallery: NSObject, MediaGalleryDataSource, MediaTileViewControllerDel
|
|
|
|
dataSourceDelegates.append(Weak(value: dataSourceDelegate))
|
|
|
|
dataSourceDelegates.append(Weak(value: dataSourceDelegate))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func delete(items: [MediaGalleryItem], initiatedBy: MediaGalleryDataSourceDelegate) {
|
|
|
|
func delete(items: [MediaGalleryItem], initiatedBy: AnyObject) {
|
|
|
|
AssertIsOnMainThread()
|
|
|
|
AssertIsOnMainThread()
|
|
|
|
|
|
|
|
|
|
|
|
Logger.info("with items: \(items.map { ($0.attachmentStream, $0.message.timestamp) })")
|
|
|
|
Logger.info("with items: \(items.map { ($0.attachmentStream, $0.message.timestamp) })")
|
|
|
|