diff --git a/DB/Sources/DB/Content/AccountList.swift b/DB/Sources/DB/Content/AccountList.swift new file mode 100644 index 0000000..dcb63fb --- /dev/null +++ b/DB/Sources/DB/Content/AccountList.swift @@ -0,0 +1,27 @@ +// Copyright © 2020 Metabolist. All rights reserved. + +import Foundation +import GRDB + +public struct AccountList: Codable, FetchableRecord, PersistableRecord { + let id: UUID + + public init() { + id = UUID() + } +} + +extension AccountList { + static let joins = hasMany( + AccountListJoin.self, + using: ForeignKey([Column("listId")])) + .order(Column("index")) + static let accounts = hasMany( + AccountRecord.self, + through: joins, + using: AccountListJoin.account) + + var accounts: QueryInterfaceRequest { + request(for: Self.accounts).accountResultRequest + } +} diff --git a/DB/Sources/DB/Content/AccountListJoin.swift b/DB/Sources/DB/Content/AccountListJoin.swift new file mode 100644 index 0000000..3b67c8d --- /dev/null +++ b/DB/Sources/DB/Content/AccountListJoin.swift @@ -0,0 +1,12 @@ +// Copyright © 2020 Metabolist. All rights reserved. + +import Foundation +import GRDB + +struct AccountListJoin: Codable, FetchableRecord, PersistableRecord { + let accountId: String + let listId: UUID + let index: Int + + static let account = belongsTo(AccountRecord.self, using: ForeignKey([Column("accountId")])) +} diff --git a/DB/Sources/DB/Content/ContentDatabase.swift b/DB/Sources/DB/Content/ContentDatabase.swift index b63db0b..e8ca47b 100644 --- a/DB/Sources/DB/Content/ContentDatabase.swift +++ b/DB/Sources/DB/Content/ContentDatabase.swift @@ -115,10 +115,15 @@ public extension ContentDatabase { .eraseToAnyPublisher() } - func insert(accounts: [Account]) -> AnyPublisher { + func append(accounts: [Account], toList list: AccountList) -> AnyPublisher { databaseWriter.writePublisher { - for account in accounts { + try list.save($0) + + let count = try list.accounts.fetchCount($0) + + for (index, account) in accounts.enumerated() { try account.save($0) + try AccountListJoin(accountId: account.id, listId: list.id, index: count + index).save($0) } } .ignoreOutput() @@ -271,6 +276,14 @@ public extension ContentDatabase { } .eraseToAnyPublisher() } + + func accountListObservation(_ list: AccountList) -> AnyPublisher<[Account], Error> { + ValueObservation.tracking(list.accounts.fetchAll) + .removeDuplicates() + .publisher(in: databaseWriter) + .map { $0.map(Account.init(result:)) } + .eraseToAnyPublisher() + } } private extension ContentDatabase { @@ -390,6 +403,20 @@ private extension ContentDatabase { t.primaryKey(["accountId", "statusId", "collection"], onConflict: .replace) } + + try db.create(table: "accountList") { t in + t.column("id", .text).primaryKey(onConflict: .replace) + } + + try db.create(table: "accountListJoin") { t in + t.column("accountId", .text).indexed().notNull() + .references("accountRecord", column: "id", onDelete: .cascade, onUpdate: .cascade) + t.column("listId", .text).indexed().notNull() + .references("accountList", column: "id", onDelete: .cascade, onUpdate: .cascade) + t.column("index", .integer).notNull() + + t.primaryKey(["accountId", "listId"], onConflict: .replace) + } } return migrator @@ -401,6 +428,7 @@ private extension ContentDatabase { try StatusContextJoin.deleteAll($0) try AccountPinnedStatusJoin.deleteAll($0) try AccountStatusJoin.deleteAll($0) + try AccountList.deleteAll($0) } completion: { _, _ in } } } diff --git a/ServiceLayer/Sources/ServiceLayer/Services/AccountListService.swift b/ServiceLayer/Sources/ServiceLayer/Services/AccountListService.swift index b65fdb5..27709aa 100644 --- a/ServiceLayer/Sources/ServiceLayer/Services/AccountListService.swift +++ b/ServiceLayer/Sources/ServiceLayer/Services/AccountListService.swift @@ -11,6 +11,7 @@ public struct AccountListService { public let nextPageMaxIDs: AnyPublisher public let navigationService: NavigationService + private let list: AccountList private let mastodonAPIClient: MastodonAPIClient private let contentDatabase: ContentDatabase private let requestClosure: (_ maxID: String?, _ minID: String?) -> AnyPublisher @@ -18,27 +19,23 @@ public struct AccountListService { extension AccountListService { init(favoritedByStatusID statusID: String, mastodonAPIClient: MastodonAPIClient, contentDatabase: ContentDatabase) { - let accountSectionsSubject = PassthroughSubject<[[Account]], Error>() + let list = AccountList() let nextPageMaxIDsSubject = PassthroughSubject() self.init( - accountSections: accountSectionsSubject.eraseToAnyPublisher(), + accountSections: contentDatabase.accountListObservation(list).map { [$0] }.eraseToAnyPublisher(), nextPageMaxIDs: nextPageMaxIDsSubject.eraseToAnyPublisher(), navigationService: NavigationService( status: nil, mastodonAPIClient: mastodonAPIClient, contentDatabase: contentDatabase), + list: list, mastodonAPIClient: mastodonAPIClient, contentDatabase: contentDatabase) { maxID, minID -> AnyPublisher in mastodonAPIClient.pagedRequest( AccountsEndpoint.statusFavouritedBy(id: statusID), maxID: maxID, minID: minID) - .handleEvents( - receiveOutput: { - nextPageMaxIDsSubject.send($0.info.maxID) - accountSectionsSubject.send([$0.result]) - }, - receiveCompletion: accountSectionsSubject.send) - .flatMap { contentDatabase.insert(accounts: $0.result) } + .handleEvents(receiveOutput: { nextPageMaxIDsSubject.send($0.info.maxID) }) + .flatMap { contentDatabase.append(accounts: $0.result, toList: list) } .eraseToAnyPublisher() } }