From 7a49a0fa82eb819f50acb154b5f73b2a0f4edcb4 Mon Sep 17 00:00:00 2001 From: george-signal <98181642+george-signal@users.noreply.github.com> Date: Wed, 16 Nov 2022 17:31:23 -0800 Subject: [PATCH] Adopt JournalingOrderedDictionary. (#5405) This change also moves mutation to the ordered dictionary into a separate queue. There is no change to behavior yet because the queue is only dispatched to synchronously so far. Doing this forces changes to how database transactions are acquired since they must be used on the same queue where they orignated. --- Pods | 2 +- .../JournalingOrderedDictionary.swift | 8 +- .../MediaGallery/MediaGallery.swift | 80 +- .../MediaGallery/MediaGallerySections.swift | 411 +++++++--- .../MediaTileViewController.swift | 36 +- .../JournalingOrderedDictionaryTests.swift | 5 +- .../MediaGallerySectionsTest.swift | 729 +++++++++--------- ThirdParty/RingRTC | 2 +- ThirdParty/WebRTC | 2 +- 9 files changed, 705 insertions(+), 570 deletions(-) diff --git a/Pods b/Pods index de19768f95..003ae542e2 160000 --- a/Pods +++ b/Pods @@ -1 +1 @@ -Subproject commit de19768f9502f2570a1a357d71b944c798f32450 +Subproject commit 003ae542e2bd1b9545d7de3bfaefcd40a23475bd diff --git a/Signal/src/ViewControllers/MediaGallery/JournalingOrderedDictionary.swift b/Signal/src/ViewControllers/MediaGallery/JournalingOrderedDictionary.swift index f434d46d88..7b214c5306 100644 --- a/Signal/src/ViewControllers/MediaGallery/JournalingOrderedDictionary.swift +++ b/Signal/src/ViewControllers/MediaGallery/JournalingOrderedDictionary.swift @@ -53,7 +53,6 @@ public struct JournalingOrderedDictionary [Change] { + defer { + journal = [] + } + return journal } public mutating func removeAll() { diff --git a/Signal/src/ViewControllers/MediaGallery/MediaGallery.swift b/Signal/src/ViewControllers/MediaGallery/MediaGallery.swift index 676c38fda2..130674f5a9 100644 --- a/Signal/src/ViewControllers/MediaGallery/MediaGallery.swift +++ b/Signal/src/ViewControllers/MediaGallery/MediaGallery.swift @@ -249,26 +249,8 @@ class MediaGallery: Dependencies { return } - var sectionIndexesNeedingUpdate = IndexSet() - var sectionsToDelete = IndexSet() + let (sectionIndexesNeedingUpdate, sectionsToDelete) = sections.reloadSections(for: sectionsNeedingUpdate) - databaseStorage.read { transaction in - for sectionDate in sectionsNeedingUpdate { - // Scan backwards; newer items are more likely to be modified. - // (We could use a binary search here as well.) - guard let sectionIndex = sections.sectionDates.lastIndex(of: sectionDate) else { - continue - } - - // Refresh the section. - let newCount = sections.reloadSection(for: sectionDate, transaction: transaction) - - sectionIndexesNeedingUpdate.insert(sectionIndex) - if newCount == 0 { - sectionsToDelete.insert(sectionIndex) - } - } - } delegates.forEach { $0.mediaGallery(self, didReloadItemsInSections: sectionIndexesNeedingUpdate) } @@ -295,49 +277,17 @@ class MediaGallery: Dependencies { } Logger.debug("") - var sectionsNeedingUpdate = IndexSet() - var didAddSectionAtEnd = false - var didReset = false - - databaseStorage.read { transaction in - for attachmentInfo in relevantAttachments { - let sectionDate = GalleryDate(date: Date(millisecondsSince1970: attachmentInfo.timestamp)) - // Do a backwards search assuming new messages usually arrive at the end. - // Still, this is kept sorted, so we ought to be able to do a binary search instead. - if let lastSectionDate = sections.sectionDates.last, sectionDate > lastSectionDate { - // Only let clients know about the new section if they thought they were at the end; - // otherwise they'll fetch more if they need to. - if sections.hasFetchedMostRecent { - sections.resetHasFetchedMostRecent() - didAddSectionAtEnd = true - } - } else if let sectionIndex = sections.sectionDates.lastIndex(of: sectionDate) { - sectionsNeedingUpdate.insert(sectionIndex) - } else { - // We've loaded the first attachment in a new section that's not at the end. That can't be done - // transparently in MediaGallery's model, so let all our delegates know to refresh *everything*. - // This should be rare, but can happen if someone has automatic attachment downloading off and then - // goes back and downloads an attachment that crosses the month boundary. - sections.reset(transaction: transaction) - didReset = true - return - } - } - - for sectionIndex in sectionsNeedingUpdate { - // Throw out everything in that section. - let sectionDate = sections.sectionDates[sectionIndex] - sections.reloadSection(for: sectionDate, transaction: transaction) - } + let dates = relevantAttachments.lazy.map { + GalleryDate(date: Date(millisecondsSince1970: $0.timestamp)) } - - if didReset { + let newAttachmentResult = sections.handleNewAttachments(dates) + if newAttachmentResult.didReset { delegates.forEach { $0.didReloadAllSectionsInMediaGallery(self) } } else { - if !sectionsNeedingUpdate.isEmpty { - delegates.forEach { $0.mediaGallery(self, didReloadItemsInSections: sectionsNeedingUpdate) } + if !newAttachmentResult.update.isEmpty { + delegates.forEach { $0.mediaGallery(self, didReloadItemsInSections: newAttachmentResult.update) } } - if didAddSectionAtEnd { + if newAttachmentResult.didAddAtEnd { delegates.forEach { $0.didAddSectionInMediaGallery(self) } } } @@ -467,7 +417,7 @@ class MediaGallery: Dependencies { if sections.isEmpty { // Set up the current section only. - sections.loadInitialSection(for: focusedItem.galleryDate, transaction: transaction) + sections.loadInitialSection(for: focusedItem.galleryDate) } return sections.getOrReplaceItem(focusedItem, offsetInSection: offsetInSection) @@ -498,8 +448,8 @@ class MediaGallery: Dependencies { /// Operates in bulk in an attempt to cut down on database traffic, meaning it may measure multiple sections at once. /// /// Returns the number of new sections loaded, which can be used to update section indexes. - internal func loadEarlierSections(batchSize: Int, transaction: SDSAnyReadTransaction) -> Int { - return sections.loadEarlierSections(batchSize: batchSize, transaction: transaction) + internal func loadEarlierSections(batchSize: Int) -> Int { + return sections.loadEarlierSections(batchSize: batchSize) } /// Loads at least one section after the latest section, though not any of the items in it. @@ -507,8 +457,8 @@ class MediaGallery: Dependencies { /// Operates in bulk in an attempt to cut down on database traffic, meaning it may measure multiple sections at once. /// /// Returns the number of new sections loaded. - internal func loadLaterSections(batchSize: Int, transaction: SDSAnyReadTransaction) -> Int { - return sections.loadLaterSections(batchSize: batchSize, transaction: transaction) + internal func loadLaterSections(batchSize: Int) -> Int { + return sections.loadLaterSections(batchSize: batchSize) } // MARK: - @@ -664,9 +614,7 @@ extension MediaGallery: DatabaseChangeDelegate { func databaseChangesDidUpdateExternally() { // Conservatively assume anything could have happened. - databaseStorage.read { transaction in - sections.reset(transaction: transaction) - } + sections.reset() delegates.forEach { $0.didReloadAllSectionsInMediaGallery(self) } } diff --git a/Signal/src/ViewControllers/MediaGallery/MediaGallerySections.swift b/Signal/src/ViewControllers/MediaGallery/MediaGallerySections.swift index 435b5495c5..da1d837c75 100644 --- a/Signal/src/ViewControllers/MediaGallery/MediaGallerySections.swift +++ b/Signal/src/ViewControllers/MediaGallery/MediaGallerySections.swift @@ -62,13 +62,13 @@ internal struct MediaGallerySections: Depende var item: Item? } - private struct State: Dependencies { + private struct State { /// All sections we know about. /// /// Each section contains an array of possibly-fetched items. /// The length of the array is always the correct number of items in the section. /// The keys are kept in sorted order. - var itemsBySection: OrderedDictionary = OrderedDictionary() + var itemsBySection: JournalingOrderedDictionary = JournalingOrderedDictionary() var hasFetchedOldest = false var hasFetchedMostRecent = false var loader: Loader @@ -200,7 +200,10 @@ internal struct MediaGallerySections: Depende offset: 0, ascending: true, transaction: transaction) - itemsBySection.replace(key: date, value: rowids.map { MediaGallerySlot(rowid: $0) }) + itemsBySection.replace(key: date) { + return (rowids.map { MediaGallerySlot(rowid: $0) }, + [.reloadSection]) + } return rowids.count } @@ -435,112 +438,122 @@ internal struct MediaGallerySections: Depende naiveRange.removeLast(countToTrim) } - /// Loads items in the given range. + internal struct LoadItemsRequest { + let trimmedRange: Range + let sectionIndex: Int + } + + /// Constructs a `LoadItemsRequest` if one is needed. Returns `nil` if no items need to be loaded in the requested range. /// - /// A "naive" range may start at a negative offset, representing a position in a section before `sectionIndex`. - /// Similarly, the endpoint may be in a section that follows `sectionIndex`. However, the range must *cross* - /// `sectionIndex`, or it is not considered valid. `sectionIndex` must refer to a loaded section. - /// - /// Will open its own database transaction, in the hopes of not having to open one at all. - /// - /// Returns the indexes of newly loaded *sections,* which could shift the indexes of existing sections. These will - /// always be before and/or after the existing sections, never interleaving. If `naiveRange.startIndex` is - /// non-negative, there will never be any sections loaded before the existing sections. That is: - /// - /// - When this method starts, we'll have sections like this: [G, H, … J]. - /// - When this method ends, we'll have sections like this: [C, D, … F] [G, H, … J] [K, L, … N], - /// where the CDF and KLN chunks could be empty. The returned index set will contain the indexes of C…F and K…N. - internal mutating func ensureItemsLoaded(in naiveRange: Range, - relativeToSection sectionIndex: Int) -> IndexSet { + /// - Parameters: + /// - naiveRange This may start at a negative offset, representing a position in a section before `sectionIndex`. Similarly, the endpoint may be in a section that follows `sectionIndex`. However, the range must *cross* `sectionIndex`, or it is not considered valid. `sectionIndex` must refer to a loaded section. + /// - sectionIndex: The section relative to which `naiveRange` is interpreted. + internal func loadItemsRequest(in naiveRange: Range, + relativeToSection sectionIndex: Int) -> LoadItemsRequest? { var trimmedRange = naiveRange trimLoadedItemsAtStart(from: &trimmedRange, relativeToSection: sectionIndex) trimLoadedItemsAtEnd(from: &trimmedRange, relativeToSection: sectionIndex) if trimmedRange.isEmpty { - return IndexSet() + return nil } + return LoadItemsRequest(trimmedRange: trimmedRange, sectionIndex: sectionIndex) + } - var numNewlyLoadedEarlierSections: Int = 0 - var numNewlyLoadedLaterSections: Int = 0 + /// Loads items specified in the request. + /// + /// Will open its own database transaction. + /// + /// If the start index in the request is non-negative, there will never be any sections loaded before the existing sections. That is: + /// - When this method starts, we'll have sections like this: [G, H, … J]. + /// - When this method ends, we'll have sections like this: [C, D, … F] [G, H, … J] [K, L, … N], + /// where the CDF and KLN chunks could be empty. The returned index set will contain the indexes of C…F and K…N. + /// + /// Returns the indexes of newly loaded *sections,* which could shift the indexes of existing sections. These will + /// always be before and/or after the existing sections, never interleaving. + internal mutating func ensureItemsLoaded(request: LoadItemsRequest, + transaction: SDSAnyReadTransaction) -> IndexSet { + return Bench(title: "fetching gallery items") { + owsAssert(!request.trimmedRange.isEmpty) - Bench(title: "fetching gallery items") { - self.databaseStorage.read { transaction in - // Figure out the earliest section this request will cross. - // Note that this is a bigger batch size than necessary: - // -trimmedRange.startIndex would be sufficient, - // and resolveNaiveStartIndex(...) could give us the exact offset we need. - // However, the media gallery is a scrolling collection view that most commonly starts at the - // present and scrolls up into the past; if we are ensuring loaded items in earlier sections, we are - // likely actively scrolling. Because of this, we potentially measure some extra sections here so - // that we don't have to on the immediate next call to ensureItemsLoaded(...). - let (requestStartPath, newlyLoadedCount) = resolveNaiveStartIndex(trimmedRange.startIndex, - relativeToSection: sectionIndex, - batchSize: trimmedRange.count, - transaction: transaction) - numNewlyLoadedEarlierSections = newlyLoadedCount - guard let requestStartPath = requestStartPath else { - owsFail("failed to resolve despite loading \(numNewlyLoadedEarlierSections) earlier sections") + var numNewlyLoadedEarlierSections: Int = 0 + var numNewlyLoadedLaterSections: Int = 0 + + // Figure out the earliest section this request will cross. + // Note that this is a bigger batch size than necessary: + // -trimmedRange.startIndex would be sufficient, + // and resolveNaiveStartIndex(...) could give us the exact offset we need. + // However, the media gallery is a scrolling collection view that most commonly starts at the + // present and scrolls up into the past; if we are ensuring loaded items in earlier sections, we are + // likely actively scrolling. Because of this, we potentially measure some extra sections here so + // that we don't have to on the immediate next call to ensureItemsLoaded(...). + let (requestStartPath, newlyLoadedCount) = resolveNaiveStartIndex(request.trimmedRange.startIndex, + relativeToSection: request.sectionIndex, + batchSize: request.trimmedRange.count, + transaction: transaction) + numNewlyLoadedEarlierSections = newlyLoadedCount + guard let requestStartPath = requestStartPath else { + owsFail("failed to resolve despite loading \(numNewlyLoadedEarlierSections) earlier sections") + } + + var currentSectionIndex = requestStartPath.section + let interval = DateInterval(start: itemsBySection.orderedKeys[currentSectionIndex].interval.start, + end: .distantFutureForMillisecondTimestamp) + let requestRange = requestStartPath.item ..< (requestStartPath.item + request.trimmedRange.count) + + var offset = 0 + loader.enumerateItems(in: interval, + range: requestRange, + transaction: transaction) { i, uniqueId, buildItem in + owsAssertDebug(i >= offset, "does not support reverse traversal") + + var (date, slots) = itemsBySection[currentSectionIndex] + var itemIndex = i - offset + + while itemIndex >= slots.count { + itemIndex -= slots.count + offset += slots.count + currentSectionIndex += 1 + + if currentSectionIndex >= itemsBySection.count { + if hasFetchedMostRecent { + // Ignore later attachments. + owsAssertDebug(itemsBySection.count == 1, "should only be used in single-album page view") + return + } + numNewlyLoadedLaterSections += loadLaterSections(batchSize: request.trimmedRange.count, + transaction: transaction) + if currentSectionIndex >= itemsBySection.count { + owsFailDebug("attachment #\(i) \(uniqueId) is beyond the last section") + return + } + } + + (date, slots) = itemsBySection[currentSectionIndex] } - var currentSectionIndex = requestStartPath.section - let interval = DateInterval(start: itemsBySection.orderedKeys[currentSectionIndex].interval.start, - end: .distantFutureForMillisecondTimestamp) - let requestRange = requestStartPath.item ..< (requestStartPath.item + trimmedRange.count) - - var offset = 0 - loader.enumerateItems(in: interval, - range: requestRange, - transaction: transaction) { i, uniqueId, buildItem in - owsAssertDebug(i >= offset, "does not support reverse traversal") - - var (date, slots) = itemsBySection[currentSectionIndex] - var itemIndex = i - offset - - while itemIndex >= slots.count { - itemIndex -= slots.count - offset += slots.count - currentSectionIndex += 1 - - if currentSectionIndex >= itemsBySection.count { - if hasFetchedMostRecent { - // Ignore later attachments. - owsAssertDebug(itemsBySection.count == 1, "should only be used in single-album page view") - return - } - numNewlyLoadedLaterSections += loadLaterSections(batchSize: trimmedRange.count, - transaction: transaction) - if currentSectionIndex >= itemsBySection.count { - owsFailDebug("attachment #\(i) \(uniqueId) is beyond the last section") - return - } - } - - (date, slots) = itemsBySection[currentSectionIndex] - } - - var slot = slots[itemIndex] - if let loadedItem = slot.item { - owsAssert(loadedItem.uniqueId == uniqueId) - return - } + var slot = slots[itemIndex] + if let loadedItem = slot.item { + owsAssert(loadedItem.uniqueId == uniqueId) + return + } + itemsBySection.replace(key: date) { let item = buildItem() owsAssertDebug(item.galleryDate == date, "item from \(item.galleryDate) put into section for \(date)") - // Performance hack: clear out the current 'items' array in 'sections' to avoid copy-on-write. - itemsBySection.replace(key: date, value: []) slot.item = item slots[itemIndex] = slot - itemsBySection.replace(key: date, value: slots) + return (slots, [.updateItem(index: itemIndex)]) } } - } - let firstNewLaterSectionIndex = itemsBySection.count - numNewlyLoadedLaterSections - var newlyLoadedSections = IndexSet() - newlyLoadedSections.insert(integersIn: 0.. Item? { @@ -559,9 +572,10 @@ internal struct MediaGallerySections: Depende } // Swap out the section items to avoid copy-on-write. - itemsBySection.replace(key: newItem.galleryDate, value: []) - items[offsetInSection].item = newItem - itemsBySection.replace(key: newItem.galleryDate, value: items) + itemsBySection.replace(key: newItem.galleryDate) { + items[offsetInSection].item = newItem + return (items, [.updateItem(index: offsetInSection)]) + } return newItem } @@ -576,17 +590,17 @@ internal struct MediaGallerySections: Depende for path in paths.sorted().reversed() { let sectionKey = itemsBySection.orderedKeys[path.section] // Swap out / swap in to avoid copy-on-write. - var section = itemsBySection.replace(key: sectionKey, value: []) + var section = itemsBySection[sectionKey]! + itemsBySection.replace(key: sectionKey) { + if section.remove(at: path.item).item == nil { + owsFailDebug("removed an item that wasn't loaded, which isn't permitted") + } - if section.remove(at: path.item).item == nil { - owsFailDebug("removed an item that wasn't loaded, which isn't permitted") - } - - if section.isEmpty { - itemsBySection.remove(at: path.section) - removedSections.insert(path.section) - } else { - itemsBySection.replace(key: sectionKey, value: section) + if section.isEmpty { + removedSections.insert(path.section) + return nil + } + return (section, [.removeItem(index: path.item)]) } } @@ -594,47 +608,119 @@ internal struct MediaGallerySections: Depende } } - private struct SnapshotManager { - private(set) var state: State + enum ItemChange: CustomDebugStringConvertible, Equatable { + var debugDescription: String { + switch self { + case .reloadSection: + return "reloadSection" + case .updateItem(index: let index): + return "updateItem(index: \(index))" + case .removeItem(index: let index): + return "removeItem(index: \(index))" + } + } + case reloadSection + case updateItem(index: Int) + case removeItem(index: Int) + } - init(_ state: State) { - self.state = state + private struct SnapshotManager: Dependencies { + private let queue: DispatchQueue + private var mutableState: State + private var snapshot: State + + /// Called on the main queue after a mutation. The snapshot will have + /// just been updated prior to this call. This is a good place to notify + /// UICollectionView of changes to its data source. + var didChange: (([JournalingOrderedDictionaryChange]) -> Void)? + + var state: State { + dispatchPrecondition(condition: .onQueue(.main)) + return snapshot + } + + init(_ queue: DispatchQueue, state: State) { + self.queue = queue + self.mutableState = state + self.snapshot = state } mutating func mutate(_ block: (inout State) -> (T)) -> T { - return block(&state) + dispatchPrecondition(condition: .onQueue(.main)) + var updatedState: State? + let (result, changes) = queue.sync { + let result = block(&mutableState) + updatedState = mutableState + return (result, mutableState.itemsBySection.takeJournal()) + } + snapshot = updatedState! + didChange?(changes) + return result + } + + mutating func mutate(_ block: (inout State, SDSAnyReadTransaction) -> (T)) -> T { + return mutate { mutableState in + return Self.databaseStorage.read { transaction in + return block(&mutableState, transaction) + } + } } } + private var snapshotManager: SnapshotManager private var state: State { snapshotManager.state } + private let mutationQueue = DispatchQueue(label: "org.signal.media-gallery-sections-mutation") internal init(loader: Loader) { - snapshotManager = SnapshotManager(State(loader: loader)) + snapshotManager = SnapshotManager(mutationQueue, state: State(loader: loader)) } // MARK: Sections - internal mutating func loadEarlierSections(batchSize: Int, transaction: SDSAnyReadTransaction) -> Int { - return snapshotManager.mutate { state in + internal mutating func loadEarlierSections(batchSize: Int) -> Int { + return snapshotManager.mutate { state, transaction in return state.loadEarlierSections(batchSize: batchSize, transaction: transaction) } } - internal mutating func loadLaterSections(batchSize: Int, transaction: SDSAnyReadTransaction) -> Int { - return snapshotManager.mutate { state in + internal mutating func loadLaterSections(batchSize: Int) -> Int { + return snapshotManager.mutate { state, transaction in return state.loadLaterSections(batchSize: batchSize, transaction: transaction) } } - internal mutating func loadInitialSection(for date: GalleryDate, transaction: SDSAnyReadTransaction) { - return snapshotManager.mutate { state in + internal mutating func loadInitialSection(for date: GalleryDate) { + return snapshotManager.mutate { state, transaction in return state.loadInitialSection(for: date, transaction: transaction) } } + internal mutating func reloadSections(for dates: Set) -> (update: IndexSet, delete: IndexSet) { + return snapshotManager.mutate { state, transaction in + var sectionIndexesNeedingUpdate = IndexSet() + var sectionsToDelete = IndexSet() + for sectionDate in dates { + // Scan backwards; newer items are more likely to be modified. + // (We could use a binary search here as well.) + guard let sectionIndex = state.itemsBySection.orderedKeys.lastIndex(of: sectionDate) else { + continue + } + + // Refresh the section. + let newCount = state.reloadSection(for: sectionDate, transaction: transaction) + + sectionIndexesNeedingUpdate.insert(sectionIndex) + if newCount == 0 { + sectionsToDelete.insert(sectionIndex) + } + } + return (update: sectionIndexesNeedingUpdate, delete: sectionsToDelete) + } + } + @discardableResult - internal mutating func reloadSection(for date: GalleryDate, transaction: SDSAnyReadTransaction) -> Int { - return snapshotManager.mutate { state in + internal mutating func reloadSection(for date: GalleryDate) -> Int { + return snapshotManager.mutate { state, transaction in return state.reloadSection(for: date, transaction: transaction) } } @@ -651,12 +737,55 @@ internal struct MediaGallerySections: Depende } } - internal mutating func reset(transaction: SDSAnyReadTransaction) { - return snapshotManager.mutate { state in + internal mutating func reset() { + return snapshotManager.mutate { state, transaction in state.reset(transaction: transaction) } } + internal struct NewAttachmentHandlingResult: Equatable { + var update = IndexSet() + var didAddAtEnd = false + var didReset = false + } + + internal mutating func handleNewAttachments(_ dates: some Sequence) -> NewAttachmentHandlingResult { + return snapshotManager.mutate { state, transaction in + var result = NewAttachmentHandlingResult() + + for sectionDate in dates { + // Do a backwards search assuming new messages usually arrive at the end. + // Still, this is kept sorted, so we ought to be able to do a binary search instead. + if let lastSectionDate = state.itemsBySection.orderedKeys.last, sectionDate > lastSectionDate { + // Only let clients know about the new section if they thought they were at the end; + // otherwise they'll fetch more if they need to. + if state.hasFetchedMostRecent { + state.resetHasFetchedMostRecent() + result.didAddAtEnd = true + } + } else if let sectionIndex = state.itemsBySection.orderedKeys.lastIndex(of: sectionDate) { + result.update.insert(sectionIndex) + } else { + // We've loaded the first attachment in a new section that's not at the end. That can't be done + // transparently in MediaGallery's model, so let all our delegates know to refresh *everything*. + // This should be rare, but can happen if someone has automatic attachment downloading off and then + // goes back and downloads an attachment that crosses the month boundary. + state.reset(transaction: transaction) + result.didReset = true + return result + } + } + + for sectionIndex in result.update { + // Throw out everything in that section. + let sectionDate = state.itemsBySection.orderedKeys[sectionIndex] + state.reloadSection(for: sectionDate, transaction: transaction) + } + + return result + } + } + // MARK: Items /// Returns the item at `path`, which will be `nil` if not yet loaded. @@ -745,10 +874,9 @@ internal struct MediaGallerySections: Depende internal mutating func resolveNaiveStartIndex( _ naiveIndex: Int, relativeToSection initialSectionIndex: Int, - batchSize: Int, - transaction: SDSAnyReadTransaction? + batchSize: Int ) -> (path: IndexPath?, numberOfSectionsLoaded: Int) { - return snapshotManager.mutate { state in + return snapshotManager.mutate { state, transaction in return state.resolveNaiveStartIndex(naiveIndex, relativeToSection: initialSectionIndex, batchSize: batchSize, @@ -772,9 +900,14 @@ internal struct MediaGallerySections: Depende internal mutating func ensureItemsLoaded(in naiveRange: Range, relativeToSection sectionIndex: Int) -> IndexSet { - return snapshotManager.mutate { state in - state.ensureItemsLoaded(in: naiveRange, - relativeToSection: sectionIndex) + return snapshotManager.mutate { state in + guard let request = state.loadItemsRequest(in: naiveRange, relativeToSection: sectionIndex) else { + return IndexSet() + } + return Self.databaseStorage.read { transaction in + return state.ensureItemsLoaded(request: request, + transaction: transaction) + } } } @@ -796,5 +929,33 @@ internal struct MediaGallerySections: Depende internal var sectionDates: [GalleryDate] { state.itemsBySection.orderedKeys } internal var hasFetchedMostRecent: Bool { state.hasFetchedMostRecent } internal var hasFetchedOldest: Bool { state.hasFetchedOldest } - internal var itemsBySection: OrderedDictionary { state.itemsBySection } + internal var itemsBySection: OrderedDictionary { state.itemsBySection.orderedDictionary } +} + +fileprivate extension JournalingOrderedDictionary where ValueType: ExpressibleByArrayLiteral { + /// This is a convenience method that provides a performance optimization to delete or set the value of an already-existing key. + /// By using this method, you avoid having two different copies of the value in memory. That could cause a copy-on-write for a value that's about to disappear. + /// A closure rather than a value is used so the value in the underlying dictionary can be removed before forcing the new value to exist. + /// + /// Example usage: + /// + /// if var value = dict["my key"] { // value is of type [String] + /// dict.replace("my key") { + /// value.append("foo") + /// return (value, [.replace]) + /// } + /// } + /// + /// If the closure returns `nil` then the key is removed from the dictionary. + mutating func replace(key: KeyType, closure: () -> (ValueType, [ChangeType])?) { + let i = orderedKeys.firstIndex(of: key)! + // Performance hack: clear out the current 'items' array in 'sections' to avoid copy-on-write. + replaceValue(at: i, value: [], changes: []) + if let tuple = closure() { + let (newValue, changes) = tuple + replaceValue(at: i, value: newValue, changes: changes) + } else { + remove(at: i) + } + } } diff --git a/Signal/src/ViewControllers/MediaGallery/MediaTileViewController.swift b/Signal/src/ViewControllers/MediaGallery/MediaTileViewController.swift index c2dcede6bc..f4145a40fd 100644 --- a/Signal/src/ViewControllers/MediaGallery/MediaTileViewController.swift +++ b/Signal/src/ViewControllers/MediaGallery/MediaTileViewController.swift @@ -106,9 +106,7 @@ public class MediaTileViewController: UICollectionViewController, MediaGalleryDe defer { super.viewWillAppear(animated) } if mediaGallery.galleryDates.isEmpty { - databaseStorage.read { transaction in - _ = self.mediaGallery.loadEarlierSections(batchSize: kLoadBatchSize, transaction: transaction) - } + _ = self.mediaGallery.loadEarlierSections(batchSize: kLoadBatchSize) if mediaGallery.galleryDates.isEmpty { // There must be no media. return @@ -704,9 +702,7 @@ public class MediaTileViewController: UICollectionViewController, MediaGalleryDe func didAddSectionInMediaGallery(_ mediaGallery: MediaGallery) { let oldSectionCount = self.numberOfSections(in: collectionView) - databaseStorage.read { transaction in - _ = mediaGallery.loadLaterSections(batchSize: kLoadBatchSize, transaction: transaction) - } + _ = mediaGallery.loadLaterSections(batchSize: kLoadBatchSize) let newSectionCount = self.numberOfSections(in: collectionView) collectionView.insertSections(IndexSet(oldSectionCount.. - switch direction { - case .before: - newSections = 0.. + switch direction { + case .before: + newSections = 0..() sut.append(key: .a, value: "a") sut.append(key: .b, value: "b") - sut.eraseJournal() + let journal = sut.takeJournal() + XCTAssertEqual(journal, [.append, .append]) XCTAssertEqual(sut.orderedKeys, [.a, .b]) XCTAssertEqual(sut[.a], "a") XCTAssertEqual(sut[.b], "b") diff --git a/Signal/test/ViewControllers/MediaGallerySectionsTest.swift b/Signal/test/ViewControllers/MediaGallerySectionsTest.swift index 233c38ed40..df8ba750df 100644 --- a/Signal/test/ViewControllers/MediaGallerySectionsTest.swift +++ b/Signal/test/ViewControllers/MediaGallerySectionsTest.swift @@ -66,6 +66,12 @@ private struct FakeItem: MediaGallerySectionItem, Equatable { self.uniqueId = UUID().uuidString self.timestamp = Date(compressedDate: compressedDate) } + + init(_ compressedDate: UInt32, uniqueId: String?, rowid: Int64) { + self.rowid = rowid + self.uniqueId = uniqueId ?? UUID().uuidString + self.timestamp = Date(compressedDate: compressedDate) + } } /// Takes the place of the database for MediaGallerySection tests. @@ -96,6 +102,12 @@ private final class FakeGalleryStore: MediaGallerySectionLoader { self.itemsBySection = OrderedDictionary(keyValueMap: itemsBySection, orderedKeys: itemsBySection.keys.sorted()) } + func set(items: [Item]) { + self.allItems = items.sorted { $0.timestamp < $1.timestamp } + let itemsBySection = Dictionary(grouping: allItems) { $0.galleryDate } + self.itemsBySection = OrderedDictionary(keyValueMap: itemsBySection, orderedKeys: itemsBySection.keys.sorted()) + } + func clone() -> Self { return Self(self.allItems) } @@ -372,30 +384,28 @@ class MediaGallerySectionsTest: SignalBaseTest { XCTAssertFalse(sections.hasFetchedOldest) XCTAssertFalse(sections.hasFetchedMostRecent) - databaseStorage.read { transaction in - XCTAssertEqual(1, sections.loadEarlierSections(batchSize: 4, transaction: transaction)) - XCTAssertEqual(1, sections.itemsBySection.count) - XCTAssertEqual([GalleryDate(2021_09_01)], sections.itemsBySection.orderedKeys) - XCTAssertEqual([5], sections.itemsBySection.orderedValues.map { $0.count }) - XCTAssertFalse(sections.hasFetchedOldest) - XCTAssertTrue(sections.hasFetchedMostRecent) + XCTAssertEqual(1, sections.loadEarlierSections(batchSize: 4)) + XCTAssertEqual(1, sections.itemsBySection.count) + XCTAssertEqual([GalleryDate(2021_09_01)], sections.itemsBySection.orderedKeys) + XCTAssertEqual([5], sections.itemsBySection.orderedValues.map { $0.count }) + XCTAssertFalse(sections.hasFetchedOldest) + XCTAssertTrue(sections.hasFetchedMostRecent) - XCTAssertEqual(2, sections.loadEarlierSections(batchSize: 4, transaction: transaction)) - XCTAssertEqual(3, sections.itemsBySection.count) - XCTAssertEqual([GalleryDate(2021_01_01), GalleryDate(2021_04_01), GalleryDate(2021_09_01)], - sections.itemsBySection.orderedKeys) - XCTAssertEqual([3, 2, 5], sections.itemsBySection.orderedValues.map { $0.count }) - XCTAssertFalse(sections.hasFetchedOldest) - XCTAssertTrue(sections.hasFetchedMostRecent) + XCTAssertEqual(2, sections.loadEarlierSections(batchSize: 4)) + XCTAssertEqual(3, sections.itemsBySection.count) + XCTAssertEqual([GalleryDate(2021_01_01), GalleryDate(2021_04_01), GalleryDate(2021_09_01)], + sections.itemsBySection.orderedKeys) + XCTAssertEqual([3, 2, 5], sections.itemsBySection.orderedValues.map { $0.count }) + XCTAssertFalse(sections.hasFetchedOldest) + XCTAssertTrue(sections.hasFetchedMostRecent) - XCTAssertEqual(0, sections.loadEarlierSections(batchSize: 4, transaction: transaction)) - XCTAssertEqual(3, sections.itemsBySection.count) - XCTAssertEqual([GalleryDate(2021_01_01), GalleryDate(2021_04_01), GalleryDate(2021_09_01)], - sections.itemsBySection.orderedKeys) - XCTAssertEqual([3, 2, 5], sections.itemsBySection.orderedValues.map { $0.count }) - XCTAssertTrue(sections.hasFetchedOldest) - XCTAssertTrue(sections.hasFetchedMostRecent) - } + XCTAssertEqual(0, sections.loadEarlierSections(batchSize: 4)) + XCTAssertEqual(3, sections.itemsBySection.count) + XCTAssertEqual([GalleryDate(2021_01_01), GalleryDate(2021_04_01), GalleryDate(2021_09_01)], + sections.itemsBySection.orderedKeys) + XCTAssertEqual([3, 2, 5], sections.itemsBySection.orderedValues.map { $0.count }) + XCTAssertTrue(sections.hasFetchedOldest) + XCTAssertTrue(sections.hasFetchedMostRecent) } func testLoadSectionsForward() { @@ -404,143 +414,136 @@ class MediaGallerySectionsTest: SignalBaseTest { XCTAssertFalse(sections.hasFetchedOldest) XCTAssertFalse(sections.hasFetchedMostRecent) - databaseStorage.read { transaction in - XCTAssertEqual(2, sections.loadLaterSections(batchSize: 4, transaction: transaction)) - XCTAssertEqual(2, sections.itemsBySection.count) - XCTAssertEqual([GalleryDate(2021_01_01), GalleryDate(2021_04_01)], sections.itemsBySection.orderedKeys) - XCTAssertEqual([3, 2], sections.itemsBySection.orderedValues.map { $0.count }) - XCTAssertTrue(sections.hasFetchedOldest) - XCTAssertFalse(sections.hasFetchedMostRecent) + XCTAssertEqual(2, sections.loadLaterSections(batchSize: 4)) + XCTAssertEqual(2, sections.itemsBySection.count) + XCTAssertEqual([GalleryDate(2021_01_01), GalleryDate(2021_04_01)], sections.itemsBySection.orderedKeys) + XCTAssertEqual([3, 2], sections.itemsBySection.orderedValues.map { $0.count }) + XCTAssertTrue(sections.hasFetchedOldest) + XCTAssertFalse(sections.hasFetchedMostRecent) - XCTAssertEqual(1, sections.loadLaterSections(batchSize: 4, transaction: transaction)) - XCTAssertEqual(3, sections.itemsBySection.count) - XCTAssertEqual([GalleryDate(2021_01_01), GalleryDate(2021_04_01), GalleryDate(2021_09_01)], - sections.itemsBySection.orderedKeys) - XCTAssertEqual([3, 2, 5], sections.itemsBySection.orderedValues.map { $0.count }) - XCTAssertTrue(sections.hasFetchedOldest) - XCTAssertFalse(sections.hasFetchedMostRecent) + XCTAssertEqual(1, sections.loadLaterSections(batchSize: 4)) + XCTAssertEqual(3, sections.itemsBySection.count) + XCTAssertEqual([GalleryDate(2021_01_01), GalleryDate(2021_04_01), GalleryDate(2021_09_01)], + sections.itemsBySection.orderedKeys) + XCTAssertEqual([3, 2, 5], sections.itemsBySection.orderedValues.map { $0.count }) + XCTAssertTrue(sections.hasFetchedOldest) + XCTAssertFalse(sections.hasFetchedMostRecent) - XCTAssertEqual(0, sections.loadLaterSections(batchSize: 4, transaction: transaction)) - XCTAssertEqual(3, sections.itemsBySection.count) - XCTAssertEqual([GalleryDate(2021_01_01), GalleryDate(2021_04_01), GalleryDate(2021_09_01)], - sections.itemsBySection.orderedKeys) - XCTAssertEqual([3, 2, 5], sections.itemsBySection.orderedValues.map { $0.count }) - XCTAssertTrue(sections.hasFetchedOldest) - XCTAssertTrue(sections.hasFetchedMostRecent) - } + XCTAssertEqual(0, sections.loadLaterSections(batchSize: 4)) + XCTAssertEqual(3, sections.itemsBySection.count) + XCTAssertEqual([GalleryDate(2021_01_01), GalleryDate(2021_04_01), GalleryDate(2021_09_01)], + sections.itemsBySection.orderedKeys) + XCTAssertEqual([3, 2, 5], sections.itemsBySection.orderedValues.map { $0.count }) + XCTAssertTrue(sections.hasFetchedOldest) + XCTAssertTrue(sections.hasFetchedMostRecent) } func testStartIndexResolution() { var sections = MediaGallerySections(loader: standardFakeStore) - databaseStorage.read { transaction in - // Load April and September - XCTAssertEqual(2, sections.loadEarlierSections(batchSize: 6, transaction: transaction)) - XCTAssertFalse(sections.hasFetchedOldest) + // Load April and September + XCTAssertEqual(2, sections.loadEarlierSections(batchSize: 6)) + XCTAssertFalse(sections.hasFetchedOldest) - XCTAssertEqual(IndexPath(item: 0, section: 1), - sections.resolveNaiveStartIndex(0, relativeToSection: 1)) - XCTAssertEqual(IndexPath(item: 4, section: 1), - sections.resolveNaiveStartIndex(4, relativeToSection: 1)) - XCTAssertEqual(IndexPath(item: 5, section: 1), - sections.resolveNaiveStartIndex(5, relativeToSection: 1)) + XCTAssertEqual(IndexPath(item: 0, section: 1), + sections.resolveNaiveStartIndex(0, relativeToSection: 1)) + XCTAssertEqual(IndexPath(item: 4, section: 1), + sections.resolveNaiveStartIndex(4, relativeToSection: 1)) + XCTAssertEqual(IndexPath(item: 5, section: 1), + sections.resolveNaiveStartIndex(5, relativeToSection: 1)) - XCTAssertEqual(IndexPath(item: 1, section: 0), - sections.resolveNaiveStartIndex(-1, relativeToSection: 1)) - XCTAssertEqual(IndexPath(item: 0, section: 0), - sections.resolveNaiveStartIndex(-2, relativeToSection: 1)) - XCTAssertNil(sections.resolveNaiveStartIndex(-3, relativeToSection: 1)) + XCTAssertEqual(IndexPath(item: 1, section: 0), + sections.resolveNaiveStartIndex(-1, relativeToSection: 1)) + XCTAssertEqual(IndexPath(item: 0, section: 0), + sections.resolveNaiveStartIndex(-2, relativeToSection: 1)) + XCTAssertNil(sections.resolveNaiveStartIndex(-3, relativeToSection: 1)) - // Load January - do { - let actual = sections.resolveNaiveStartIndex(-3, relativeToSection: 1, batchSize: 1, transaction: transaction) - XCTAssertEqual(IndexPath(item: 2, section: 0), actual.path) - XCTAssertEqual(1, actual.numberOfSectionsLoaded) - } - XCTAssertFalse(sections.hasFetchedOldest) - - XCTAssertEqual(IndexPath(item: 2, section: 0), - sections.resolveNaiveStartIndex(-3, relativeToSection: 2)) - XCTAssertEqual(IndexPath(item: 0, section: 0), - sections.resolveNaiveStartIndex(-5, relativeToSection: 2)) - XCTAssertNil(sections.resolveNaiveStartIndex(-6, relativeToSection: 2)) - - // Find out that January was the earliest section. - do { - let actual = sections.resolveNaiveStartIndex(-6, relativeToSection: 2, batchSize: 1, transaction: transaction) - XCTAssertEqual(IndexPath(item: 0, section: 0), actual.0) - XCTAssertEqual(0, actual.1) - } - - XCTAssertTrue(sections.hasFetchedOldest) - - XCTAssertEqual(IndexPath(item: 0, section: 0), - sections.resolveNaiveStartIndex(-6, relativeToSection: 2)) + // Load January + do { + let actual = sections.resolveNaiveStartIndex(-3, relativeToSection: 1, batchSize: 1) + XCTAssertEqual(IndexPath(item: 2, section: 0), actual.path) + XCTAssertEqual(1, actual.numberOfSectionsLoaded) } + XCTAssertFalse(sections.hasFetchedOldest) + + XCTAssertEqual(IndexPath(item: 2, section: 0), + sections.resolveNaiveStartIndex(-3, relativeToSection: 2)) + XCTAssertEqual(IndexPath(item: 0, section: 0), + sections.resolveNaiveStartIndex(-5, relativeToSection: 2)) + XCTAssertNil(sections.resolveNaiveStartIndex(-6, relativeToSection: 2)) + + // Find out that January was the earliest section. + do { + let actual = sections.resolveNaiveStartIndex(-6, relativeToSection: 2, batchSize: 1) + XCTAssertEqual(IndexPath(item: 0, section: 0), actual.0) + XCTAssertEqual(0, actual.1) + } + + XCTAssertTrue(sections.hasFetchedOldest) + + XCTAssertEqual(IndexPath(item: 0, section: 0), + sections.resolveNaiveStartIndex(-6, relativeToSection: 2)) } func testEndIndexResolution() { var sections = MediaGallerySections(loader: standardFakeStore) - databaseStorage.read { transaction in - // Load January and April - XCTAssertEqual(2, sections.loadLaterSections(batchSize: 4, transaction: transaction)) - XCTAssertFalse(sections.hasFetchedMostRecent) + // Load January and April + XCTAssertEqual(2, sections.loadLaterSections(batchSize: 4)) + XCTAssertFalse(sections.hasFetchedMostRecent) - XCTAssertEqual(IndexPath(item: 0, section: 0), - sections.resolveNaiveEndIndex(0, relativeToSection: 0)) - XCTAssertEqual(IndexPath(item: 2, section: 0), - sections.resolveNaiveEndIndex(2, relativeToSection: 0)) - // Note: (0, 3) rather than (1, 0), because this is an end index. - XCTAssertEqual(IndexPath(item: 3, section: 0), - sections.resolveNaiveEndIndex(3, relativeToSection: 0)) - XCTAssertEqual(IndexPath(item: 1, section: 1), - sections.resolveNaiveEndIndex(4, relativeToSection: 0)) - // Note: (1, 2) rather than nil. - XCTAssertEqual(IndexPath(item: 2, section: 1), - sections.resolveNaiveEndIndex(5, relativeToSection: 0)) - XCTAssertNil(sections.resolveNaiveEndIndex(6, relativeToSection: 0)) + XCTAssertEqual(IndexPath(item: 0, section: 0), + sections.resolveNaiveEndIndex(0, relativeToSection: 0)) + XCTAssertEqual(IndexPath(item: 2, section: 0), + sections.resolveNaiveEndIndex(2, relativeToSection: 0)) + // Note: (0, 3) rather than (1, 0), because this is an end index. + XCTAssertEqual(IndexPath(item: 3, section: 0), + sections.resolveNaiveEndIndex(3, relativeToSection: 0)) + XCTAssertEqual(IndexPath(item: 1, section: 1), + sections.resolveNaiveEndIndex(4, relativeToSection: 0)) + // Note: (1, 2) rather than nil. + XCTAssertEqual(IndexPath(item: 2, section: 1), + sections.resolveNaiveEndIndex(5, relativeToSection: 0)) + XCTAssertNil(sections.resolveNaiveEndIndex(6, relativeToSection: 0)) - // Load September - XCTAssertEqual(1, sections.loadLaterSections(batchSize: 20, transaction: transaction)) - XCTAssertTrue(sections.hasFetchedMostRecent) + // Load September + XCTAssertEqual(1, sections.loadLaterSections(batchSize: 20)) + XCTAssertTrue(sections.hasFetchedMostRecent) - XCTAssertEqual(IndexPath(item: 2, section: 1), - sections.resolveNaiveEndIndex(5, relativeToSection: 0)) - XCTAssertEqual(IndexPath(item: 1, section: 2), - sections.resolveNaiveEndIndex(6, relativeToSection: 0)) - XCTAssertEqual(IndexPath(item: 4, section: 2), - sections.resolveNaiveEndIndex(9, relativeToSection: 0)) - XCTAssertEqual(IndexPath(item: 5, section: 2), - sections.resolveNaiveEndIndex(10, relativeToSection: 0)) - // Reached end. - XCTAssertEqual(IndexPath(item: 5, section: 2), - sections.resolveNaiveEndIndex(11, relativeToSection: 0)) - XCTAssertEqual(IndexPath(item: 5, section: 2), - sections.resolveNaiveEndIndex(12, relativeToSection: 0)) + XCTAssertEqual(IndexPath(item: 2, section: 1), + sections.resolveNaiveEndIndex(5, relativeToSection: 0)) + XCTAssertEqual(IndexPath(item: 1, section: 2), + sections.resolveNaiveEndIndex(6, relativeToSection: 0)) + XCTAssertEqual(IndexPath(item: 4, section: 2), + sections.resolveNaiveEndIndex(9, relativeToSection: 0)) + XCTAssertEqual(IndexPath(item: 5, section: 2), + sections.resolveNaiveEndIndex(10, relativeToSection: 0)) + // Reached end. + XCTAssertEqual(IndexPath(item: 5, section: 2), + sections.resolveNaiveEndIndex(11, relativeToSection: 0)) + XCTAssertEqual(IndexPath(item: 5, section: 2), + sections.resolveNaiveEndIndex(12, relativeToSection: 0)) - XCTAssertEqual(IndexPath(item: 1, section: 1), - sections.resolveNaiveEndIndex(1, relativeToSection: 1)) - XCTAssertEqual(IndexPath(item: 2, section: 1), - sections.resolveNaiveEndIndex(2, relativeToSection: 1)) - XCTAssertEqual(IndexPath(item: 1, section: 2), - sections.resolveNaiveEndIndex(3, relativeToSection: 1)) - XCTAssertEqual(IndexPath(item: 2, section: 2), - sections.resolveNaiveEndIndex(4, relativeToSection: 1)) - XCTAssertEqual(IndexPath(item: 5, section: 2), - sections.resolveNaiveEndIndex(10, relativeToSection: 1)) - } + XCTAssertEqual(IndexPath(item: 1, section: 1), + sections.resolveNaiveEndIndex(1, relativeToSection: 1)) + XCTAssertEqual(IndexPath(item: 2, section: 1), + sections.resolveNaiveEndIndex(2, relativeToSection: 1)) + XCTAssertEqual(IndexPath(item: 1, section: 2), + sections.resolveNaiveEndIndex(3, relativeToSection: 1)) + XCTAssertEqual(IndexPath(item: 2, section: 2), + sections.resolveNaiveEndIndex(4, relativeToSection: 1)) + XCTAssertEqual(IndexPath(item: 5, section: 2), + sections.resolveNaiveEndIndex(10, relativeToSection: 1)) } func testLoadingFromEnd() { let store = standardFakeStore var sections = MediaGallerySections(loader: store) - databaseStorage.read { transaction in - // Load April and September - XCTAssertEqual(2, sections.loadEarlierSections(batchSize: 6, transaction: transaction)) - XCTAssertFalse(sections.hasFetchedOldest) - XCTAssertEqual(2, sections.itemsBySection.count) - XCTAssertEqual([3, 4], sections.itemsBySection[0].value.map { $0.rowid }) - XCTAssertEqual([5, 6, 7, 8, 9], sections.itemsBySection[1].value.map { $0.rowid }) - } + + // Load April and September + XCTAssertEqual(2, sections.loadEarlierSections(batchSize: 6)) + XCTAssertFalse(sections.hasFetchedOldest) + XCTAssertEqual(2, sections.itemsBySection.count) + XCTAssertEqual([3, 4], sections.itemsBySection[0].value.map { $0.rowid }) + XCTAssertEqual([5, 6, 7, 8, 9], sections.itemsBySection[1].value.map { $0.rowid }) XCTAssert(sections.ensureItemsLoaded(in: 1..<3, relativeToSection: 1).isEmpty) XCTAssertEqual(2, sections.itemsBySection.count) @@ -567,12 +570,10 @@ class MediaGallerySectionsTest: SignalBaseTest { func testLoadingFromEndInBigJump() { let store = standardFakeStore var sections = MediaGallerySections(loader: store) - databaseStorage.read { transaction in - // Load September - XCTAssertEqual(1, sections.loadEarlierSections(batchSize: 1, transaction: transaction)) - XCTAssertFalse(sections.hasFetchedOldest) - XCTAssertEqual(1, sections.itemsBySection.count) - } + // Load September + XCTAssertEqual(1, sections.loadEarlierSections(batchSize: 1)) + XCTAssertFalse(sections.hasFetchedOldest) + XCTAssertEqual(1, sections.itemsBySection.count) XCTAssert(sections.ensureItemsLoaded(in: 1..<3, relativeToSection: 0).isEmpty) XCTAssertEqual(1, sections.itemsBySection.count) @@ -588,12 +589,11 @@ class MediaGallerySectionsTest: SignalBaseTest { func testLoadingFromStart() { let store = standardFakeStore var sections = MediaGallerySections(loader: store) - databaseStorage.read { transaction in - // Load January and April - XCTAssertEqual(2, sections.loadLaterSections(batchSize: 4, transaction: transaction)) - XCTAssertFalse(sections.hasFetchedMostRecent) - XCTAssertEqual(2, sections.itemsBySection.count) - } + + // Load January and April + XCTAssertEqual(2, sections.loadLaterSections(batchSize: 4)) + XCTAssertFalse(sections.hasFetchedMostRecent) + XCTAssertEqual(2, sections.itemsBySection.count) XCTAssert(sections.ensureItemsLoaded(in: 1..<3, relativeToSection: 0).isEmpty) XCTAssertEqual(2, sections.itemsBySection.count) @@ -620,12 +620,11 @@ class MediaGallerySectionsTest: SignalBaseTest { func testLoadingFromStartInBigJump() { let store = standardFakeStore var sections = MediaGallerySections(loader: store) - databaseStorage.read { transaction in - // Load January - XCTAssertEqual(1, sections.loadLaterSections(batchSize: 1, transaction: transaction)) - XCTAssertFalse(sections.hasFetchedMostRecent) - XCTAssertEqual(1, sections.itemsBySection.count) - } + + // Load January + XCTAssertEqual(1, sections.loadLaterSections(batchSize: 1)) + XCTAssertFalse(sections.hasFetchedMostRecent) + XCTAssertEqual(1, sections.itemsBySection.count) XCTAssert(sections.ensureItemsLoaded(in: 1..<3, relativeToSection: 0).isEmpty) XCTAssertEqual(1, sections.itemsBySection.count) @@ -645,10 +644,9 @@ class MediaGallerySectionsTest: SignalBaseTest { func testLoadingFromMiddle() { let store = standardFakeStore var sections = MediaGallerySections(loader: store) - databaseStorage.read { transaction in - sections.loadInitialSection(for: GalleryDate(2021_04_01), transaction: transaction) - XCTAssertEqual(1, sections.itemsBySection.count) - } + + sections.loadInitialSection(for: GalleryDate(2021_04_01)) + XCTAssertEqual(1, sections.itemsBySection.count) XCTAssert(sections.ensureItemsLoaded(in: 1..<2, relativeToSection: 0).isEmpty) XCTAssertEqual(1, sections.itemsBySection.count) @@ -665,12 +663,11 @@ class MediaGallerySectionsTest: SignalBaseTest { func testReloadSection() { let store = standardFakeStore var sections = MediaGallerySections(loader: store) - databaseStorage.read { transaction in - // Load January - XCTAssertEqual(1, sections.loadLaterSections(batchSize: 1, transaction: transaction)) - XCTAssertFalse(sections.hasFetchedMostRecent) - XCTAssertEqual(1, sections.itemsBySection.count) - } + + // Load January + XCTAssertEqual(1, sections.loadLaterSections(batchSize: 1)) + XCTAssertFalse(sections.hasFetchedMostRecent) + XCTAssertEqual(1, sections.itemsBySection.count) XCTAssertEqual(IndexSet(integersIn: 1...2), sections.ensureItemsLoaded(in: 3..<7, relativeToSection: 0)) XCTAssertEqual(3, sections.itemsBySection.count) @@ -678,24 +675,21 @@ class MediaGallerySectionsTest: SignalBaseTest { XCTAssertEqual([store.allItems[3], store.allItems[4]], sections.itemsBySection[1].value.map { $0.item }) XCTAssertEqual([store.allItems[5], store.allItems[6], nil, nil, nil], sections.itemsBySection[2].value.map { $0.item }) - databaseStorage.read { transaction in - XCTAssertEqual(2, sections.reloadSection(for: GalleryDate(2021_04_01), transaction: transaction)) - XCTAssertEqual(3, sections.itemsBySection.count) - XCTAssertEqual([nil, nil, nil], sections.itemsBySection[0].value.map { $0.item }) - XCTAssertEqual([nil, nil], sections.itemsBySection[1].value.map { $0.item }) - XCTAssertEqual([store.allItems[5], store.allItems[6], nil, nil, nil], sections.itemsBySection[2].value.map { $0.item }) - } + XCTAssertEqual(2, sections.reloadSection(for: GalleryDate(2021_04_01))) + XCTAssertEqual(3, sections.itemsBySection.count) + XCTAssertEqual([nil, nil, nil], sections.itemsBySection[0].value.map { $0.item }) + XCTAssertEqual([nil, nil], sections.itemsBySection[1].value.map { $0.item }) + XCTAssertEqual([store.allItems[5], store.allItems[6], nil, nil, nil], sections.itemsBySection[2].value.map { $0.item }) } func testRemoveEmptySections() { let store = standardFakeStore.clone() var sections = MediaGallerySections(loader: store) - databaseStorage.read { transaction in - // Load January - XCTAssertEqual(1, sections.loadLaterSections(batchSize: 1, transaction: transaction)) - XCTAssertFalse(sections.hasFetchedMostRecent) - XCTAssertEqual(1, sections.itemsBySection.count) - } + + // Load January + XCTAssertEqual(1, sections.loadLaterSections(batchSize: 1)) + XCTAssertFalse(sections.hasFetchedMostRecent) + XCTAssertEqual(1, sections.itemsBySection.count) XCTAssertEqual(IndexSet(integersIn: 1...2), sections.ensureItemsLoaded(in: 3..<7, relativeToSection: 0)) XCTAssertEqual(3, sections.itemsBySection.count) @@ -704,26 +698,22 @@ class MediaGallerySectionsTest: SignalBaseTest { store.itemsBySection.replace(key: GalleryDate(2021_01_01), value: []) store.itemsBySection.replace(key: GalleryDate(2021_09_01), value: []) - databaseStorage.read { transaction in - XCTAssertEqual(0, sections.reloadSection(for: GalleryDate(2021_01_01), transaction: transaction)) - XCTAssertEqual(0, sections.reloadSection(for: GalleryDate(2021_09_01), transaction: transaction)) - XCTAssertEqual([], sections.itemsBySection[0].value.map { $0.item }) - XCTAssertEqual([store.allItems[3], store.allItems[4]], sections.itemsBySection[1].value.map { $0.item }) - XCTAssertEqual([], sections.itemsBySection[2].value.map { $0.item }) + XCTAssertEqual(0, sections.reloadSection(for: GalleryDate(2021_01_01))) + XCTAssertEqual(0, sections.reloadSection(for: GalleryDate(2021_09_01))) + XCTAssertEqual([], sections.itemsBySection[0].value.map { $0.item }) + XCTAssertEqual([store.allItems[3], store.allItems[4]], sections.itemsBySection[1].value.map { $0.item }) + XCTAssertEqual([], sections.itemsBySection[2].value.map { $0.item }) - sections.removeEmptySections(atIndexes: IndexSet([0, 2])) - XCTAssertEqual(1, sections.itemsBySection.count) - XCTAssertEqual([GalleryDate(2021_04_01)], sections.itemsBySection.orderedKeys) - XCTAssertEqual([store.allItems[3], store.allItems[4]], sections.itemsBySection[0].value.map { $0.item }) - } + sections.removeEmptySections(atIndexes: IndexSet([0, 2])) + XCTAssertEqual(1, sections.itemsBySection.count) + XCTAssertEqual([GalleryDate(2021_04_01)], sections.itemsBySection.orderedKeys) + XCTAssertEqual([store.allItems[3], store.allItems[4]], sections.itemsBySection[0].value.map { $0.item }) } func testResetWhenEmpty() { let store = standardFakeStore var sections = MediaGallerySections(loader: store) - databaseStorage.read { transaction in - sections.reset(transaction: transaction) - } + sections.reset() XCTAssert(sections.itemsBySection.isEmpty) XCTAssertFalse(sections.hasFetchedOldest) @@ -733,16 +723,13 @@ class MediaGallerySectionsTest: SignalBaseTest { func testResetOneSection() { let store = standardFakeStore var sections = MediaGallerySections(loader: store) - databaseStorage.read { transaction in - // Load January - XCTAssertEqual(1, sections.loadLaterSections(batchSize: 1, transaction: transaction)) - XCTAssertFalse(sections.hasFetchedMostRecent) - XCTAssertEqual(1, sections.itemsBySection.count) - } - databaseStorage.read { transaction in - sections.reset(transaction: transaction) - } + // Load January + XCTAssertEqual(1, sections.loadLaterSections(batchSize: 1)) + XCTAssertFalse(sections.hasFetchedMostRecent) + XCTAssertEqual(1, sections.itemsBySection.count) + + sections.reset() XCTAssertFalse(sections.hasFetchedOldest) XCTAssertFalse(sections.hasFetchedMostRecent) @@ -753,21 +740,18 @@ class MediaGallerySectionsTest: SignalBaseTest { func testResetTwoSections() { let store = standardFakeStore var sections = MediaGallerySections(loader: store) - databaseStorage.read { transaction in - // Load January - XCTAssertEqual(1, sections.loadLaterSections(batchSize: 1, transaction: transaction)) - XCTAssertFalse(sections.hasFetchedMostRecent) - XCTAssertEqual(1, sections.itemsBySection.count) - } + + // Load January + XCTAssertEqual(1, sections.loadLaterSections(batchSize: 1)) + XCTAssertFalse(sections.hasFetchedMostRecent) + XCTAssertEqual(1, sections.itemsBySection.count) XCTAssertEqual(IndexSet(integer: 1), sections.ensureItemsLoaded(in: 3..<4, relativeToSection: 0)) XCTAssertEqual(2, sections.itemsBySection.count) XCTAssertEqual([nil, nil, nil], sections.itemsBySection[0].value.map { $0.item }) XCTAssertEqual([store.allItems[3], nil], sections.itemsBySection[1].value.map { $0.item }) - databaseStorage.read { transaction in - sections.reset(transaction: transaction) - } + sections.reset() XCTAssertFalse(sections.hasFetchedOldest) XCTAssertFalse(sections.hasFetchedMostRecent) @@ -779,12 +763,11 @@ class MediaGallerySectionsTest: SignalBaseTest { func testResetFull() { let store = standardFakeStore var sections = MediaGallerySections(loader: store) - databaseStorage.read { transaction in - // Load January - XCTAssertEqual(1, sections.loadLaterSections(batchSize: 1, transaction: transaction)) - XCTAssertFalse(sections.hasFetchedMostRecent) - XCTAssertEqual(1, sections.itemsBySection.count) - } + + // Load January + XCTAssertEqual(1, sections.loadLaterSections(batchSize: 1)) + XCTAssertFalse(sections.hasFetchedMostRecent) + XCTAssertEqual(1, sections.itemsBySection.count) XCTAssertEqual(IndexSet(integersIn: 1...2), sections.ensureItemsLoaded(in: 2..<7, relativeToSection: 0)) XCTAssertTrue(sections.hasFetchedOldest) @@ -794,9 +777,7 @@ class MediaGallerySectionsTest: SignalBaseTest { XCTAssertEqual([store.allItems[3], store.allItems[4]], sections.itemsBySection[1].value.map { $0.item }) XCTAssertEqual([store.allItems[5], store.allItems[6], nil, nil, nil], sections.itemsBySection[2].value.map { $0.item }) - databaseStorage.read { transaction in - sections.reset(transaction: transaction) - } + sections.reset() XCTAssertFalse(sections.hasFetchedOldest) XCTAssertFalse(sections.hasFetchedMostRecent) @@ -809,19 +790,16 @@ class MediaGallerySectionsTest: SignalBaseTest { func testResetOneSectionAfterDeletingStart() { let store = standardFakeStore.clone() var sections = MediaGallerySections(loader: store) - databaseStorage.read { transaction in - // Load January - XCTAssertEqual(1, sections.loadLaterSections(batchSize: 1, transaction: transaction)) - XCTAssertFalse(sections.hasFetchedMostRecent) - XCTAssertEqual(1, sections.itemsBySection.count) - } + + // Load January + XCTAssertEqual(1, sections.loadLaterSections(batchSize: 1)) + XCTAssertFalse(sections.hasFetchedMostRecent) + XCTAssertEqual(1, sections.itemsBySection.count) store.allItems.removeFirst(3) store.itemsBySection.replace(key: GalleryDate(2021_01_01), value: []) - databaseStorage.read { transaction in - sections.reset(transaction: transaction) - } + sections.reset() XCTAssertFalse(sections.hasFetchedOldest) XCTAssertTrue(sections.hasFetchedMostRecent) @@ -833,12 +811,11 @@ class MediaGallerySectionsTest: SignalBaseTest { func testResetTwoSectionsAfterDeletingStart() { let store = standardFakeStore.clone() var sections = MediaGallerySections(loader: store) - databaseStorage.read { transaction in - // Load January - XCTAssertEqual(1, sections.loadLaterSections(batchSize: 1, transaction: transaction)) - XCTAssertFalse(sections.hasFetchedMostRecent) - XCTAssertEqual(1, sections.itemsBySection.count) - } + + // Load January + XCTAssertEqual(1, sections.loadLaterSections(batchSize: 1)) + XCTAssertFalse(sections.hasFetchedMostRecent) + XCTAssertEqual(1, sections.itemsBySection.count) XCTAssertEqual(IndexSet(integer: 1), sections.ensureItemsLoaded(in: 3..<4, relativeToSection: 0)) XCTAssertEqual(2, sections.itemsBySection.count) @@ -846,9 +823,7 @@ class MediaGallerySectionsTest: SignalBaseTest { store.allItems.removeFirst(3) store.itemsBySection.replace(key: GalleryDate(2021_01_01), value: []) - databaseStorage.read { transaction in - sections.reset(transaction: transaction) - } + sections.reset() XCTAssertFalse(sections.hasFetchedOldest) XCTAssertTrue(sections.hasFetchedMostRecent) @@ -860,12 +835,11 @@ class MediaGallerySectionsTest: SignalBaseTest { func testResetTwoSectionsAfterDeletingEndWithAnotherSectionFollowing() { let store = standardFakeStore.clone() var sections = MediaGallerySections(loader: store) - databaseStorage.read { transaction in - // Load January - XCTAssertEqual(1, sections.loadLaterSections(batchSize: 1, transaction: transaction)) - XCTAssertFalse(sections.hasFetchedMostRecent) - XCTAssertEqual(1, sections.itemsBySection.count) - } + + // Load January + XCTAssertEqual(1, sections.loadLaterSections(batchSize: 1)) + XCTAssertFalse(sections.hasFetchedMostRecent) + XCTAssertEqual(1, sections.itemsBySection.count) XCTAssertEqual(IndexSet(integer: 1), sections.ensureItemsLoaded(in: 3..<4, relativeToSection: 0)) XCTAssertEqual(2, sections.itemsBySection.count) @@ -873,9 +847,7 @@ class MediaGallerySectionsTest: SignalBaseTest { store.allItems.removeSubrange(3..<5) store.itemsBySection.replace(key: GalleryDate(2021_04_01), value: []) - databaseStorage.read { transaction in - sections.reset(transaction: transaction) - } + sections.reset() XCTAssertFalse(sections.hasFetchedOldest) XCTAssertFalse(sections.hasFetchedMostRecent) @@ -887,18 +859,15 @@ class MediaGallerySectionsTest: SignalBaseTest { func testResetTwoSectionsAfterDeletingEndWithNothingFollowing() { let store = standardFakeStore.clone() var sections = MediaGallerySections(loader: store) - databaseStorage.read { transaction in - // Load April and September - XCTAssertEqual(2, sections.loadEarlierSections(batchSize: 6, transaction: transaction)) - XCTAssertTrue(sections.hasFetchedMostRecent) - } + + // Load April and September + XCTAssertEqual(2, sections.loadEarlierSections(batchSize: 6)) + XCTAssertTrue(sections.hasFetchedMostRecent) store.allItems.removeSubrange(5...) store.itemsBySection.replace(key: GalleryDate(2021_09_01), value: []) - databaseStorage.read { transaction in - sections.reset(transaction: transaction) - } + sections.reset() XCTAssertFalse(sections.hasFetchedOldest) XCTAssertTrue(sections.hasFetchedMostRecent) @@ -910,12 +879,11 @@ class MediaGallerySectionsTest: SignalBaseTest { func testResetFullAfterDeletingStart() { let store = standardFakeStore.clone() var sections = MediaGallerySections(loader: store) - databaseStorage.read { transaction in - // Load January - XCTAssertEqual(1, sections.loadLaterSections(batchSize: 1, transaction: transaction)) - XCTAssertFalse(sections.hasFetchedMostRecent) - XCTAssertEqual(1, sections.itemsBySection.count) - } + + // Load January + XCTAssertEqual(1, sections.loadLaterSections(batchSize: 1)) + XCTAssertFalse(sections.hasFetchedMostRecent) + XCTAssertEqual(1, sections.itemsBySection.count) XCTAssertEqual(IndexSet(integersIn: 1...2), sections.ensureItemsLoaded(in: 2..<7, relativeToSection: 0)) XCTAssertTrue(sections.hasFetchedOldest) @@ -925,9 +893,7 @@ class MediaGallerySectionsTest: SignalBaseTest { store.allItems.removeFirst(3) store.itemsBySection.replace(key: GalleryDate(2021_01_01), value: []) - databaseStorage.read { transaction in - sections.reset(transaction: transaction) - } + sections.reset() XCTAssertFalse(sections.hasFetchedOldest) XCTAssertTrue(sections.hasFetchedMostRecent) @@ -939,12 +905,11 @@ class MediaGallerySectionsTest: SignalBaseTest { func testResetFullAfterDeletingMiddle() { let store = standardFakeStore.clone() var sections = MediaGallerySections(loader: store) - databaseStorage.read { transaction in - // Load January - XCTAssertEqual(1, sections.loadLaterSections(batchSize: 1, transaction: transaction)) - XCTAssertFalse(sections.hasFetchedMostRecent) - XCTAssertEqual(1, sections.itemsBySection.count) - } + + // Load January + XCTAssertEqual(1, sections.loadLaterSections(batchSize: 1)) + XCTAssertFalse(sections.hasFetchedMostRecent) + XCTAssertEqual(1, sections.itemsBySection.count) XCTAssertEqual(IndexSet(integersIn: 1...2), sections.ensureItemsLoaded(in: 2..<7, relativeToSection: 0)) XCTAssertTrue(sections.hasFetchedOldest) @@ -954,9 +919,7 @@ class MediaGallerySectionsTest: SignalBaseTest { store.allItems.removeSubrange(3..<5) store.itemsBySection.replace(key: GalleryDate(2021_04_01), value: []) - databaseStorage.read { transaction in - sections.reset(transaction: transaction) - } + sections.reset() XCTAssertFalse(sections.hasFetchedOldest) XCTAssertTrue(sections.hasFetchedMostRecent) @@ -968,12 +931,11 @@ class MediaGallerySectionsTest: SignalBaseTest { func testResetFullAfterDeletingEnd() { let store = standardFakeStore.clone() var sections = MediaGallerySections(loader: store) - databaseStorage.read { transaction in - // Load January - XCTAssertEqual(1, sections.loadLaterSections(batchSize: 1, transaction: transaction)) - XCTAssertFalse(sections.hasFetchedMostRecent) - XCTAssertEqual(1, sections.itemsBySection.count) - } + + // Load January + XCTAssertEqual(1, sections.loadLaterSections(batchSize: 1)) + XCTAssertFalse(sections.hasFetchedMostRecent) + XCTAssertEqual(1, sections.itemsBySection.count) XCTAssertEqual(IndexSet(integersIn: 1...2), sections.ensureItemsLoaded(in: 2..<7, relativeToSection: 0)) XCTAssertTrue(sections.hasFetchedOldest) @@ -983,9 +945,7 @@ class MediaGallerySectionsTest: SignalBaseTest { store.allItems.removeSubrange(5...) store.itemsBySection.replace(key: GalleryDate(2021_09_01), value: []) - databaseStorage.read { transaction in - sections.reset(transaction: transaction) - } + sections.reset() XCTAssertFalse(sections.hasFetchedOldest) XCTAssertTrue(sections.hasFetchedMostRecent) @@ -997,13 +957,12 @@ class MediaGallerySectionsTest: SignalBaseTest { func testGetOrAddItem() { let store = standardFakeStore var sections = MediaGallerySections(loader: store) - databaseStorage.read { transaction in - // Load January - XCTAssertEqual(1, sections.loadLaterSections(batchSize: 1, transaction: transaction)) - XCTAssertFalse(sections.hasFetchedMostRecent) - XCTAssertEqual(1, sections.itemsBySection.count) - XCTAssertEqual([nil, nil, nil], sections.itemsBySection[0].value.map { $0.item }) - } + + // Load January + XCTAssertEqual(1, sections.loadLaterSections(batchSize: 1)) + XCTAssertFalse(sections.hasFetchedMostRecent) + XCTAssertEqual(1, sections.itemsBySection.count) + XCTAssertEqual([nil, nil, nil], sections.itemsBySection[0].value.map { $0.item }) let fakeItem = FakeItem(2021_01_05) let newItem = sections.getOrReplaceItem(fakeItem, offsetInSection: 1) @@ -1019,24 +978,21 @@ class MediaGallerySectionsTest: SignalBaseTest { func testIndexAfter() { let store = standardFakeStore var sections = MediaGallerySections(loader: store) - databaseStorage.read { transaction in - // Load January - XCTAssertEqual(1, sections.loadLaterSections(batchSize: 1, transaction: transaction)) - XCTAssertFalse(sections.hasFetchedMostRecent) - XCTAssertEqual(1, sections.itemsBySection.count) - XCTAssertEqual([nil, nil, nil], sections.itemsBySection[0].value.map { $0.item }) - } + + // Load January + XCTAssertEqual(1, sections.loadLaterSections(batchSize: 1)) + XCTAssertFalse(sections.hasFetchedMostRecent) + XCTAssertEqual(1, sections.itemsBySection.count) + XCTAssertEqual([nil, nil, nil], sections.itemsBySection[0].value.map { $0.item }) XCTAssertEqual(IndexPath(item: 1, section: 0), sections.indexPath(after: IndexPath(item: 0, section: 0))) XCTAssertEqual(IndexPath(item: 2, section: 0), sections.indexPath(after: IndexPath(item: 1, section: 0))) XCTAssertNil(sections.indexPath(after: IndexPath(item: 2, section: 0))) - databaseStorage.read { transaction in - // Load remaining sections - XCTAssertEqual(2, sections.loadLaterSections(batchSize: 20, transaction: transaction)) - XCTAssertTrue(sections.hasFetchedMostRecent) - XCTAssertEqual(3, sections.itemsBySection.count) - } + // Load remaining sections + XCTAssertEqual(2, sections.loadLaterSections(batchSize: 20)) + XCTAssertTrue(sections.hasFetchedMostRecent) + XCTAssertEqual(3, sections.itemsBySection.count) XCTAssertEqual(IndexPath(item: 1, section: 0), sections.indexPath(after: IndexPath(item: 0, section: 0))) XCTAssertEqual(IndexPath(item: 2, section: 0), sections.indexPath(after: IndexPath(item: 1, section: 0))) @@ -1049,24 +1005,21 @@ class MediaGallerySectionsTest: SignalBaseTest { func testIndexBefore() { let store = standardFakeStore var sections = MediaGallerySections(loader: store) - databaseStorage.read { transaction in - // Load January - XCTAssertEqual(1, sections.loadLaterSections(batchSize: 1, transaction: transaction)) - XCTAssertFalse(sections.hasFetchedMostRecent) - XCTAssertEqual(1, sections.itemsBySection.count) - XCTAssertEqual([nil, nil, nil], sections.itemsBySection[0].value.map { $0.item }) - } + + // Load January + XCTAssertEqual(1, sections.loadLaterSections(batchSize: 1)) + XCTAssertFalse(sections.hasFetchedMostRecent) + XCTAssertEqual(1, sections.itemsBySection.count) + XCTAssertEqual([nil, nil, nil], sections.itemsBySection[0].value.map { $0.item }) XCTAssertEqual(IndexPath(item: 1, section: 0), sections.indexPath(before: IndexPath(item: 2, section: 0))) XCTAssertEqual(IndexPath(item: 0, section: 0), sections.indexPath(before: IndexPath(item: 1, section: 0))) XCTAssertNil(sections.indexPath(before: IndexPath(item: 0, section: 0))) - databaseStorage.read { transaction in - // Load remaining sections - XCTAssertEqual(2, sections.loadLaterSections(batchSize: 20, transaction: transaction)) - XCTAssertTrue(sections.hasFetchedMostRecent) - XCTAssertEqual(3, sections.itemsBySection.count) - } + // Load remaining sections + XCTAssertEqual(2, sections.loadLaterSections(batchSize: 20)) + XCTAssertTrue(sections.hasFetchedMostRecent) + XCTAssertEqual(3, sections.itemsBySection.count) XCTAssertEqual(IndexPath(item: 1, section: 1), sections.indexPath(before: IndexPath(item: 0, section: 2))) XCTAssertEqual(IndexPath(item: 0, section: 1), sections.indexPath(before: IndexPath(item: 1, section: 1))) @@ -1079,12 +1032,11 @@ class MediaGallerySectionsTest: SignalBaseTest { func testIndexPathOf() { let store = standardFakeStore var sections = MediaGallerySections(loader: store) - databaseStorage.read { transaction in - // Load all months - XCTAssertEqual(3, sections.loadLaterSections(batchSize: 20, transaction: transaction)) - XCTAssertTrue(sections.hasFetchedMostRecent) - XCTAssertEqual(3, sections.itemsBySection.count) - } + + // Load all months + XCTAssertEqual(3, sections.loadLaterSections(batchSize: 20)) + XCTAssertTrue(sections.hasFetchedMostRecent) + XCTAssertEqual(3, sections.itemsBySection.count) // Load all items. XCTAssert(sections.ensureItemsLoaded(in: 0..<20, relativeToSection: 0).isEmpty) @@ -1105,12 +1057,11 @@ class MediaGallerySectionsTest: SignalBaseTest { func testRemoveLoadedItems() { let store = standardFakeStore var sections = MediaGallerySections(loader: store) - databaseStorage.read { transaction in - // Load all months - XCTAssertEqual(3, sections.loadLaterSections(batchSize: 20, transaction: transaction)) - XCTAssertTrue(sections.hasFetchedMostRecent) - XCTAssertEqual(3, sections.itemsBySection.count) - } + + // Load all months + XCTAssertEqual(3, sections.loadLaterSections(batchSize: 20)) + XCTAssertTrue(sections.hasFetchedMostRecent) + XCTAssertEqual(3, sections.itemsBySection.count) // Load all items. XCTAssert(sections.ensureItemsLoaded(in: 0..<20, relativeToSection: 0).isEmpty) @@ -1139,12 +1090,11 @@ class MediaGallerySectionsTest: SignalBaseTest { func testTrimFromStart() { let store = standardFakeStore var sections = MediaGallerySections(loader: store) - databaseStorage.read { transaction in - // Load all months - XCTAssertEqual(3, sections.loadLaterSections(batchSize: 20, transaction: transaction)) - XCTAssertTrue(sections.hasFetchedMostRecent) - XCTAssertEqual(3, sections.itemsBySection.count) - } + + // Load all months + XCTAssertEqual(3, sections.loadLaterSections(batchSize: 20)) + XCTAssertTrue(sections.hasFetchedMostRecent) + XCTAssertEqual(3, sections.itemsBySection.count) // _ _ _ | _ _ | x _ x x _ _ = sections.ensureItemsLoaded(in: 0..<1, relativeToSection: 2) @@ -1182,9 +1132,8 @@ class MediaGallerySectionsTest: SignalBaseTest { XCTAssertEqual(2..<4, sections.trimLoadedItemsAtStart(from: 0..<4, relativeToSection: 1)) // _ _ x | x _ | x _ x x _ - databaseStorage.read { transaction in - sections.reloadSection(for: sections.sectionDates[1], transaction: transaction) - } + sections.reloadSection(for: sections.sectionDates[1]) + _ = sections.ensureItemsLoaded(in: -1 ..< 1, relativeToSection: 1) XCTAssertEqual(-1 ..< 5, sections.trimLoadedItemsAtStart(from: -1 ..< 5, relativeToSection: 2)) XCTAssertEqual(-1 ..< 5, sections.trimLoadedItemsAtStart(from: -2 ..< 5, relativeToSection: 2)) @@ -1204,12 +1153,11 @@ class MediaGallerySectionsTest: SignalBaseTest { func testTrimFromEnd() { let store = standardFakeStore var sections = MediaGallerySections(loader: store) - databaseStorage.read { transaction in - // Load all months - XCTAssertEqual(3, sections.loadLaterSections(batchSize: 20, transaction: transaction)) - XCTAssertTrue(sections.hasFetchedMostRecent) - XCTAssertEqual(3, sections.itemsBySection.count) - } + + // Load all months + XCTAssertEqual(3, sections.loadLaterSections(batchSize: 20)) + XCTAssertTrue(sections.hasFetchedMostRecent) + XCTAssertEqual(3, sections.itemsBySection.count) // x x _ | _ _ | _ _ _ _ _ _ = sections.ensureItemsLoaded(in: 0..<2, relativeToSection: 0) @@ -1225,12 +1173,11 @@ class MediaGallerySectionsTest: SignalBaseTest { func testLoadingTrimsRequestedRange() { let store = standardFakeStore var sections = MediaGallerySections(loader: store) - databaseStorage.read { transaction in - // Load all months - XCTAssertEqual(3, sections.loadLaterSections(batchSize: 20, transaction: transaction)) - XCTAssertTrue(sections.hasFetchedMostRecent) - XCTAssertEqual(3, sections.itemsBySection.count) - } + + // Load all months + XCTAssertEqual(3, sections.loadLaterSections(batchSize: 20)) + XCTAssertTrue(sections.hasFetchedMostRecent) + XCTAssertEqual(3, sections.itemsBySection.count) // _ _ x | x x | x _ _ _ _ _ = sections.ensureItemsLoaded(in: -1 ..< 3, relativeToSection: 1) @@ -1247,9 +1194,7 @@ class MediaGallerySectionsTest: SignalBaseTest { XCTAssertEqual(0..<3, store.mostRecentRequest) // x x x | _ _ | x x x _ _ - databaseStorage.read { transaction in - sections.reloadSection(for: sections.sectionDates[1], transaction: transaction) - } + sections.reloadSection(for: sections.sectionDates[1]) // x x x | x x | x x x _ _ _ = sections.ensureItemsLoaded(in: -1 ..< 3, relativeToSection: 1) @@ -1260,4 +1205,90 @@ class MediaGallerySectionsTest: SignalBaseTest { // The request should be skipped entirely. XCTAssertEqual(5000..<5000, store.mostRecentRequest) } + + func testReloadSections() { + let store = standardFakeStore.clone() + var sections = MediaGallerySections(loader: store) + + // Load all months + XCTAssertEqual(3, sections.loadLaterSections(batchSize: 20)) + XCTAssertTrue(sections.hasFetchedMostRecent) + XCTAssertEqual(3, sections.itemsBySection.count) + + // x x x | x x | x x x x x + _ = sections.ensureItemsLoaded(in: -1 ..< 10, relativeToSection: 1) + + let modifiedItems = store.allItems.filter { + // Keep January unmodified, drop all of April, and drop one value from September. + [0, 1, 2, 5, 6, 8, 9].contains($0.rowid) + } + + store.set(items: modifiedItems) + + let (update, delete) = sections.reloadSections(for: Set(sections.sectionDates + [GalleryDate(2022_01_01)])) + + XCTAssertEqual(update, IndexSet([0, 1, 2])) + XCTAssertEqual(delete, IndexSet(integer: 1)) + } + + func testHandleNewAttachments_NewSectionButMostRecentUnfetched() { + let store = standardFakeStore + var sections = MediaGallerySections(loader: store) + // Load the first month, January 2021 + XCTAssertEqual(1, sections.loadLaterSections(batchSize: 3)) + XCTAssertFalse(sections.hasFetchedMostRecent) + XCTAssertEqual(1, sections.itemsBySection.count) + let expected = MediaGallerySections.NewAttachmentHandlingResult(update: IndexSet(), + didAddAtEnd: false, + didReset: false) + let actual = sections.handleNewAttachments([GalleryDate(2022_01_01)]) + XCTAssertEqual(expected, actual) + } + + func testHandleNewAttachments_NewSectionAndMostRecentFetched() { + let store = standardFakeStore + var sections = MediaGallerySections(loader: store) + // Load all months + XCTAssertEqual(3, sections.loadLaterSections(batchSize: 20)) + XCTAssertTrue(sections.hasFetchedMostRecent) + XCTAssertEqual(3, sections.itemsBySection.count) + _ = sections.ensureItemsLoaded(in: -1 ..< 10, relativeToSection: 1) + XCTAssertTrue(sections.hasFetchedMostRecent) + let expected = MediaGallerySections.NewAttachmentHandlingResult(update: IndexSet(), + didAddAtEnd: true, + didReset: false) + let actual = sections.handleNewAttachments([GalleryDate(2022_01_01)]) + XCTAssertFalse(sections.hasFetchedMostRecent) + XCTAssertEqual(expected, actual) + } + + func testHandleNewAttachments_ExistingSection() { + let store = standardFakeStore + var sections = MediaGallerySections(loader: store) + // Load all months + XCTAssertEqual(3, sections.loadLaterSections(batchSize: 20)) + XCTAssertTrue(sections.hasFetchedMostRecent) + XCTAssertEqual(3, sections.itemsBySection.count) + + let expected = MediaGallerySections.NewAttachmentHandlingResult(update: IndexSet(integer: 1), + didAddAtEnd: false, + didReset: false) + let actual = sections.handleNewAttachments([GalleryDate(2021_04_01)]) + XCTAssertEqual(expected, actual) + } + + func testHandleNewAttachments_FirstAttachmentInNewSectionNotAtEnd() { + let store = standardFakeStore + var sections = MediaGallerySections(loader: store) + // Load all months + XCTAssertEqual(3, sections.loadLaterSections(batchSize: 20)) + XCTAssertTrue(sections.hasFetchedMostRecent) + XCTAssertEqual(3, sections.itemsBySection.count) + + let expected = MediaGallerySections.NewAttachmentHandlingResult(update: IndexSet(), + didAddAtEnd: false, + didReset: true) + let actual = sections.handleNewAttachments([GalleryDate(2020_07_01)]) + XCTAssertEqual(expected, actual) + } } diff --git a/ThirdParty/RingRTC b/ThirdParty/RingRTC index 965213f6e5..a081914ee7 160000 --- a/ThirdParty/RingRTC +++ b/ThirdParty/RingRTC @@ -1 +1 @@ -Subproject commit 965213f6e5cacf1cc2ea23f354061a63a137cd65 +Subproject commit a081914ee7fc02bd905a27accea47ad76017c61e diff --git a/ThirdParty/WebRTC b/ThirdParty/WebRTC index d20b610393..7550f3bd31 160000 --- a/ThirdParty/WebRTC +++ b/ThirdParty/WebRTC @@ -1 +1 @@ -Subproject commit d20b610393af19f96f42137f2c044e5bdcc50768 +Subproject commit 7550f3bd317002fd77eacebb7f7305f501b4a19a