mirror of
https://github.com/metabolist/metatext.git
synced 2024-11-21 15:50:59 +00:00
Collection section type
This commit is contained in:
parent
c4da421846
commit
182bc5ce18
22 changed files with 141 additions and 102 deletions
|
@ -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()
|
||||
}
|
||||
|
||||
|
|
|
@ -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) }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
13
DB/Sources/DB/Entities/CollectionSection.swift
Normal file
13
DB/Sources/DB/Entities/CollectionSection.swift
Normal 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
|
||||
}
|
||||
}
|
|
@ -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 {
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
27
Extensions/CollectionSection+Extensions.swift
Normal file
27
Extensions/CollectionSection+Extensions.swift
Normal 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
|
||||
}
|
||||
}
|
|
@ -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";
|
||||
|
|
|
@ -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 */,
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
// Copyright © 2021 Metabolist. All rights reserved.
|
||||
|
||||
import DB
|
||||
|
||||
public typealias CollectionSection = DB.CollectionSection
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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 }
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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 }
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
// Copyright © 2021 Metabolist. All rights reserved.
|
||||
|
||||
import ServiceLayer
|
||||
|
||||
public typealias CollectionSection = ServiceLayer.CollectionSection
|
|
@ -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
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue