From 182bc5ce18787a7dc5d03ced92c8ba4c0fe783d6 Mon Sep 17 00:00:00 2001 From: Justin Mazzocchi <2831158+jzzocc@users.noreply.github.com> Date: Fri, 22 Jan 2021 22:15:52 -0800 Subject: [PATCH] Collection section type --- DB/Sources/DB/Content/ContentDatabase.swift | 31 +++++----- DB/Sources/DB/Content/ContextItemsInfo.swift | 3 +- DB/Sources/DB/Content/TimelineItemsInfo.swift | 10 +-- .../DB/Entities/CollectionSection.swift | 13 ++++ Data Sources/TableViewDataSource.swift | 16 ++++- Extensions/Array+Extensions.swift | 25 -------- Extensions/CollectionSection+Extensions.swift | 27 ++++++++ Localizations/Localizable.strings | 3 + Metatext.xcodeproj/project.pbxproj | 12 ++-- .../Entities/CollectionSection.swift | 5 ++ .../Services/AccountListService.swift | 4 +- .../Services/CollectionService.swift | 2 +- .../Services/ContextService.swift | 2 +- .../Services/ConversationsService.swift | 4 +- .../Services/NotificationsService.swift | 4 +- .../ServiceLayer/Services/SearchService.swift | 4 +- .../Services/TimelineService.swift | 2 +- .../NewStatusViewController.swift | 5 +- View Controllers/TableViewController.swift | 2 +- .../ViewModels/CollectionItemsViewModel.swift | 62 +++++++++---------- .../Entities/CollectionSection.swift | 5 ++ .../Entities/CollectionUpdate.swift | 2 +- 22 files changed, 141 insertions(+), 102 deletions(-) create mode 100644 DB/Sources/DB/Entities/CollectionSection.swift delete mode 100644 Extensions/Array+Extensions.swift create mode 100644 Extensions/CollectionSection+Extensions.swift create mode 100644 ServiceLayer/Sources/ServiceLayer/Entities/CollectionSection.swift create mode 100644 ViewModels/Sources/ViewModels/Entities/CollectionSection.swift diff --git a/DB/Sources/DB/Content/ContentDatabase.swift b/DB/Sources/DB/Content/ContentDatabase.swift index 5314d12..6328e38 100644 --- a/DB/Sources/DB/Content/ContentDatabase.swift +++ b/DB/Sources/DB/Content/ContentDatabase.swift @@ -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() } diff --git a/DB/Sources/DB/Content/ContextItemsInfo.swift b/DB/Sources/DB/Content/ContextItemsInfo.swift index ee862b5..5ae0a9b 100644 --- a/DB/Sources/DB/Content/ContextItemsInfo.swift +++ b/DB/Sources/DB/Content/ContextItemsInfo.swift @@ -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) } } } diff --git a/DB/Sources/DB/Content/TimelineItemsInfo.swift b/DB/Sources/DB/Content/TimelineItemsInfo.swift index b0c054a..ca9fd63 100644 --- a/DB/Sources/DB/Content/TimelineItemsInfo.swift +++ b/DB/Sources/DB/Content/TimelineItemsInfo.swift @@ -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)] } } } diff --git a/DB/Sources/DB/Entities/CollectionSection.swift b/DB/Sources/DB/Entities/CollectionSection.swift new file mode 100644 index 0000000..e35e52c --- /dev/null +++ b/DB/Sources/DB/Entities/CollectionSection.swift @@ -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 + } +} diff --git a/Data Sources/TableViewDataSource.swift b/Data Sources/TableViewDataSource.swift index 5b50ed0..5b72d3b 100644 --- a/Data Sources/TableViewDataSource.swift +++ b/Data Sources/TableViewDataSource.swift @@ -3,7 +3,7 @@ import UIKit import ViewModels -final class TableViewDataSource: UITableViewDiffableDataSource { +final class TableViewDataSource: UITableViewDiffableDataSource { private let updateQueue = DispatchQueue(label: "com.metabolist.metatext.collection-data-source.update-queue") @@ -36,13 +36,25 @@ final class TableViewDataSource: UITableViewDiffableDataSource, + override func apply(_ snapshot: NSDiffableDataSourceSnapshot, 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 { diff --git a/Extensions/Array+Extensions.swift b/Extensions/Array+Extensions.swift deleted file mode 100644 index b89d441..0000000 --- a/Extensions/Array+Extensions.swift +++ /dev/null @@ -1,25 +0,0 @@ -// Copyright © 2020 Metabolist. All rights reserved. - -import UIKit - -extension Array where Element: Sequence, Element.Element: Hashable { - func snapshot() -> NSDiffableDataSourceSnapshot { - var snapshot = NSDiffableDataSourceSnapshot() - - let sections = [Int](0.. NSDiffableDataSourceSnapshot { - [self].snapshot() - } -} diff --git a/Extensions/CollectionSection+Extensions.swift b/Extensions/CollectionSection+Extensions.swift new file mode 100644 index 0000000..4b0b365 --- /dev/null +++ b/Extensions/CollectionSection+Extensions.swift @@ -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 { + var snapshot = NSDiffableDataSourceSnapshot() + + 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 + } +} diff --git a/Localizations/Localizable.strings b/Localizations/Localizable.strings index a52f82f..4b598cb 100644 --- a/Localizations/Localizable.strings +++ b/Localizations/Localizable.strings @@ -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"; diff --git a/Metatext.xcodeproj/project.pbxproj b/Metatext.xcodeproj/project.pbxproj index 6ce4508..38877d5 100644 --- a/Metatext.xcodeproj/project.pbxproj +++ b/Metatext.xcodeproj/project.pbxproj @@ -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 = ""; }; D0070251255921B100F38136 /* AccountFieldView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountFieldView.swift; sourceTree = ""; }; D00CB2EC2533ACC00080096B /* StatusView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusView.swift; sourceTree = ""; }; - D01C6FAB252024BD003D0300 /* Array+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Array+Extensions.swift"; sourceTree = ""; }; D01EF22325182B1F00650C6B /* AccountHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountHeaderView.swift; sourceTree = ""; }; D01F41D624F880C400D55A2D /* TouchFallthroughTextView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TouchFallthroughTextView.swift; sourceTree = ""; }; D01F41E224F8889700D55A2D /* AttachmentsView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AttachmentsView.swift; sourceTree = ""; }; @@ -304,6 +302,7 @@ D0C7D46E24F76169001EBDBB /* KingfisherOptionsInfo+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "KingfisherOptionsInfo+Extensions.swift"; sourceTree = ""; }; D0C7D46F24F76169001EBDBB /* View+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "View+Extensions.swift"; sourceTree = ""; }; D0CE9F86258B076900E3A6B6 /* AttachmentUploadView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttachmentUploadView.swift; sourceTree = ""; }; + D0D2AC3825BBEC0F003D5DF2 /* CollectionSection+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CollectionSection+Extensions.swift"; sourceTree = ""; }; D0D7C013250440610039AD6F /* CodableBloomFilter */ = {isa = PBXFileReference; lastKnownFileType = folder; path = CodableBloomFilter; sourceTree = ""; }; D0DD50CA256B1F24004A04F7 /* ReportView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportView.swift; sourceTree = ""; }; D0E0F1E424FC49FC002C04BF /* Mastodon */ = {isa = PBXFileReference; lastKnownFileType = folder; path = Mastodon; sourceTree = ""; }; @@ -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 = ""; @@ -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 */, diff --git a/ServiceLayer/Sources/ServiceLayer/Entities/CollectionSection.swift b/ServiceLayer/Sources/ServiceLayer/Entities/CollectionSection.swift new file mode 100644 index 0000000..0d3d21f --- /dev/null +++ b/ServiceLayer/Sources/ServiceLayer/Entities/CollectionSection.swift @@ -0,0 +1,5 @@ +// Copyright © 2021 Metabolist. All rights reserved. + +import DB + +public typealias CollectionSection = DB.CollectionSection diff --git a/ServiceLayer/Sources/ServiceLayer/Services/AccountListService.swift b/ServiceLayer/Sources/ServiceLayer/Services/AccountListService.swift index 3908427..89947c8 100644 --- a/ServiceLayer/Sources/ServiceLayer/Services/AccountListService.swift +++ b/ServiceLayer/Sources/ServiceLayer/Services/AccountListService.swift @@ -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 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) } diff --git a/ServiceLayer/Sources/ServiceLayer/Services/CollectionService.swift b/ServiceLayer/Sources/ServiceLayer/Services/CollectionService.swift index 7174839..425d9bc 100644 --- a/ServiceLayer/Sources/ServiceLayer/Services/CollectionService.swift +++ b/ServiceLayer/Sources/ServiceLayer/Services/CollectionService.swift @@ -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 { get } var preferLastPresentIdOverNextPageMaxId: Bool { get } var canRefresh: Bool { get } diff --git a/ServiceLayer/Sources/ServiceLayer/Services/ContextService.swift b/ServiceLayer/Sources/ServiceLayer/Services/ContextService.swift index abddb04..41fbc9a 100644 --- a/ServiceLayer/Sources/ServiceLayer/Services/ContextService.swift +++ b/ServiceLayer/Sources/ServiceLayer/Services/ContextService.swift @@ -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 diff --git a/ServiceLayer/Sources/ServiceLayer/Services/ConversationsService.swift b/ServiceLayer/Sources/ServiceLayer/Services/ConversationsService.swift index 9f290e7..f2c3cb7 100644 --- a/ServiceLayer/Sources/ServiceLayer/Services/ConversationsService.swift +++ b/ServiceLayer/Sources/ServiceLayer/Services/ConversationsService.swift @@ -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 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) diff --git a/ServiceLayer/Sources/ServiceLayer/Services/NotificationsService.swift b/ServiceLayer/Sources/ServiceLayer/Services/NotificationsService.swift index 95f8b9b..8fb9697 100644 --- a/ServiceLayer/Sources/ServiceLayer/Services/NotificationsService.swift +++ b/ServiceLayer/Sources/ServiceLayer/Services/NotificationsService.swift @@ -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 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 } diff --git a/ServiceLayer/Sources/ServiceLayer/Services/SearchService.swift b/ServiceLayer/Sources/ServiceLayer/Services/SearchService.swift index 633cc54..08277fc 100644 --- a/ServiceLayer/Sources/ServiceLayer/Services/SearchService.swift +++ b/ServiceLayer/Sources/ServiceLayer/Services/SearchService.swift @@ -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 private let mastodonAPIClient: MastodonAPIClient private let contentDatabase: ContentDatabase private let nextPageMaxIdSubject = PassthroughSubject() - private let sectionsSubject = PassthroughSubject<[[CollectionItem]], Error>() + private let sectionsSubject = PassthroughSubject<[CollectionSection], Error>() init(mastodonAPIClient: MastodonAPIClient, contentDatabase: ContentDatabase) { self.mastodonAPIClient = mastodonAPIClient diff --git a/ServiceLayer/Sources/ServiceLayer/Services/TimelineService.swift b/ServiceLayer/Sources/ServiceLayer/Services/TimelineService.swift index a451736..2f5b862 100644 --- a/ServiceLayer/Sources/ServiceLayer/Services/TimelineService.swift +++ b/ServiceLayer/Sources/ServiceLayer/Services/TimelineService.swift @@ -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 public let preferLastPresentIdOverNextPageMaxId = true diff --git a/View Controllers/NewStatusViewController.swift b/View Controllers/NewStatusViewController.swift index 3d0e85f..b625af7 100644 --- a/View Controllers/NewStatusViewController.swift +++ b/View Controllers/NewStatusViewController.swift @@ -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, diff --git a/View Controllers/TableViewController.swift b/View Controllers/TableViewController.swift index f4aca05..5b776e8 100644 --- a/View Controllers/TableViewController.swift +++ b/View Controllers/TableViewController.swift @@ -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, diff --git a/ViewModels/Sources/ViewModels/CollectionItemsViewModel.swift b/ViewModels/Sources/ViewModels/CollectionItemsViewModel.swift index 93fad65..b60ac96 100644 --- a/ViewModels/Sources/ViewModels/CollectionItemsViewModel.swift +++ b/ViewModels/Sources/ViewModels/CollectionItemsViewModel.swift @@ -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 } diff --git a/ViewModels/Sources/ViewModels/Entities/CollectionSection.swift b/ViewModels/Sources/ViewModels/Entities/CollectionSection.swift new file mode 100644 index 0000000..38f91c7 --- /dev/null +++ b/ViewModels/Sources/ViewModels/Entities/CollectionSection.swift @@ -0,0 +1,5 @@ +// Copyright © 2021 Metabolist. All rights reserved. + +import ServiceLayer + +public typealias CollectionSection = ServiceLayer.CollectionSection diff --git a/ViewModels/Sources/ViewModels/Entities/CollectionUpdate.swift b/ViewModels/Sources/ViewModels/Entities/CollectionUpdate.swift index 36e4999..53e6176 100644 --- a/ViewModels/Sources/ViewModels/Entities/CollectionUpdate.swift +++ b/ViewModels/Sources/ViewModels/Entities/CollectionUpdate.swift @@ -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 }