This commit is contained in:
Justin Mazzocchi 2021-02-07 20:38:06 -08:00
parent b32a85aebc
commit f1e3f1a7fa
No known key found for this signature in database
GPG key ID: E223E6937AAFB01C
7 changed files with 130 additions and 12 deletions

View file

@ -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<T: DerivableRequest>(_ request: T) -> T where T.RowDecoder == AccountList {
request.including(all: AccountInfo.addingIncludes(AccountList.accounts).forKey(CodingKeys.accountInfos))
}
static func request(_ request: QueryInterfaceRequest<AccountList>) -> QueryInterfaceRequest<Self> {
addingIncludes(request).asRequest(of: self)
}
}

View file

@ -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)
}
}

View file

@ -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 return migrator
} }
} }

View file

@ -269,12 +269,39 @@ public extension ContentDatabase {
.eraseToAnyPublisher() .eraseToAnyPublisher()
} }
func insert(accounts: [Account]) -> AnyPublisher<Never, Error> { func insert(accounts: [Account], listId: AccountList.Id? = nil) -> AnyPublisher<Never, Error> {
databaseWriter.writePublisher { 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 { for account in accounts {
try account.save($0) 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<Never, Error> {
databaseWriter.writePublisher(
updates: AccountListJoin.filter(
AccountListJoin.Columns.accountId == id
&& AccountListJoin.Columns.accountListId == listId)
.deleteAll)
.ignoreOutput() .ignoreOutput()
.eraseToAnyPublisher() .eraseToAnyPublisher()
} }
@ -494,6 +521,19 @@ public extension ContentDatabase {
.eraseToAnyPublisher() .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> { func listsPublisher() -> AnyPublisher<[Timeline], Error> {
ValueObservation.tracking(TimelineRecord.filter(TimelineRecord.Columns.listId != nil) ValueObservation.tracking(TimelineRecord.filter(TimelineRecord.Columns.listId != nil)
.order(TimelineRecord.Columns.listTitle.asc) .order(TimelineRecord.Columns.listTitle.asc)

View file

@ -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)
}

View file

@ -13,7 +13,7 @@ public struct AccountListService {
public let navigationService: NavigationService public let navigationService: NavigationService
public let canRefresh = false public let canRefresh = false
private let accountsSubject = CurrentValueSubject<[Account], Error>([]) private let listId = UUID().uuidString
private let endpoint: AccountsEndpoint private let endpoint: AccountsEndpoint
private let mastodonAPIClient: MastodonAPIClient private let mastodonAPIClient: MastodonAPIClient
private let contentDatabase: ContentDatabase private let contentDatabase: ContentDatabase
@ -29,10 +29,7 @@ public struct AccountListService {
self.mastodonAPIClient = mastodonAPIClient self.mastodonAPIClient = mastodonAPIClient
self.contentDatabase = contentDatabase self.contentDatabase = contentDatabase
self.titleComponents = titleComponents self.titleComponents = titleComponents
sections = accountsSubject sections = contentDatabase.accountListPublisher(id: listId, configuration: endpoint.configuration)
.map { [.init(items: $0.map { CollectionItem.account($0, endpoint.configuration, nil) })] } // TODO: revisit
.removeDuplicates()
.eraseToAnyPublisher()
nextPageMaxId = nextPageMaxIdSubject.eraseToAnyPublisher() nextPageMaxId = nextPageMaxIdSubject.eraseToAnyPublisher()
accountIdsForRelationships = accountIdsForRelationshipsSubject.eraseToAnyPublisher() accountIdsForRelationships = accountIdsForRelationshipsSubject.eraseToAnyPublisher()
navigationService = NavigationService(mastodonAPIClient: mastodonAPIClient, contentDatabase: contentDatabase) navigationService = NavigationService(mastodonAPIClient: mastodonAPIClient, contentDatabase: contentDatabase)
@ -40,8 +37,8 @@ public struct AccountListService {
} }
public extension AccountListService { public extension AccountListService {
func remove(id: Account.Id) { func remove(id: Account.Id) -> AnyPublisher<Never, Error> {
accountsSubject.value.removeAll { $0.id == id } contentDatabase.remove(id: id, from: listId)
} }
} }
@ -49,15 +46,13 @@ extension AccountListService: CollectionService {
public func request(maxId: String?, minId: String?, search: Search?) -> AnyPublisher<Never, Error> { public func request(maxId: String?, minId: String?, search: Search?) -> AnyPublisher<Never, Error> {
mastodonAPIClient.pagedRequest(endpoint, maxId: maxId, minId: minId) mastodonAPIClient.pagedRequest(endpoint, maxId: maxId, minId: minId)
.handleEvents(receiveOutput: { .handleEvents(receiveOutput: {
let presentIds = Set(accountsSubject.value.map(\.id)) accountIdsForRelationshipsSubject.send(Set($0.result.map(\.id)))
accountsSubject.value.append(contentsOf: $0.result.filter { !presentIds.contains($0.id) })
guard let maxId = $0.info.maxId else { return } guard let maxId = $0.info.maxId else { return }
nextPageMaxIdSubject.send(maxId) 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() .ignoreOutput()
.eraseToAnyPublisher() .eraseToAnyPublisher()
} }

View file

@ -336,6 +336,8 @@ extension CollectionItemsViewModel: CollectionViewModel {
public func applyAccountListEdit(viewModel: AccountViewModel, edit: CollectionItemEvent.AccountListEdit) { public func applyAccountListEdit(viewModel: AccountViewModel, edit: CollectionItemEvent.AccountListEdit) {
(collectionService as? AccountListService)?.remove(id: viewModel.id) (collectionService as? AccountListService)?.remove(id: viewModel.id)
.sink { _ in } receiveValue: { _ in }
.store(in: &cancellables)
switch edit { switch edit {
case .acceptFollowRequest, .rejectFollowRequest: case .acceptFollowRequest, .rejectFollowRequest: