From f1e3f1a7fa211f45b71a10d059cd7904ae51c52b Mon Sep 17 00:00:00 2001 From: Justin Mazzocchi <2831158+jzzocc@users.noreply.github.com> Date: Sun, 7 Feb 2021 20:38:06 -0800 Subject: [PATCH] wip --- .../DB/Content/AccountListItemsInfo.swift | 20 +++++++++ DB/Sources/DB/Content/AccountListJoin.swift | 21 ++++++++++ .../Content/ContentDatabase+Migration.swift | 16 +++++++ DB/Sources/DB/Content/ContentDatabase.swift | 42 ++++++++++++++++++- DB/Sources/DB/Entities/AccountList.swift | 24 +++++++++++ .../Services/AccountListService.swift | 17 +++----- .../CollectionItemsViewModel.swift | 2 + 7 files changed, 130 insertions(+), 12 deletions(-) create mode 100644 DB/Sources/DB/Content/AccountListItemsInfo.swift create mode 100644 DB/Sources/DB/Content/AccountListJoin.swift create mode 100644 DB/Sources/DB/Entities/AccountList.swift diff --git a/DB/Sources/DB/Content/AccountListItemsInfo.swift b/DB/Sources/DB/Content/AccountListItemsInfo.swift new file mode 100644 index 0000000..e2c85d8 --- /dev/null +++ b/DB/Sources/DB/Content/AccountListItemsInfo.swift @@ -0,0 +1,20 @@ +// Copyright © 2021 Metabolist. All rights reserved. + +import Foundation +import GRDB +import Mastodon + +struct AccountListItemsInfo: Codable, Hashable, FetchableRecord { + let accountList: AccountList + let accountInfos: [AccountInfo] +} + +extension AccountListItemsInfo { + static func addingIncludes(_ request: T) -> T where T.RowDecoder == AccountList { + request.including(all: AccountInfo.addingIncludes(AccountList.accounts).forKey(CodingKeys.accountInfos)) + } + + static func request(_ request: QueryInterfaceRequest) -> QueryInterfaceRequest { + addingIncludes(request).asRequest(of: self) + } +} diff --git a/DB/Sources/DB/Content/AccountListJoin.swift b/DB/Sources/DB/Content/AccountListJoin.swift new file mode 100644 index 0000000..b23ee01 --- /dev/null +++ b/DB/Sources/DB/Content/AccountListJoin.swift @@ -0,0 +1,21 @@ +// Copyright © 2021 Metabolist. All rights reserved. + +import Foundation +import GRDB +import Mastodon + +struct AccountListJoin: ContentDatabaseRecord { + let accountListId: AccountList.Id + let accountId: Account.Id + let order: Int + + static let account = belongsTo(AccountRecord.self) +} + +extension AccountListJoin { + enum Columns { + static let accountListId = Column(CodingKeys.accountListId) + static let accountId = Column(CodingKeys.accountId) + static let order = Column(CodingKeys.order) + } +} diff --git a/DB/Sources/DB/Content/ContentDatabase+Migration.swift b/DB/Sources/DB/Content/ContentDatabase+Migration.swift index afef0db..d2cc517 100644 --- a/DB/Sources/DB/Content/ContentDatabase+Migration.swift +++ b/DB/Sources/DB/Content/ContentDatabase+Migration.swift @@ -252,6 +252,22 @@ extension ContentDatabase { } } + migrator.registerMigration("1.0.0") { db in + try db.create(table: "accountList") { t in + t.column("id", .text).primaryKey(onConflict: .replace) + } + + try db.create(table: "accountListJoin") { t in + t.column("accountListId", .text).indexed().notNull() + .references("accountList", onDelete: .cascade) + t.column("accountId", .text).indexed().notNull() + .references("accountRecord", onDelete: .cascade) + t.column("order", .integer).notNull() + + t.primaryKey(["accountListId", "accountId", "order"], onConflict: .replace) + } + } + return migrator } } diff --git a/DB/Sources/DB/Content/ContentDatabase.swift b/DB/Sources/DB/Content/ContentDatabase.swift index 100a085..89c9174 100644 --- a/DB/Sources/DB/Content/ContentDatabase.swift +++ b/DB/Sources/DB/Content/ContentDatabase.swift @@ -269,16 +269,43 @@ public extension ContentDatabase { .eraseToAnyPublisher() } - func insert(accounts: [Account]) -> AnyPublisher { + func insert(accounts: [Account], listId: AccountList.Id? = nil) -> AnyPublisher { databaseWriter.writePublisher { + var order: Int? + + if let listId = listId { + try AccountList(id: listId).save($0) + order = try Int.fetchOne( + $0, + AccountListJoin.filter(AccountListJoin.Columns.accountListId == listId) + .select(max(AccountListJoin.Columns.order))) + ?? 0 + } + for account in accounts { try account.save($0) + + if let listId = listId, let presentOrder = order { + try AccountListJoin(accountListId: listId, accountId: account.id, order: presentOrder).save($0) + + order = presentOrder + 1 + } } } .ignoreOutput() .eraseToAnyPublisher() } + func remove(id: Account.Id, from listId: AccountList.Id) -> AnyPublisher { + databaseWriter.writePublisher( + updates: AccountListJoin.filter( + AccountListJoin.Columns.accountId == id + && AccountListJoin.Columns.accountListId == listId) + .deleteAll) + .ignoreOutput() + .eraseToAnyPublisher() + } + func insert(identityProofs: [IdentityProof], id: Account.Id) -> AnyPublisher { databaseWriter.writePublisher { for identityProof in identityProofs { @@ -494,6 +521,19 @@ public extension ContentDatabase { .eraseToAnyPublisher() } + func accountListPublisher( + id: AccountList.Id, + configuration: CollectionItem.AccountConfiguration) -> AnyPublisher<[CollectionSection], Error> { + ValueObservation.tracking( + AccountListItemsInfo.request(AccountList.filter(AccountList.Columns.id == id)).fetchOne) + .removeDuplicates() + .publisher(in: databaseWriter) + .map { $0?.accountInfos.map { CollectionItem.account(.init(info: $0), configuration, nil) } } + .replaceNil(with: []) + .map { [CollectionSection(items: $0)] } + .eraseToAnyPublisher() + } + func listsPublisher() -> AnyPublisher<[Timeline], Error> { ValueObservation.tracking(TimelineRecord.filter(TimelineRecord.Columns.listId != nil) .order(TimelineRecord.Columns.listTitle.asc) diff --git a/DB/Sources/DB/Entities/AccountList.swift b/DB/Sources/DB/Entities/AccountList.swift new file mode 100644 index 0000000..e68ff79 --- /dev/null +++ b/DB/Sources/DB/Entities/AccountList.swift @@ -0,0 +1,24 @@ +// Copyright © 2021 Metabolist. All rights reserved. + +import Foundation +import GRDB + +public struct AccountList: ContentDatabaseRecord, Hashable { + let id: Id +} + +public extension AccountList { + typealias Id = String +} + +extension AccountList { + enum Columns { + static let id = Column(CodingKeys.id) + } + + static let accountListJoins = hasMany(AccountListJoin.self) + static let accounts = hasMany( + AccountRecord.self, + through: accountListJoins.order(AccountListJoin.Columns.order), + using: AccountListJoin.account) +} diff --git a/ServiceLayer/Sources/ServiceLayer/Services/AccountListService.swift b/ServiceLayer/Sources/ServiceLayer/Services/AccountListService.swift index 7d8fcef..39950d9 100644 --- a/ServiceLayer/Sources/ServiceLayer/Services/AccountListService.swift +++ b/ServiceLayer/Sources/ServiceLayer/Services/AccountListService.swift @@ -13,7 +13,7 @@ public struct AccountListService { public let navigationService: NavigationService public let canRefresh = false - private let accountsSubject = CurrentValueSubject<[Account], Error>([]) + private let listId = UUID().uuidString private let endpoint: AccountsEndpoint private let mastodonAPIClient: MastodonAPIClient private let contentDatabase: ContentDatabase @@ -29,10 +29,7 @@ public struct AccountListService { self.mastodonAPIClient = mastodonAPIClient self.contentDatabase = contentDatabase self.titleComponents = titleComponents - sections = accountsSubject - .map { [.init(items: $0.map { CollectionItem.account($0, endpoint.configuration, nil) })] } // TODO: revisit - .removeDuplicates() - .eraseToAnyPublisher() + sections = contentDatabase.accountListPublisher(id: listId, configuration: endpoint.configuration) nextPageMaxId = nextPageMaxIdSubject.eraseToAnyPublisher() accountIdsForRelationships = accountIdsForRelationshipsSubject.eraseToAnyPublisher() navigationService = NavigationService(mastodonAPIClient: mastodonAPIClient, contentDatabase: contentDatabase) @@ -40,8 +37,8 @@ public struct AccountListService { } public extension AccountListService { - func remove(id: Account.Id) { - accountsSubject.value.removeAll { $0.id == id } + func remove(id: Account.Id) -> AnyPublisher { + contentDatabase.remove(id: id, from: listId) } } @@ -49,15 +46,13 @@ extension AccountListService: CollectionService { public func request(maxId: String?, minId: String?, search: Search?) -> AnyPublisher { mastodonAPIClient.pagedRequest(endpoint, maxId: maxId, minId: minId) .handleEvents(receiveOutput: { - let presentIds = Set(accountsSubject.value.map(\.id)) - accountsSubject.value.append(contentsOf: $0.result.filter { !presentIds.contains($0.id) }) + accountIdsForRelationshipsSubject.send(Set($0.result.map(\.id))) guard let maxId = $0.info.maxId else { return } nextPageMaxIdSubject.send(maxId) - accountIdsForRelationshipsSubject.send(Set($0.result.map(\.id))) }) - .flatMap { contentDatabase.insert(accounts: $0.result) } + .flatMap { contentDatabase.insert(accounts: $0.result, listId: listId) } .ignoreOutput() .eraseToAnyPublisher() } diff --git a/ViewModels/Sources/ViewModels/View Models/CollectionItemsViewModel.swift b/ViewModels/Sources/ViewModels/View Models/CollectionItemsViewModel.swift index d42b382..3312c8c 100644 --- a/ViewModels/Sources/ViewModels/View Models/CollectionItemsViewModel.swift +++ b/ViewModels/Sources/ViewModels/View Models/CollectionItemsViewModel.swift @@ -336,6 +336,8 @@ extension CollectionItemsViewModel: CollectionViewModel { public func applyAccountListEdit(viewModel: AccountViewModel, edit: CollectionItemEvent.AccountListEdit) { (collectionService as? AccountListService)?.remove(id: viewModel.id) + .sink { _ in } receiveValue: { _ in } + .store(in: &cancellables) switch edit { case .acceptFollowRequest, .rejectFollowRequest: