Use timeline logic for profiles
This commit is contained in:
parent
31156dd482
commit
e7c6ac3f98
|
@ -70,21 +70,11 @@ extension AccountRecord {
|
|||
StatusRecord.self,
|
||||
through: pinnedStatusJoins,
|
||||
using: AccountPinnedStatusJoin.status)
|
||||
static let statusJoins = hasMany(AccountStatusJoin.self)
|
||||
|
||||
var pinnedStatuses: QueryInterfaceRequest<StatusInfo> {
|
||||
StatusInfo.request(request(for: Self.pinnedStatuses))
|
||||
}
|
||||
|
||||
func statuses(collection: ProfileCollection) -> QueryInterfaceRequest<StatusInfo> {
|
||||
StatusInfo.request(
|
||||
request(for: Self.hasMany(
|
||||
StatusRecord.self,
|
||||
through: Self.statusJoins.filter(AccountStatusJoin.Columns.collection == collection.rawValue),
|
||||
using: AccountStatusJoin.status)
|
||||
.order(StatusRecord.Columns.createdAt.desc)))
|
||||
}
|
||||
|
||||
init(account: Account) {
|
||||
id = account.id
|
||||
username = account.username
|
||||
|
|
|
@ -1,20 +0,0 @@
|
|||
// Copyright © 2020 Metabolist. All rights reserved.
|
||||
|
||||
import Foundation
|
||||
import GRDB
|
||||
|
||||
struct AccountStatusJoin: Codable, FetchableRecord, PersistableRecord {
|
||||
let accountId: String
|
||||
let statusId: String
|
||||
let collection: ProfileCollection
|
||||
|
||||
static let status = belongsTo(StatusRecord.self)
|
||||
}
|
||||
|
||||
extension AccountStatusJoin {
|
||||
enum Columns {
|
||||
static let accountId = Column(AccountStatusJoin.CodingKeys.accountId)
|
||||
static let statusId = Column(AccountStatusJoin.CodingKeys.statusId)
|
||||
static let collection = Column(AccountStatusJoin.CodingKeys.collection)
|
||||
}
|
||||
}
|
|
@ -27,14 +27,14 @@ extension ContentDatabase {
|
|||
t.column("emojis", .blob).notNull()
|
||||
t.column("bot", .boolean).notNull()
|
||||
t.column("discoverable", .boolean)
|
||||
t.column("movedId", .text).references("accountRecord", column: "id")
|
||||
t.column("movedId", .text).references("accountRecord")
|
||||
}
|
||||
|
||||
try db.create(table: "statusRecord") { t in
|
||||
t.column("id", .text).primaryKey(onConflict: .replace)
|
||||
t.column("uri", .text).notNull()
|
||||
t.column("createdAt", .datetime).notNull()
|
||||
t.column("accountId", .text).notNull().references("accountRecord", column: "id")
|
||||
t.column("accountId", .text).notNull().references("accountRecord")
|
||||
t.column("content", .text).notNull()
|
||||
t.column("visibility", .text).notNull()
|
||||
t.column("sensitive", .boolean).notNull()
|
||||
|
@ -50,7 +50,7 @@ extension ContentDatabase {
|
|||
t.column("url", .text)
|
||||
t.column("inReplyToId", .text)
|
||||
t.column("inReplyToAccountId", .text)
|
||||
t.column("reblogId", .text).references("statusRecord", column: "id")
|
||||
t.column("reblogId", .text).references("statusRecord")
|
||||
t.column("poll", .blob)
|
||||
t.column("card", .blob)
|
||||
t.column("language", .text)
|
||||
|
@ -62,16 +62,20 @@ extension ContentDatabase {
|
|||
t.column("pinned", .boolean)
|
||||
}
|
||||
|
||||
try db.create(table: "timeline") { t in
|
||||
try db.create(table: "timelineRecord") { t in
|
||||
t.column("id", .text).primaryKey(onConflict: .replace)
|
||||
t.column("listId", .text)
|
||||
t.column("listTitle", .text).indexed().collate(.localizedCaseInsensitiveCompare)
|
||||
t.column("tag", .text)
|
||||
t.column("accountId", .text).references("accountRecord", onDelete: .cascade, onUpdate: .cascade)
|
||||
t.column("profileCollection", .text)
|
||||
}
|
||||
|
||||
try db.create(table: "timelineStatusJoin") { t in
|
||||
t.column("timelineId", .text).indexed().notNull()
|
||||
.references("timeline", column: "id", onDelete: .cascade, onUpdate: .cascade)
|
||||
.references("timelineRecord", onDelete: .cascade, onUpdate: .cascade)
|
||||
t.column("statusId", .text).indexed().notNull()
|
||||
.references("statusRecord", column: "id", onDelete: .cascade, onUpdate: .cascade)
|
||||
.references("statusRecord", onDelete: .cascade, onUpdate: .cascade)
|
||||
|
||||
t.primaryKey(["timelineId", "statusId"], onConflict: .replace)
|
||||
}
|
||||
|
@ -87,9 +91,9 @@ extension ContentDatabase {
|
|||
|
||||
try db.create(table: "statusContextJoin") { t in
|
||||
t.column("parentId", .text).indexed().notNull()
|
||||
.references("statusRecord", column: "id", onDelete: .cascade, onUpdate: .cascade)
|
||||
.references("statusRecord", onDelete: .cascade, onUpdate: .cascade)
|
||||
t.column("statusId", .text).indexed().notNull()
|
||||
.references("statusRecord", column: "id", onDelete: .cascade, onUpdate: .cascade)
|
||||
.references("statusRecord", onDelete: .cascade, onUpdate: .cascade)
|
||||
t.column("section", .text).indexed().notNull()
|
||||
t.column("index", .integer).notNull()
|
||||
|
||||
|
@ -98,33 +102,23 @@ extension ContentDatabase {
|
|||
|
||||
try db.create(table: "accountPinnedStatusJoin") { t in
|
||||
t.column("accountId", .text).indexed().notNull()
|
||||
.references("accountRecord", column: "id", onDelete: .cascade, onUpdate: .cascade)
|
||||
.references("accountRecord", onDelete: .cascade, onUpdate: .cascade)
|
||||
t.column("statusId", .text).indexed().notNull()
|
||||
.references("statusRecord", column: "id", onDelete: .cascade, onUpdate: .cascade)
|
||||
.references("statusRecord", onDelete: .cascade, onUpdate: .cascade)
|
||||
t.column("index", .integer).notNull()
|
||||
|
||||
t.primaryKey(["accountId", "statusId"], onConflict: .replace)
|
||||
}
|
||||
|
||||
try db.create(table: "accountStatusJoin") { t in
|
||||
t.column("accountId", .text).indexed().notNull()
|
||||
.references("accountRecord", column: "id", onDelete: .cascade, onUpdate: .cascade)
|
||||
t.column("statusId", .text).indexed().notNull()
|
||||
.references("statusRecord", column: "id", onDelete: .cascade, onUpdate: .cascade)
|
||||
t.column("collection", .text).indexed().notNull()
|
||||
|
||||
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)
|
||||
.references("accountRecord", onDelete: .cascade, onUpdate: .cascade)
|
||||
t.column("listId", .text).indexed().notNull()
|
||||
.references("accountList", column: "id", onDelete: .cascade, onUpdate: .cascade)
|
||||
.references("accountList", onDelete: .cascade, onUpdate: .cascade)
|
||||
t.column("index", .integer).notNull()
|
||||
|
||||
t.primaryKey(["accountId", "listId"], onConflict: .replace)
|
||||
|
|
|
@ -99,21 +99,6 @@ public extension ContentDatabase {
|
|||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
func insert(
|
||||
statuses: [Status],
|
||||
accountID: String,
|
||||
collection: ProfileCollection) -> AnyPublisher<Never, Error> {
|
||||
databaseWriter.writePublisher {
|
||||
for status in statuses {
|
||||
try status.save($0)
|
||||
|
||||
try AccountStatusJoin(accountId: accountID, statusId: status.id, collection: collection).save($0)
|
||||
}
|
||||
}
|
||||
.ignoreOutput()
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
func append(accounts: [Account], toList list: AccountList) -> AnyPublisher<Never, Error> {
|
||||
databaseWriter.writePublisher {
|
||||
try list.save($0)
|
||||
|
@ -135,9 +120,9 @@ public extension ContentDatabase {
|
|||
try Timeline.list(list).save($0)
|
||||
}
|
||||
|
||||
try Timeline
|
||||
.filter(!(Timeline.authenticatedDefaults.map(\.id) + lists.map(\.id)).contains(Timeline.Columns.id)
|
||||
&& Timeline.Columns.listTitle != nil)
|
||||
try TimelineRecord
|
||||
.filter(!lists.map(\.id).contains(TimelineRecord.Columns.listId)
|
||||
&& TimelineRecord.Columns.listTitle != nil)
|
||||
.deleteAll($0)
|
||||
}
|
||||
.ignoreOutput()
|
||||
|
@ -151,7 +136,7 @@ public extension ContentDatabase {
|
|||
}
|
||||
|
||||
func deleteList(id: String) -> AnyPublisher<Never, Error> {
|
||||
databaseWriter.writePublisher(updates: Timeline.filter(Timeline.Columns.id == id).deleteAll)
|
||||
databaseWriter.writePublisher(updates: TimelineRecord.filter(TimelineRecord.Columns.listId == id).deleteAll)
|
||||
.ignoreOutput()
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
@ -181,10 +166,21 @@ public extension ContentDatabase {
|
|||
}
|
||||
|
||||
func statusesObservation(timeline: Timeline) -> AnyPublisher<[[Status]], Error> {
|
||||
ValueObservation.tracking(timeline.statuses.fetchAll)
|
||||
ValueObservation.tracking { db -> [[StatusInfo]] in
|
||||
let statuses = try TimelineRecord(timeline: timeline).statuses.fetchAll(db)
|
||||
|
||||
if case let .profile(accountId, profileCollection) = timeline, profileCollection == .statuses {
|
||||
let pinnedStatuses = try AccountRecord.filter(AccountRecord.Columns.id == accountId)
|
||||
.fetchOne(db)?.pinnedStatuses.fetchAll(db) ?? []
|
||||
|
||||
return [pinnedStatuses, statuses]
|
||||
} else {
|
||||
return [statuses]
|
||||
}
|
||||
}
|
||||
.removeDuplicates()
|
||||
.map { $0.map { $0.map(Status.init(info:)) } }
|
||||
.publisher(in: databaseWriter)
|
||||
.map { [$0.map(Status.init(info:))] }
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
|
@ -201,40 +197,17 @@ public extension ContentDatabase {
|
|||
return [ancestors, [parent], descendants]
|
||||
}
|
||||
.removeDuplicates()
|
||||
.publisher(in: databaseWriter)
|
||||
.map { $0.map { $0.map(Status.init(info:)) } }
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
func statusesObservation(
|
||||
accountID: String,
|
||||
collection: ProfileCollection) -> AnyPublisher<[[Status]], Error> {
|
||||
ValueObservation.tracking { db -> [[StatusInfo]] in
|
||||
guard let accountRecord = try AccountRecord
|
||||
.filter(AccountRecord.Columns.id == accountID)
|
||||
.fetchOne(db) else {
|
||||
return []
|
||||
}
|
||||
|
||||
let statuses = try accountRecord.statuses(collection: collection).fetchAll(db)
|
||||
|
||||
if case .statuses = collection {
|
||||
return [try accountRecord.pinnedStatuses.fetchAll(db), statuses]
|
||||
} else {
|
||||
return [statuses]
|
||||
}
|
||||
}
|
||||
.removeDuplicates()
|
||||
.publisher(in: databaseWriter)
|
||||
.map { $0.map { $0.map(Status.init(info:)) } }
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
func listsObservation() -> AnyPublisher<[Timeline], Error> {
|
||||
ValueObservation.tracking(Timeline.filter(Timeline.Columns.listTitle != nil)
|
||||
.order(Timeline.Columns.listTitle.asc)
|
||||
ValueObservation.tracking(TimelineRecord.filter(TimelineRecord.Columns.listId != nil)
|
||||
.order(TimelineRecord.Columns.listTitle.asc)
|
||||
.fetchAll)
|
||||
.removeDuplicates()
|
||||
.map { $0.map(Timeline.init(record:)).compactMap { $0 } }
|
||||
.publisher(in: databaseWriter)
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
@ -243,12 +216,12 @@ public extension ContentDatabase {
|
|||
ValueObservation.tracking(
|
||||
Filter.filter(Filter.Columns.expiresAt == nil || Filter.Columns.expiresAt > date).fetchAll)
|
||||
.removeDuplicates()
|
||||
.publisher(in: databaseWriter)
|
||||
.map {
|
||||
guard let context = context else { return $0 }
|
||||
|
||||
return $0.filter { $0.context.contains(context) }
|
||||
}
|
||||
.publisher(in: databaseWriter)
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
|
@ -262,7 +235,6 @@ public extension ContentDatabase {
|
|||
func accountObservation(id: String) -> AnyPublisher<Account?, Error> {
|
||||
ValueObservation.tracking(AccountInfo.request(AccountRecord.filter(AccountRecord.Columns.id == id)).fetchOne)
|
||||
.removeDuplicates()
|
||||
.publisher(in: databaseWriter)
|
||||
.map {
|
||||
if let info = $0 {
|
||||
return Account(info: info)
|
||||
|
@ -270,14 +242,15 @@ public extension ContentDatabase {
|
|||
return nil
|
||||
}
|
||||
}
|
||||
.publisher(in: databaseWriter)
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
func accountListObservation(_ list: AccountList) -> AnyPublisher<[Account], Error> {
|
||||
ValueObservation.tracking(list.accounts.fetchAll)
|
||||
.removeDuplicates()
|
||||
.publisher(in: databaseWriter)
|
||||
.map { $0.map(Account.init(info:)) }
|
||||
.publisher(in: databaseWriter)
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
}
|
||||
|
@ -289,7 +262,7 @@ private extension ContentDatabase {
|
|||
|
||||
func clean() throws {
|
||||
try databaseWriter.write {
|
||||
try Timeline.deleteAll($0)
|
||||
try TimelineRecord.deleteAll($0)
|
||||
try StatusRecord.deleteAll($0)
|
||||
try AccountRecord.deleteAll($0)
|
||||
try AccountList.deleteAll($0)
|
||||
|
|
|
@ -0,0 +1,77 @@
|
|||
// Copyright © 2020 Metabolist. All rights reserved.
|
||||
|
||||
import Foundation
|
||||
import GRDB
|
||||
import Mastodon
|
||||
|
||||
struct TimelineRecord: Codable, Hashable {
|
||||
let id: String
|
||||
let listId: String?
|
||||
let listTitle: String?
|
||||
let tag: String?
|
||||
let accountId: String?
|
||||
let profileCollection: ProfileCollection?
|
||||
}
|
||||
|
||||
extension TimelineRecord: FetchableRecord, PersistableRecord {
|
||||
static func databaseJSONDecoder(for column: String) -> JSONDecoder {
|
||||
MastodonDecoder()
|
||||
}
|
||||
|
||||
static func databaseJSONEncoder(for column: String) -> JSONEncoder {
|
||||
MastodonEncoder()
|
||||
}
|
||||
}
|
||||
|
||||
extension TimelineRecord {
|
||||
enum Columns {
|
||||
static let id = Column(TimelineRecord.CodingKeys.id)
|
||||
static let listId = Column(TimelineRecord.CodingKeys.listId)
|
||||
static let listTitle = Column(TimelineRecord.CodingKeys.listTitle)
|
||||
static let tag = Column(TimelineRecord.CodingKeys.tag)
|
||||
static let accountId = Column(TimelineRecord.CodingKeys.accountId)
|
||||
static let profileCollection = Column(TimelineRecord.CodingKeys.profileCollection)
|
||||
}
|
||||
|
||||
static let statusJoins = hasMany(TimelineStatusJoin.self)
|
||||
static let statuses = hasMany(
|
||||
StatusRecord.self,
|
||||
through: statusJoins,
|
||||
using: TimelineStatusJoin.status)
|
||||
.order(StatusRecord.Columns.createdAt.desc)
|
||||
|
||||
var statuses: QueryInterfaceRequest<StatusInfo> {
|
||||
StatusInfo.request(request(for: Self.statuses))
|
||||
}
|
||||
|
||||
init(timeline: Timeline) {
|
||||
id = timeline.id
|
||||
|
||||
switch timeline {
|
||||
case .home, .local, .federated:
|
||||
listId = nil
|
||||
listTitle = nil
|
||||
tag = nil
|
||||
accountId = nil
|
||||
profileCollection = nil
|
||||
case let .list(list):
|
||||
listId = list.id
|
||||
listTitle = list.title
|
||||
tag = nil
|
||||
accountId = nil
|
||||
profileCollection = nil
|
||||
case let .tag(tag):
|
||||
listId = nil
|
||||
listTitle = nil
|
||||
self.tag = tag
|
||||
accountId = nil
|
||||
profileCollection = nil
|
||||
case let .profile(accountId, profileCollection):
|
||||
listId = nil
|
||||
listTitle = nil
|
||||
tag = nil
|
||||
self.accountId = accountId
|
||||
self.profileCollection = profileCollection
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,6 +1,7 @@
|
|||
// Copyright © 2020 Metabolist. All rights reserved.
|
||||
|
||||
import Foundation
|
||||
import Mastodon
|
||||
|
||||
public enum Timeline: Hashable {
|
||||
case home
|
||||
|
@ -8,6 +9,7 @@ public enum Timeline: Hashable {
|
|||
case federated
|
||||
case list(List)
|
||||
case tag(String)
|
||||
case profile(accountId: String, profileCollection: ProfileCollection)
|
||||
}
|
||||
|
||||
public extension Timeline {
|
||||
|
@ -25,9 +27,11 @@ extension Timeline: Identifiable {
|
|||
case .federated:
|
||||
return "federated"
|
||||
case let .list(list):
|
||||
return list.id
|
||||
return "list-".appending(list.id)
|
||||
case let .tag(tag):
|
||||
return "#".appending(tag).lowercased()
|
||||
return "tag-".appending(tag).lowercased()
|
||||
case let .profile(accountId, profileCollection):
|
||||
return "profile-\(accountId)-\(profileCollection)"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -4,48 +4,32 @@ import Foundation
|
|||
import GRDB
|
||||
import Mastodon
|
||||
|
||||
extension Timeline: FetchableRecord, PersistableRecord {
|
||||
enum Columns: String, ColumnExpression {
|
||||
case id
|
||||
case listTitle
|
||||
}
|
||||
|
||||
public init(row: Row) {
|
||||
switch (row[Columns.id] as String, row[Columns.listTitle] as String?) {
|
||||
case (Timeline.home.id, _):
|
||||
self = .home
|
||||
case (Timeline.local.id, _):
|
||||
self = .local
|
||||
case (Timeline.federated.id, _):
|
||||
self = .federated
|
||||
case (let id, .some(let title)):
|
||||
self = .list(List(id: id, title: title))
|
||||
default:
|
||||
var tag: String = row[Columns.id]
|
||||
|
||||
tag.removeFirst()
|
||||
self = .tag(tag)
|
||||
}
|
||||
}
|
||||
|
||||
public func encode(to container: inout PersistenceContainer) {
|
||||
container[Columns.id] = id
|
||||
|
||||
if case let .list(list) = self {
|
||||
container[Columns.listTitle] = list.title
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Timeline {
|
||||
static let statusJoins = hasMany(TimelineStatusJoin.self)
|
||||
static let statuses = hasMany(
|
||||
StatusRecord.self,
|
||||
through: statusJoins,
|
||||
using: TimelineStatusJoin.status)
|
||||
.order(StatusRecord.Columns.createdAt.desc)
|
||||
func save(_ db: Database) throws {
|
||||
try TimelineRecord(timeline: self).save(db)
|
||||
}
|
||||
|
||||
var statuses: QueryInterfaceRequest<StatusInfo> {
|
||||
StatusInfo.request(request(for: Self.statuses))
|
||||
init?(record: TimelineRecord) {
|
||||
switch (record.id,
|
||||
record.listId,
|
||||
record.listTitle,
|
||||
record.tag,
|
||||
record.accountId,
|
||||
record.profileCollection) {
|
||||
case (Timeline.home.id, _, _, _, _, _):
|
||||
self = .home
|
||||
case (Timeline.local.id, _, _, _, _, _):
|
||||
self = .local
|
||||
case (Timeline.federated.id, _, _, _, _, _):
|
||||
self = .federated
|
||||
case (_, .some(let listId), .some(let listTitle), _, _, _):
|
||||
self = .list(List(id: listId, title: listTitle))
|
||||
case (_, _, _, .some(let tag), _, _):
|
||||
self = .tag(tag)
|
||||
case (_, _, _, _, .some(let accountId), .some(let profileCollection)):
|
||||
self = .profile(accountId: accountId, profileCollection: profileCollection)
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,21 +0,0 @@
|
|||
// Copyright © 2020 Metabolist. All rights reserved.
|
||||
|
||||
import Foundation
|
||||
import Mastodon
|
||||
|
||||
public extension Timeline {
|
||||
var endpoint: StatusesEndpoint {
|
||||
switch self {
|
||||
case .home:
|
||||
return .timelinesHome
|
||||
case .local:
|
||||
return .timelinesPublic(local: true)
|
||||
case .federated:
|
||||
return .timelinesPublic(local: false)
|
||||
case let .list(list):
|
||||
return .timelinesList(id: list.id)
|
||||
case let .tag(tag):
|
||||
return .timelinesTag(tag)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
// Copyright © 2020 Metabolist. All rights reserved.
|
||||
|
||||
import DB
|
||||
|
||||
public typealias Timeline = DB.Timeline
|
|
@ -7,7 +7,7 @@ import Mastodon
|
|||
import MastodonAPI
|
||||
|
||||
public struct ProfileService {
|
||||
public let accountService: AnyPublisher<AccountService, Error>
|
||||
public let accountServicePublisher: AnyPublisher<AccountService, Error>
|
||||
|
||||
private let accountID: String
|
||||
private let mastodonAPIClient: MastodonAPIClient
|
||||
|
@ -45,18 +45,16 @@ public struct ProfileService {
|
|||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
accountService = accountPublisher
|
||||
accountServicePublisher = accountPublisher
|
||||
.map { AccountService(account: $0, mastodonAPIClient: mastodonAPIClient, contentDatabase: contentDatabase) }
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
}
|
||||
|
||||
public extension ProfileService {
|
||||
func statusListService(
|
||||
collectionPublisher: CurrentValueSubject<ProfileCollection, Never>) -> StatusListService {
|
||||
func statusListService(profileCollection: ProfileCollection) -> StatusListService {
|
||||
StatusListService(
|
||||
accountID: accountID,
|
||||
collection: collectionPublisher,
|
||||
timeline: .profile(accountId: accountID, profileCollection: profileCollection),
|
||||
mastodonAPIClient: mastodonAPIClient,
|
||||
contentDatabase: contentDatabase)
|
||||
}
|
||||
|
|
|
@ -21,15 +21,6 @@ public struct StatusListService {
|
|||
|
||||
extension StatusListService {
|
||||
init(timeline: Timeline, mastodonAPIClient: MastodonAPIClient, contentDatabase: ContentDatabase) {
|
||||
let filterContext: Filter.Context
|
||||
|
||||
switch timeline {
|
||||
case .home, .list:
|
||||
filterContext = .home
|
||||
case .local, .federated, .tag:
|
||||
filterContext = .public
|
||||
}
|
||||
|
||||
var title: String?
|
||||
|
||||
if case let .tag(tag) = timeline {
|
||||
|
@ -46,7 +37,7 @@ extension StatusListService {
|
|||
status: nil,
|
||||
mastodonAPIClient: mastodonAPIClient,
|
||||
contentDatabase: contentDatabase),
|
||||
filterContext: filterContext,
|
||||
filterContext: timeline.filterContext,
|
||||
mastodonAPIClient: mastodonAPIClient,
|
||||
contentDatabase: contentDatabase) { maxID, minID in
|
||||
mastodonAPIClient.pagedRequest(timeline.endpoint, maxID: maxID, minID: minID)
|
||||
|
@ -78,59 +69,6 @@ extension StatusListService {
|
|||
.eraseToAnyPublisher()
|
||||
}
|
||||
}
|
||||
|
||||
init(
|
||||
accountID: String,
|
||||
collection: CurrentValueSubject<ProfileCollection, Never>,
|
||||
mastodonAPIClient: MastodonAPIClient,
|
||||
contentDatabase: ContentDatabase) {
|
||||
let nextPageMaxIDsSubject = PassthroughSubject<String?, Never>()
|
||||
|
||||
self.init(
|
||||
statusSections: collection
|
||||
.flatMap { contentDatabase.statusesObservation(accountID: accountID, collection: $0) }
|
||||
.eraseToAnyPublisher(),
|
||||
nextPageMaxIDs: nextPageMaxIDsSubject.eraseToAnyPublisher(),
|
||||
contextParentID: nil,
|
||||
title: nil,
|
||||
navigationService: NavigationService(
|
||||
status: nil,
|
||||
mastodonAPIClient: mastodonAPIClient,
|
||||
contentDatabase: contentDatabase),
|
||||
filterContext: .account,
|
||||
mastodonAPIClient: mastodonAPIClient,
|
||||
contentDatabase: contentDatabase) { maxID, minID in
|
||||
let excludeReplies: Bool
|
||||
let onlyMedia: Bool
|
||||
|
||||
switch collection.value {
|
||||
case .statuses:
|
||||
excludeReplies = true
|
||||
onlyMedia = false
|
||||
case .statusesAndReplies:
|
||||
excludeReplies = false
|
||||
onlyMedia = false
|
||||
case .media:
|
||||
excludeReplies = true
|
||||
onlyMedia = true
|
||||
}
|
||||
|
||||
let endpoint = StatusesEndpoint.accountsStatuses(
|
||||
id: accountID,
|
||||
excludeReplies: excludeReplies,
|
||||
onlyMedia: onlyMedia,
|
||||
pinned: false)
|
||||
return mastodonAPIClient.pagedRequest(endpoint, maxID: maxID, minID: minID)
|
||||
.handleEvents(receiveOutput: { nextPageMaxIDsSubject.send($0.info.maxID) })
|
||||
.flatMap {
|
||||
contentDatabase.insert(
|
||||
statuses: $0.result,
|
||||
accountID: accountID,
|
||||
collection: collection.value)
|
||||
}
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public extension StatusListService {
|
||||
|
@ -142,3 +80,52 @@ public extension StatusListService {
|
|||
contentDatabase.activeFiltersObservation(date: Date(), context: filterContext)
|
||||
}
|
||||
}
|
||||
|
||||
private extension Timeline {
|
||||
var endpoint: StatusesEndpoint {
|
||||
switch self {
|
||||
case .home:
|
||||
return .timelinesHome
|
||||
case .local:
|
||||
return .timelinesPublic(local: true)
|
||||
case .federated:
|
||||
return .timelinesPublic(local: false)
|
||||
case let .list(list):
|
||||
return .timelinesList(id: list.id)
|
||||
case let .tag(tag):
|
||||
return .timelinesTag(tag)
|
||||
case let .profile(accountId, profileCollection):
|
||||
let excludeReplies: Bool
|
||||
let onlyMedia: Bool
|
||||
|
||||
switch profileCollection {
|
||||
case .statuses:
|
||||
excludeReplies = true
|
||||
onlyMedia = false
|
||||
case .statusesAndReplies:
|
||||
excludeReplies = false
|
||||
onlyMedia = false
|
||||
case .media:
|
||||
excludeReplies = true
|
||||
onlyMedia = true
|
||||
}
|
||||
|
||||
return .accountsStatuses(
|
||||
id: accountId,
|
||||
excludeReplies: excludeReplies,
|
||||
onlyMedia: onlyMedia,
|
||||
pinned: false)
|
||||
}
|
||||
}
|
||||
|
||||
var filterContext: Filter.Context {
|
||||
switch self {
|
||||
case .home, .list:
|
||||
return .home
|
||||
case .local, .federated, .tag:
|
||||
return .public
|
||||
case .profile:
|
||||
return .account
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
// Copyright © 2020 Metabolist. All rights reserved.
|
||||
|
||||
import ServiceLayer
|
||||
|
||||
public typealias Timeline = ServiceLayer.Timeline
|
|
@ -53,7 +53,7 @@ public extension NavigationViewModel {
|
|||
switch timeline {
|
||||
case .home, .list:
|
||||
return identification.identity.handle
|
||||
case .local, .federated, .tag:
|
||||
case .local, .federated, .tag, .profile:
|
||||
return identification.identity.instance?.uri ?? ""
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,57 +5,86 @@ import Foundation
|
|||
import Mastodon
|
||||
import ServiceLayer
|
||||
|
||||
public class ProfileViewModel: StatusListViewModel {
|
||||
final public class ProfileViewModel {
|
||||
@Published public private(set) var accountViewModel: AccountViewModel?
|
||||
@Published public var collection = ProfileCollection.statuses
|
||||
@Published public var alertItem: AlertItem?
|
||||
|
||||
private let profileService: ProfileService
|
||||
private let collectionViewModel: CurrentValueSubject<StatusListViewModel, Never>
|
||||
private var cancellables = Set<AnyCancellable>()
|
||||
|
||||
init(profileService: ProfileService) {
|
||||
self.profileService = profileService
|
||||
|
||||
let collectionSubject = CurrentValueSubject<ProfileCollection, Never>(.statuses)
|
||||
collectionViewModel = CurrentValueSubject(
|
||||
StatusListViewModel(statusListService: profileService.statusListService(profileCollection: .statuses)))
|
||||
|
||||
super.init(
|
||||
statusListService: profileService.statusListService(
|
||||
collectionPublisher: collectionSubject))
|
||||
|
||||
$collection.sink(receiveValue: collectionSubject.send).store(in: &cancellables)
|
||||
|
||||
profileService.accountService
|
||||
profileService.accountServicePublisher
|
||||
.map(AccountViewModel.init(accountService:))
|
||||
.assignErrorsToAlertItem(to: \.alertItem, on: self)
|
||||
.assign(to: &$accountViewModel)
|
||||
}
|
||||
|
||||
public override var collectionItems: AnyPublisher<[[CollectionItem]], Never> {
|
||||
// The pinned key is added to the info of collection items in the first section
|
||||
// so a diffable data source can potentially render it in both sections
|
||||
super.collectionItems
|
||||
.map {
|
||||
$collection.dropFirst()
|
||||
.map(profileService.statusListService(profileCollection:))
|
||||
.map(StatusListViewModel.init(statusListService:))
|
||||
.sink { [weak self] in
|
||||
guard let self = self else { return }
|
||||
|
||||
self.collectionViewModel.send($0)
|
||||
$0.$alertItem.assign(to: &self.$alertItem)
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
}
|
||||
|
||||
extension ProfileViewModel: CollectionViewModel {
|
||||
public var collectionItems: AnyPublisher<[[CollectionItem]], Never> {
|
||||
collectionViewModel.flatMap(\.collectionItems).map {
|
||||
$0.enumerated().map { [weak self] in
|
||||
if let self = self, self.collection == .statuses, $0 == 0 {
|
||||
// The pinned key is added to the info of collection items in the first section
|
||||
// so a diffable data source can potentially render it in both sections
|
||||
return $1.map { .init(id: $0.id, kind: $0.kind, info: [.pinned: true]) }
|
||||
} else {
|
||||
return $1
|
||||
}
|
||||
}
|
||||
}
|
||||
.eraseToAnyPublisher()
|
||||
}.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
public override var navigationEvents: AnyPublisher<NavigationEvent, Never> {
|
||||
public var title: AnyPublisher<String?, Never> {
|
||||
$accountViewModel.map { $0?.accountName }.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
public var alertItems: AnyPublisher<AlertItem, Never> {
|
||||
collectionViewModel.flatMap(\.alertItems).eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
public var loading: AnyPublisher<Bool, Never> {
|
||||
collectionViewModel.flatMap(\.loading).eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
public var navigationEvents: AnyPublisher<NavigationEvent, Never> {
|
||||
$accountViewModel.compactMap { $0 }
|
||||
.flatMap(\.events)
|
||||
.flatMap { $0 }
|
||||
.map(NavigationEvent.init)
|
||||
.compactMap { $0 }
|
||||
.assignErrorsToAlertItem(to: \.alertItem, on: self)
|
||||
.merge(with: super.navigationEvents)
|
||||
.merge(with: collectionViewModel.flatMap(\.navigationEvents))
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
public override func request(maxID: String? = nil, minID: String? = nil) {
|
||||
public var nextPageMaxID: String? {
|
||||
collectionViewModel.value.nextPageMaxID
|
||||
}
|
||||
|
||||
public var maintainScrollPositionOfItem: CollectionItem? {
|
||||
collectionViewModel.value.maintainScrollPositionOfItem
|
||||
}
|
||||
|
||||
public func request(maxID: String?, minID: String?) {
|
||||
if case .statuses = collection, maxID == nil {
|
||||
profileService.fetchPinnedStatuses()
|
||||
.assignErrorsToAlertItem(to: \.alertItem, on: self)
|
||||
|
@ -63,10 +92,18 @@ public class ProfileViewModel: StatusListViewModel {
|
|||
.store(in: &cancellables)
|
||||
}
|
||||
|
||||
super.request(maxID: maxID, minID: minID)
|
||||
collectionViewModel.value.request(maxID: maxID, minID: minID)
|
||||
}
|
||||
|
||||
public override var title: AnyPublisher<String?, Never> {
|
||||
$accountViewModel.map { $0?.accountName }.eraseToAnyPublisher()
|
||||
public func itemSelected(_ item: CollectionItem) {
|
||||
collectionViewModel.value.itemSelected(item)
|
||||
}
|
||||
|
||||
public func canSelect(item: CollectionItem) -> Bool {
|
||||
collectionViewModel.value.canSelect(item: item)
|
||||
}
|
||||
|
||||
public func viewModel(item: CollectionItem) -> Any? {
|
||||
collectionViewModel.value.viewModel(item: item)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,7 +5,7 @@ import Foundation
|
|||
import Mastodon
|
||||
import ServiceLayer
|
||||
|
||||
public class StatusListViewModel: ObservableObject {
|
||||
final public class StatusListViewModel: ObservableObject {
|
||||
@Published public private(set) var items = [[CollectionItem]]()
|
||||
@Published public var alertItem: AlertItem?
|
||||
public private(set) var nextPageMaxID: String?
|
||||
|
@ -40,13 +40,19 @@ public class StatusListViewModel: ObservableObject {
|
|||
.sink { [weak self] in self?.nextPageMaxID = $0 }
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
}
|
||||
|
||||
extension StatusListViewModel: CollectionViewModel {
|
||||
public var collectionItems: AnyPublisher<[[CollectionItem]], Never> { $items.eraseToAnyPublisher() }
|
||||
|
||||
public var navigationEvents: AnyPublisher<NavigationEvent, Never> { navigationEventsSubject.eraseToAnyPublisher() }
|
||||
|
||||
public var title: AnyPublisher<String?, Never> { Just(statusListService.title).eraseToAnyPublisher() }
|
||||
|
||||
public var alertItems: AnyPublisher<AlertItem, Never> { $alertItem.compactMap { $0 }.eraseToAnyPublisher() }
|
||||
|
||||
public var loading: AnyPublisher<Bool, Never> { loadingSubject.eraseToAnyPublisher() }
|
||||
|
||||
public var navigationEvents: AnyPublisher<NavigationEvent, Never> { navigationEventsSubject.eraseToAnyPublisher() }
|
||||
|
||||
public func request(maxID: String? = nil, minID: String? = nil) {
|
||||
statusListService.request(maxID: maxID, minID: minID)
|
||||
.receive(on: DispatchQueue.main)
|
||||
|
@ -57,12 +63,6 @@ public class StatusListViewModel: ObservableObject {
|
|||
.sink { _ in }
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
}
|
||||
|
||||
extension StatusListViewModel: CollectionViewModel {
|
||||
public var alertItems: AnyPublisher<AlertItem, Never> { $alertItem.compactMap { $0 }.eraseToAnyPublisher() }
|
||||
|
||||
public var loading: AnyPublisher<Bool, Never> { loadingSubject.eraseToAnyPublisher() }
|
||||
|
||||
public func itemSelected(_ item: CollectionItem) {
|
||||
switch item.kind {
|
||||
|
|
|
@ -81,7 +81,7 @@ private extension AccountHeaderView {
|
|||
segmentedControl.insertSegment(
|
||||
action: UIAction(title: collection.title) { [weak self] _ in
|
||||
self?.viewModel?.collection = collection
|
||||
self?.viewModel?.request()
|
||||
self?.viewModel?.request(maxID: nil, minID: nil)
|
||||
},
|
||||
at: index,
|
||||
animated: false)
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
// Copyright © 2020 Metabolist. All rights reserved.
|
||||
|
||||
import KingfisherSwiftUI
|
||||
import enum Mastodon.Timeline
|
||||
import SwiftUI
|
||||
import ViewModels
|
||||
|
||||
|
@ -138,6 +137,8 @@ private extension Timeline {
|
|||
return list.title
|
||||
case let .tag(tag):
|
||||
return "#" + tag
|
||||
case .profile:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -148,6 +149,7 @@ private extension Timeline {
|
|||
case .federated: return "globe"
|
||||
case .list: return "scroll"
|
||||
case .tag: return "number"
|
||||
case .profile: return "person"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue