Collection section type

This commit is contained in:
Justin Mazzocchi 2021-01-22 22:15:52 -08:00
parent c4da421846
commit 182bc5ce18
No known key found for this signature in database
GPG key ID: E223E6937AAFB01C
22 changed files with 141 additions and 102 deletions

View file

@ -425,7 +425,7 @@ public extension ContentDatabase {
.eraseToAnyPublisher()
}
func process(results: Results) -> AnyPublisher<[[CollectionItem]], Error> {
func process(results: Results) -> AnyPublisher<[CollectionSection], Error> {
databaseWriter.writePublisher { db -> ([StatusInfo], [Status.Id]) in
for account in results.accounts {
try account.save(db)
@ -442,22 +442,23 @@ public extension ContentDatabase {
return (statusInfos, ids)
}
.map { statusInfos, ids -> [[CollectionItem]] in
.map { statusInfos, ids -> [CollectionSection] in
[
results.accounts.map(CollectionItem.account),
statusInfos
.sorted { ids.firstIndex(of: $0.record.id) ?? 0 < ids.firstIndex(of: $1.record.id) ?? 0 }
.map {
.status(.init(info: $0),
.init(showContentToggled: $0.showContentToggled,
showAttachmentsToggled: $0.showAttachmentsToggled))
}
.init(items: results.accounts.map(CollectionItem.account), titleLocalizedStringKey: "search.accounts"),
.init(items: statusInfos
.sorted { ids.firstIndex(of: $0.record.id) ?? 0 < ids.firstIndex(of: $1.record.id) ?? 0 }
.map {
.status(.init(info: $0),
.init(showContentToggled: $0.showContentToggled,
showAttachmentsToggled: $0.showAttachmentsToggled))
},
titleLocalizedStringKey: "search.statuses")
]
}
.eraseToAnyPublisher()
}
func timelinePublisher(_ timeline: Timeline) -> AnyPublisher<[[CollectionItem]], Error> {
func timelinePublisher(_ timeline: Timeline) -> AnyPublisher<[CollectionSection], Error> {
ValueObservation.tracking(
TimelineItemsInfo.request(TimelineRecord.filter(TimelineRecord.Columns.id == timeline.id)).fetchOne)
.removeDuplicates()
@ -482,7 +483,7 @@ public extension ContentDatabase {
.eraseToAnyPublisher()
}
func contextPublisher(id: Status.Id) -> AnyPublisher<[[CollectionItem]], Error> {
func contextPublisher(id: Status.Id) -> AnyPublisher<[CollectionSection], Error> {
ValueObservation.tracking(
ContextItemsInfo.request(StatusRecord.filter(StatusRecord.Columns.id == id)).fetchOne)
.removeDuplicates()
@ -526,13 +527,13 @@ public extension ContentDatabase {
.eraseToAnyPublisher()
}
func notificationsPublisher() -> AnyPublisher<[[CollectionItem]], Error> {
func notificationsPublisher() -> AnyPublisher<[CollectionSection], Error> {
ValueObservation.tracking(
NotificationInfo.request(
NotificationRecord.order(NotificationRecord.Columns.id.desc)).fetchAll)
.removeDuplicates()
.publisher(in: databaseWriter)
.map { [$0.map {
.map { [.init(items: $0.map {
let configuration: CollectionItem.StatusConfiguration?
if $0.record.type == .mention, let statusInfo = $0.statusInfo {
@ -544,7 +545,7 @@ public extension ContentDatabase {
}
return .notification(MastodonNotification(info: $0), configuration)
}] }
})] }
.eraseToAnyPublisher()
}

View file

@ -21,7 +21,7 @@ extension ContextItemsInfo {
addingIncludes(request).asRequest(of: self)
}
func items(filters: [Filter]) -> [[CollectionItem]] {
func items(filters: [Filter]) -> [CollectionSection] {
let regularExpression = filters.regularExpression(context: .thread)
return [ancestors, [parent], descendants].map { section in
@ -52,5 +52,6 @@ extension ContextItemsInfo {
hasReplyFollowing: hasReplyFollowing))
}
}
.map { CollectionSection(items: $0) }
}
}

View file

@ -28,7 +28,7 @@ extension TimelineItemsInfo {
addingIncludes(request).asRequest(of: self)
}
func items(filters: [Filter]) -> [[CollectionItem]] {
func items(filters: [Filter]) -> [CollectionSection] {
let timeline = Timeline(record: timelineRecord)!
let filterRegularExpression = filters.regularExpression(context: timeline.filterContext)
var timelineItems = statusInfos.filtered(regularExpression: filterRegularExpression)
@ -55,17 +55,17 @@ extension TimelineItemsInfo {
}
if let pinnedStatusInfos = pinnedStatusesInfo?.pinnedStatusInfos {
return [pinnedStatusInfos.filtered(regularExpression: filterRegularExpression)
return [.init(items: pinnedStatusInfos.filtered(regularExpression: filterRegularExpression)
.map {
CollectionItem.status(
.init(info: $0),
.init(showContentToggled: $0.showContentToggled,
showAttachmentsToggled: $0.showAttachmentsToggled,
isPinned: true))
},
timelineItems]
}),
.init(items: timelineItems)]
} else {
return [timelineItems]
return [.init(items: timelineItems)]
}
}
}

View file

@ -0,0 +1,13 @@
// Copyright © 2021 Metabolist. All rights reserved.
import Foundation
public struct CollectionSection: Hashable {
public let items: [CollectionItem]
public let titleLocalizedStringKey: String?
public init(items: [CollectionItem], titleLocalizedStringKey: String? = nil) {
self.items = items
self.titleLocalizedStringKey = titleLocalizedStringKey
}
}

View file

@ -3,7 +3,7 @@
import UIKit
import ViewModels
final class TableViewDataSource: UITableViewDiffableDataSource<Int, CollectionItem> {
final class TableViewDataSource: UITableViewDiffableDataSource<CollectionSection.Identifier, CollectionItem> {
private let updateQueue =
DispatchQueue(label: "com.metabolist.metatext.collection-data-source.update-queue")
@ -36,13 +36,25 @@ final class TableViewDataSource: UITableViewDiffableDataSource<Int, CollectionIt
}
}
override func apply(_ snapshot: NSDiffableDataSourceSnapshot<Int, CollectionItem>,
override func apply(_ snapshot: NSDiffableDataSourceSnapshot<CollectionSection.Identifier, CollectionItem>,
animatingDifferences: Bool = true,
completion: (() -> Void)? = nil) {
updateQueue.async {
super.apply(snapshot, animatingDifferences: animatingDifferences, completion: completion)
}
}
override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
let currentSnapshot = snapshot()
let section = currentSnapshot.sectionIdentifiers[section]
if currentSnapshot.numberOfItems(inSection: section) > 0,
let localizedStringKey = section.titleLocalizedStringKey {
return NSLocalizedString(localizedStringKey, comment: "")
}
return nil
}
}
extension TableViewDataSource {

View file

@ -1,25 +0,0 @@
// Copyright © 2020 Metabolist. All rights reserved.
import UIKit
extension Array where Element: Sequence, Element.Element: Hashable {
func snapshot() -> NSDiffableDataSourceSnapshot<Int, Element.Element> {
var snapshot = NSDiffableDataSourceSnapshot<Int, Element.Element>()
let sections = [Int](0..<count)
snapshot.appendSections(sections)
for section in sections {
snapshot.appendItems(self[section].map { $0 }, toSection: section)
}
return snapshot
}
}
extension Array where Element: Hashable {
func snapshot() -> NSDiffableDataSourceSnapshot<Int, Element> {
[self].snapshot()
}
}

View file

@ -0,0 +1,27 @@
// Copyright © 2021 Metabolist. All rights reserved.
import UIKit
import ViewModels
extension CollectionSection {
struct Identifier: Hashable {
let index: Int
let titleLocalizedStringKey: String?
}
}
extension Array where Element == CollectionSection {
func snapshot() -> NSDiffableDataSourceSnapshot<CollectionSection.Identifier, CollectionItem> {
var snapshot = NSDiffableDataSourceSnapshot<CollectionSection.Identifier, CollectionItem>()
for (index, section) in enumerated() {
let identifier = CollectionSection.Identifier(
index: index,
titleLocalizedStringKey: section.titleLocalizedStringKey)
snapshot.appendSections([identifier])
snapshot.appendItems(section.items, toSection: identifier)
}
return snapshot
}
}

View file

@ -174,6 +174,9 @@
"report.target-%@" = "Reporting %@";
"report.forward.hint" = "The account is from another server. Send an anonymized copy of the report there as well?";
"report.forward-%@" = "Forward report to %@";
"search.accounts" = "People";
"search.statuses" = "Posts";
"search.tags" = "Hashtags";
"share-extension-error.no-account-found" = "No account found";
"status.bookmark" = "Bookmark";
"status.content-warning-abbreviation" = "CW";

View file

@ -18,7 +18,6 @@
D015B13A25A812E6006D88A8 /* AttachmentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0BEB1F224F8EE8C001B0F04 /* AttachmentView.swift */; };
D015B13F25A812EC006D88A8 /* PlayerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0FE1C8E253686F9003EF1EB /* PlayerView.swift */; };
D015B14425A812F6006D88A8 /* PlayerCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0FE1C9725368A9D003EF1EB /* PlayerCache.swift */; };
D01C6FAC252024BD003D0300 /* Array+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D01C6FAB252024BD003D0300 /* Array+Extensions.swift */; };
D01EF22425182B1F00650C6B /* AccountHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D01EF22325182B1F00650C6B /* AccountHeaderView.swift */; };
D01F41D924F880C400D55A2D /* TouchFallthroughTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D01F41D624F880C400D55A2D /* TouchFallthroughTextView.swift */; };
D01F41E424F8889700D55A2D /* AttachmentsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D01F41E224F8889700D55A2D /* AttachmentsView.swift */; };
@ -36,7 +35,6 @@
D036AA17254CA824009094DF /* StatusBodyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D036AA16254CA823009094DF /* StatusBodyView.swift */; };
D036EBB3259FE28800EC1CFC /* UIColor+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0C7D46C24F76169001EBDBB /* UIColor+Extensions.swift */; };
D036EBB8259FE29800EC1CFC /* Status+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0849C7E25903C4900A5EBCC /* Status+Extensions.swift */; };
D036EBBD259FE2A100EC1CFC /* Array+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D01C6FAB252024BD003D0300 /* Array+Extensions.swift */; };
D036EBC2259FE2AD00EC1CFC /* UIVIewController+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0E7AD3825870B13005F5E2D /* UIVIewController+Extensions.swift */; };
D036EBC7259FE2B700EC1CFC /* KingfisherOptionsInfo+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0C7D46E24F76169001EBDBB /* KingfisherOptionsInfo+Extensions.swift */; };
D03B1B2A253818F3008F964B /* MediaPreferencesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D03B1B29253818F3008F964B /* MediaPreferencesView.swift */; };
@ -131,6 +129,7 @@
D0C7D4DA24F7616A001EBDBB /* View+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0C7D46F24F76169001EBDBB /* View+Extensions.swift */; };
D0CE9F87258B076900E3A6B6 /* AttachmentUploadView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0CE9F86258B076900E3A6B6 /* AttachmentUploadView.swift */; };
D0CE9F88258B076900E3A6B6 /* AttachmentUploadView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0CE9F86258B076900E3A6B6 /* AttachmentUploadView.swift */; };
D0D2AC3925BBEC0F003D5DF2 /* CollectionSection+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0D2AC3825BBEC0F003D5DF2 /* CollectionSection+Extensions.swift */; };
D0DD50CB256B1F24004A04F7 /* ReportView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0DD50CA256B1F24004A04F7 /* ReportView.swift */; };
D0E1F583251F13EC00D45315 /* WebfingerIndicatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0E1F582251F13EC00D45315 /* WebfingerIndicatorView.swift */; };
D0E2C1D124FD97F000854680 /* ViewModels in Frameworks */ = {isa = PBXBuildFile; productRef = D0E2C1D024FD97F000854680 /* ViewModels */; };
@ -204,7 +203,6 @@
D007023D25562A2800F38136 /* ConversationAvatarsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConversationAvatarsView.swift; sourceTree = "<group>"; };
D0070251255921B100F38136 /* AccountFieldView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountFieldView.swift; sourceTree = "<group>"; };
D00CB2EC2533ACC00080096B /* StatusView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusView.swift; sourceTree = "<group>"; };
D01C6FAB252024BD003D0300 /* Array+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Array+Extensions.swift"; sourceTree = "<group>"; };
D01EF22325182B1F00650C6B /* AccountHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountHeaderView.swift; sourceTree = "<group>"; };
D01F41D624F880C400D55A2D /* TouchFallthroughTextView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TouchFallthroughTextView.swift; sourceTree = "<group>"; };
D01F41E224F8889700D55A2D /* AttachmentsView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AttachmentsView.swift; sourceTree = "<group>"; };
@ -304,6 +302,7 @@
D0C7D46E24F76169001EBDBB /* KingfisherOptionsInfo+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "KingfisherOptionsInfo+Extensions.swift"; sourceTree = "<group>"; };
D0C7D46F24F76169001EBDBB /* View+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "View+Extensions.swift"; sourceTree = "<group>"; };
D0CE9F86258B076900E3A6B6 /* AttachmentUploadView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttachmentUploadView.swift; sourceTree = "<group>"; };
D0D2AC3825BBEC0F003D5DF2 /* CollectionSection+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CollectionSection+Extensions.swift"; sourceTree = "<group>"; };
D0D7C013250440610039AD6F /* CodableBloomFilter */ = {isa = PBXFileReference; lastKnownFileType = folder; path = CodableBloomFilter; sourceTree = "<group>"; };
D0DD50CA256B1F24004A04F7 /* ReportView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportView.swift; sourceTree = "<group>"; };
D0E0F1E424FC49FC002C04BF /* Mastodon */ = {isa = PBXFileReference; lastKnownFileType = folder; path = Mastodon; sourceTree = "<group>"; };
@ -585,9 +584,9 @@
D0C7D46824F76169001EBDBB /* Extensions */ = {
isa = PBXGroup;
children = (
D01C6FAB252024BD003D0300 /* Array+Extensions.swift */,
D05E688425B55AE8001FB2C6 /* AVURLAsset+Extensions.swift */,
D0F0B135251AA12700942152 /* CollectionItem+Extensions.swift */,
D0D2AC3825BBEC0F003D5DF2 /* CollectionSection+Extensions.swift */,
D0C7D46E24F76169001EBDBB /* KingfisherOptionsInfo+Extensions.swift */,
D035F88625B8016000DC75ED /* NavigationViewModel+Extensions.swift */,
D0C7D46B24F76169001EBDBB /* NSMutableAttributedString+Extensions.swift */,
@ -596,13 +595,13 @@
D0849C7E25903C4900A5EBCC /* Status+Extensions.swift */,
D0C7D46A24F76169001EBDBB /* String+Extensions.swift */,
D07EC81025B232C2006DF726 /* SystemEmoji+Extensions.swift */,
D035F8B225B9616000DC75ED /* Timeline+Extensions.swift */,
D08E512025786A6600FA2C5F /* UIButton+Extensions.swift */,
D0C7D46C24F76169001EBDBB /* UIColor+Extensions.swift */,
D05936F325AA66A600754FDF /* UIView+Extensions.swift */,
D0E7AD3825870B13005F5E2D /* UIVIewController+Extensions.swift */,
D0030981250C6C8500EACB32 /* URL+Extensions.swift */,
D0C7D46F24F76169001EBDBB /* View+Extensions.swift */,
D035F8B225B9616000DC75ED /* Timeline+Extensions.swift */,
);
path = Extensions;
sourceTree = "<group>";
@ -883,7 +882,6 @@
D00702362555F4C500F38136 /* ConversationContentConfiguration.swift in Sources */,
D0BEB1F724F9A84B001B0F04 /* LoadingTableFooterView.swift in Sources */,
D06BC5E625202AD90079541D /* ProfileViewController.swift in Sources */,
D01C6FAC252024BD003D0300 /* Array+Extensions.swift in Sources */,
D0C7D4D924F7616A001EBDBB /* KingfisherOptionsInfo+Extensions.swift in Sources */,
D08B8D72254246E200B1EBEF /* PollView.swift in Sources */,
D035F8A925B9155900DC75ED /* NewStatusButtonView.swift in Sources */,
@ -929,6 +927,7 @@
D088406D25AFBBE200BB749B /* EmojiPickerViewController.swift in Sources */,
D07EC7FD25B16994006DF726 /* EmojiCategoryHeaderView.swift in Sources */,
D0C7D4D724F7616A001EBDBB /* UIColor+Extensions.swift in Sources */,
D0D2AC3925BBEC0F003D5DF2 /* CollectionSection+Extensions.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@ -968,7 +967,6 @@
D05936EA25AA3F3D00754FDF /* EditAttachmentView.swift in Sources */,
D07EC7D025B13921006DF726 /* PickerEmoji+Extensions.swift in Sources */,
D0FCC106259C4E62000B67DF /* NewStatusViewController.swift in Sources */,
D036EBBD259FE2A100EC1CFC /* Array+Extensions.swift in Sources */,
D036EBB3259FE28800EC1CFC /* UIColor+Extensions.swift in Sources */,
D088406E25AFBBE200BB749B /* EmojiPickerViewController.swift in Sources */,
D036EBB8259FE29800EC1CFC /* Status+Extensions.swift in Sources */,

View file

@ -0,0 +1,5 @@
// Copyright © 2021 Metabolist. All rights reserved.
import DB
public typealias CollectionSection = DB.CollectionSection

View file

@ -7,7 +7,7 @@ import Mastodon
import MastodonAPI
public struct AccountListService {
public let sections: AnyPublisher<[[CollectionItem]], Error>
public let sections: AnyPublisher<[CollectionSection], Error>
public let nextPageMaxId: AnyPublisher<String, Never>
public let navigationService: NavigationService
public let canRefresh = false
@ -27,7 +27,7 @@ public struct AccountListService {
self.mastodonAPIClient = mastodonAPIClient
self.contentDatabase = contentDatabase
self.titleComponents = titleComponents
sections = accountList.map { [$0.map(CollectionItem.account)] }.eraseToAnyPublisher()
sections = accountList.map { [.init(items: $0.map(CollectionItem.account))] }.eraseToAnyPublisher()
nextPageMaxId = nextPageMaxIdSubject.eraseToAnyPublisher()
navigationService = NavigationService(mastodonAPIClient: mastodonAPIClient, contentDatabase: contentDatabase)
}

View file

@ -4,7 +4,7 @@ import Combine
import Mastodon
public protocol CollectionService {
var sections: AnyPublisher<[[CollectionItem]], Error> { get }
var sections: AnyPublisher<[CollectionSection], Error> { get }
var nextPageMaxId: AnyPublisher<String, Never> { get }
var preferLastPresentIdOverNextPageMaxId: Bool { get }
var canRefresh: Bool { get }

View file

@ -7,7 +7,7 @@ import Mastodon
import MastodonAPI
public struct ContextService {
public let sections: AnyPublisher<[[CollectionItem]], Error>
public let sections: AnyPublisher<[CollectionSection], Error>
public let navigationService: NavigationService
private let id: Status.Id

View file

@ -7,7 +7,7 @@ import Mastodon
import MastodonAPI
public struct ConversationsService {
public let sections: AnyPublisher<[[CollectionItem]], Error>
public let sections: AnyPublisher<[CollectionSection], Error>
public let nextPageMaxId: AnyPublisher<String, Never>
public let navigationService: NavigationService
@ -19,7 +19,7 @@ public struct ConversationsService {
self.mastodonAPIClient = mastodonAPIClient
self.contentDatabase = contentDatabase
sections = contentDatabase.conversationsPublisher()
.map { [$0.map(CollectionItem.conversation)] }
.map { [.init(items: $0.map(CollectionItem.conversation))] }
.eraseToAnyPublisher()
nextPageMaxId = nextPageMaxIdSubject.eraseToAnyPublisher()
navigationService = NavigationService(mastodonAPIClient: mastodonAPIClient, contentDatabase: contentDatabase)

View file

@ -7,7 +7,7 @@ import Mastodon
import MastodonAPI
public struct NotificationsService {
public let sections: AnyPublisher<[[CollectionItem]], Error>
public let sections: AnyPublisher<[CollectionSection], Error>
public let nextPageMaxId: AnyPublisher<String, Never>
public let navigationService: NavigationService
@ -24,7 +24,7 @@ public struct NotificationsService {
self.nextPageMaxIdSubject = nextPageMaxIdSubject
sections = contentDatabase.notificationsPublisher()
.handleEvents(receiveOutput: {
guard case let .notification(notification, _) = $0.last?.last,
guard case let .notification(notification, _) = $0.last?.items.last,
notification.id < nextPageMaxIdSubject.value
else { return }

View file

@ -7,14 +7,14 @@ import Mastodon
import MastodonAPI
public struct SearchService {
public let sections: AnyPublisher<[[CollectionItem]], Error>
public let sections: AnyPublisher<[CollectionSection], Error>
public let navigationService: NavigationService
public let nextPageMaxId: AnyPublisher<String, Never>
private let mastodonAPIClient: MastodonAPIClient
private let contentDatabase: ContentDatabase
private let nextPageMaxIdSubject = PassthroughSubject<String, Never>()
private let sectionsSubject = PassthroughSubject<[[CollectionItem]], Error>()
private let sectionsSubject = PassthroughSubject<[CollectionSection], Error>()
init(mastodonAPIClient: MastodonAPIClient, contentDatabase: ContentDatabase) {
self.mastodonAPIClient = mastodonAPIClient

View file

@ -7,7 +7,7 @@ import Mastodon
import MastodonAPI
public struct TimelineService {
public let sections: AnyPublisher<[[CollectionItem]], Error>
public let sections: AnyPublisher<[CollectionSection], Error>
public let navigationService: NavigationService
public let nextPageMaxId: AnyPublisher<String, Never>
public let preferLastPresentIdOverNextPageMaxId = true

View file

@ -169,9 +169,8 @@ private extension NewStatusViewController {
}
func set(compositionViewModels: [CompositionViewModel]) {
let diff = compositionViewModels.map(\.id).snapshot().itemIdentifiers.difference(
from: stackView.arrangedSubviews.compactMap { ($0 as? CompositionView)?.id }
.snapshot().itemIdentifiers)
let diff = compositionViewModels.map(\.id)
.difference(from: stackView.arrangedSubviews.compactMap { ($0 as? CompositionView)?.id })
for insertion in diff.insertions {
guard case let .insert(index, id, _) = insertion,

View file

@ -302,7 +302,7 @@ private extension TableViewController {
positionMaintenanceOffset = 0
}
self.dataSource.apply(update.items.snapshot(), animatingDifferences: false) { [weak self] in
self.dataSource.apply(update.sections.snapshot(), animatingDifferences: false) { [weak self] in
guard let self = self else { return }
if let itemId = update.maintainScrollPositionItemId,

View file

@ -10,7 +10,7 @@ public class CollectionItemsViewModel: ObservableObject {
public private(set) var nextPageMaxId: String?
@Published private var lastUpdate = CollectionUpdate(
items: [],
sections: [],
maintainScrollPositionItemId: nil,
shouldAdjustContentInset: false)
private let collectionService: CollectionService
@ -34,7 +34,7 @@ public class CollectionItemsViewModel: ObservableObject {
? .expand : .hidden)
collectionService.sections
.handleEvents(receiveOutput: { [weak self] in self?.process(items: $0) })
.handleEvents(receiveOutput: { [weak self] in self?.process(sections: $0) })
.receive(on: DispatchQueue.main)
.assignErrorsToAlertItem(to: \.alertItem, on: self)
.sink { _ in }
@ -119,7 +119,7 @@ extension CollectionItemsViewModel: CollectionViewModel {
}
public func select(indexPath: IndexPath) {
let item = lastUpdate.items[indexPath.section][indexPath.item]
let item = lastUpdate.sections[indexPath.section].items[indexPath.item]
switch item {
case let .status(status, _):
@ -161,14 +161,14 @@ extension CollectionItemsViewModel: CollectionViewModel {
topVisibleIndexPath = indexPath
if !shouldRestorePositionOfLocalLastReadId,
lastUpdate.items.count > indexPath.section,
lastUpdate.items[indexPath.section].count > indexPath.item {
lastReadId.send(lastUpdate.items[indexPath.section][indexPath.item].itemId)
lastUpdate.sections.count > indexPath.section,
lastUpdate.sections[indexPath.section].items.count > indexPath.item {
lastReadId.send(lastUpdate.sections[indexPath.section].items[indexPath.item].itemId)
}
}
public func canSelect(indexPath: IndexPath) -> Bool {
switch lastUpdate.items[indexPath.section][indexPath.item] {
switch lastUpdate.sections[indexPath.section].items[indexPath.item] {
case let .status(_, configuration):
return !configuration.isContextParent
case .loadMore:
@ -180,7 +180,7 @@ extension CollectionItemsViewModel: CollectionViewModel {
// swiftlint:disable:next function_body_length cyclomatic_complexity
public func viewModel(indexPath: IndexPath) -> CollectionItemViewModel {
let item = lastUpdate.items[indexPath.section][indexPath.item]
let item = lastUpdate.sections[indexPath.section].items[indexPath.item]
let cachedViewModel = viewModelCache[item]?.viewModel
switch item {
@ -260,7 +260,7 @@ extension CollectionItemsViewModel: CollectionViewModel {
}
public func toggleExpandAll() {
let statusIds = Set(lastUpdate.items.reduce([], +).compactMap { item -> Status.Id? in
let statusIds = Set(lastUpdate.sections.map(\.items).reduce([], +).compactMap { item -> Status.Id? in
guard case let .status(status, _) = item else { return nil }
return status.id
@ -289,7 +289,7 @@ private extension CollectionItemsViewModel {
private static let lastReadIdDebounceInterval: TimeInterval = 0.5
var lastUpdateWasContextParentOnly: Bool {
collectionService is ContextService && lastUpdate.items.map(\.count) == [0, 1, 0]
collectionService is ContextService && lastUpdate.sections.map(\.items).map(\.count) == [0, 1, 0]
}
func cache(viewModel: CollectionItemViewModel, forItem item: CollectionItem) {
@ -303,14 +303,14 @@ private extension CollectionItemsViewModel {
.sink { [weak self] in self?.eventsSubject.send($0) })
}
func process(items: [[CollectionItem]]) {
let flatItems = items.reduce([], +)
let itemsSet = Set(flatItems)
func process(sections: [CollectionSection]) {
let items = sections.map(\.items).reduce([], +)
let itemsSet = Set(items)
self.lastUpdate = .init(
items: items,
maintainScrollPositionItemId: idForScrollPositionMaintenance(newItems: items),
shouldAdjustContentInset: lastUpdateWasContextParentOnly && flatItems.count > 1)
sections: sections,
maintainScrollPositionItemId: idForScrollPositionMaintenance(newSections: sections),
shouldAdjustContentInset: lastUpdateWasContextParentOnly && items.count > 1)
viewModelCache = viewModelCache.filter { itemsSet.contains($0.key) }
}
@ -320,35 +320,35 @@ private extension CollectionItemsViewModel {
guard let markerTimeline = collectionService.markerTimeline,
identification.appPreferences.positionBehavior(markerTimeline: markerTimeline) == .rememberPosition,
let lastItemId = lastUpdate.items.last?.last?.itemId
let lastItemId = lastUpdate.sections.last?.items.last?.itemId
else { return maxId }
return min(maxId, lastItemId)
}
func idForScrollPositionMaintenance(newItems: [[CollectionItem]]) -> CollectionItem.Id? {
let flatItems = lastUpdate.items.reduce([], +)
let flatNewItems = newItems.reduce([], +)
func idForScrollPositionMaintenance(newSections: [CollectionSection]) -> CollectionItem.Id? {
let items = lastUpdate.sections.map(\.items).reduce([], +)
let newItems = newSections.map(\.items).reduce([], +)
if shouldRestorePositionOfLocalLastReadId,
let markerTimeline = collectionService.markerTimeline,
let localLastReadId = identification.service.getLocalLastReadId(markerTimeline),
flatNewItems.contains(where: { $0.itemId == localLastReadId }) {
newItems.contains(where: { $0.itemId == localLastReadId }) {
shouldRestorePositionOfLocalLastReadId = false
return localLastReadId
}
if collectionService is ContextService,
lastUpdate.items.isEmpty || lastUpdate.items.map(\.count) == [0, 1, 0],
let contextParent = flatNewItems.first(where: {
lastUpdate.sections.isEmpty || lastUpdate.sections.map(\.items.count) == [0, 1, 0],
let contextParent = newItems.first(where: {
guard case let .status(_, configuration) = $0 else { return false }
return configuration.isContextParent // Maintain scroll position of parent after initial load of context
}) {
return contextParent.itemId
} else if collectionService is TimelineService {
let difference = flatNewItems.difference(from: flatItems)
let difference = newItems.difference(from: items)
if let lastSelectedLoadMore = lastSelectedLoadMore {
for removal in difference.removals {
@ -357,7 +357,7 @@ private extension CollectionItemsViewModel {
loadMore == lastSelectedLoadMore,
let direction = (viewModelCache[item]?.viewModel as? LoadMoreViewModel)?.direction,
direction == .up,
let statusAfterLoadMore = flatItems.first(where: {
let statusAfterLoadMore = items.first(where: {
guard case let .status(status, _) = $0 else { return false }
return status.id == loadMore.beforeStatusId
@ -367,13 +367,13 @@ private extension CollectionItemsViewModel {
}
}
if lastUpdate.items.count > topVisibleIndexPath.section,
lastUpdate.items[topVisibleIndexPath.section].count > topVisibleIndexPath.item {
let topVisibleItem = lastUpdate.items[topVisibleIndexPath.section][topVisibleIndexPath.item]
if lastUpdate.sections.count > topVisibleIndexPath.section,
lastUpdate.sections[topVisibleIndexPath.section].items.count > topVisibleIndexPath.item {
let topVisibleItem = lastUpdate.sections[topVisibleIndexPath.section].items[topVisibleIndexPath.item]
if newItems.count > topVisibleIndexPath.section,
let newIndex = newItems[topVisibleIndexPath.section]
.firstIndex(where: { $0.itemId == topVisibleItem.itemId }),
if newSections.count > topVisibleIndexPath.section,
let newIndex = newSections[topVisibleIndexPath.section]
.items.firstIndex(where: { $0.itemId == topVisibleItem.itemId }),
newIndex > topVisibleIndexPath.item {
return topVisibleItem.itemId
}

View file

@ -0,0 +1,5 @@
// Copyright © 2021 Metabolist. All rights reserved.
import ServiceLayer
public typealias CollectionSection = ServiceLayer.CollectionSection

View file

@ -1,7 +1,7 @@
// Copyright © 2020 Metabolist. All rights reserved.
public struct CollectionUpdate: Hashable {
public let items: [[CollectionItem]]
public let sections: [CollectionSection]
public let maintainScrollPositionItemId: String?
public let shouldAdjustContentInset: Bool
}