mirror of
https://github.com/metabolist/metatext.git
synced 2024-11-25 17:50:59 +00:00
Use timeline logic for profiles
This commit is contained in:
parent
31156dd482
commit
e7c6ac3f98
18 changed files with 294 additions and 279 deletions
|
@ -70,21 +70,11 @@ extension AccountRecord {
|
||||||
StatusRecord.self,
|
StatusRecord.self,
|
||||||
through: pinnedStatusJoins,
|
through: pinnedStatusJoins,
|
||||||
using: AccountPinnedStatusJoin.status)
|
using: AccountPinnedStatusJoin.status)
|
||||||
static let statusJoins = hasMany(AccountStatusJoin.self)
|
|
||||||
|
|
||||||
var pinnedStatuses: QueryInterfaceRequest<StatusInfo> {
|
var pinnedStatuses: QueryInterfaceRequest<StatusInfo> {
|
||||||
StatusInfo.request(request(for: Self.pinnedStatuses))
|
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) {
|
init(account: Account) {
|
||||||
id = account.id
|
id = account.id
|
||||||
username = account.username
|
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("emojis", .blob).notNull()
|
||||||
t.column("bot", .boolean).notNull()
|
t.column("bot", .boolean).notNull()
|
||||||
t.column("discoverable", .boolean)
|
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
|
try db.create(table: "statusRecord") { t in
|
||||||
t.column("id", .text).primaryKey(onConflict: .replace)
|
t.column("id", .text).primaryKey(onConflict: .replace)
|
||||||
t.column("uri", .text).notNull()
|
t.column("uri", .text).notNull()
|
||||||
t.column("createdAt", .datetime).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("content", .text).notNull()
|
||||||
t.column("visibility", .text).notNull()
|
t.column("visibility", .text).notNull()
|
||||||
t.column("sensitive", .boolean).notNull()
|
t.column("sensitive", .boolean).notNull()
|
||||||
|
@ -50,7 +50,7 @@ extension ContentDatabase {
|
||||||
t.column("url", .text)
|
t.column("url", .text)
|
||||||
t.column("inReplyToId", .text)
|
t.column("inReplyToId", .text)
|
||||||
t.column("inReplyToAccountId", .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("poll", .blob)
|
||||||
t.column("card", .blob)
|
t.column("card", .blob)
|
||||||
t.column("language", .text)
|
t.column("language", .text)
|
||||||
|
@ -62,16 +62,20 @@ extension ContentDatabase {
|
||||||
t.column("pinned", .boolean)
|
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("id", .text).primaryKey(onConflict: .replace)
|
||||||
|
t.column("listId", .text)
|
||||||
t.column("listTitle", .text).indexed().collate(.localizedCaseInsensitiveCompare)
|
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
|
try db.create(table: "timelineStatusJoin") { t in
|
||||||
t.column("timelineId", .text).indexed().notNull()
|
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()
|
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)
|
t.primaryKey(["timelineId", "statusId"], onConflict: .replace)
|
||||||
}
|
}
|
||||||
|
@ -87,9 +91,9 @@ extension ContentDatabase {
|
||||||
|
|
||||||
try db.create(table: "statusContextJoin") { t in
|
try db.create(table: "statusContextJoin") { t in
|
||||||
t.column("parentId", .text).indexed().notNull()
|
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()
|
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("section", .text).indexed().notNull()
|
||||||
t.column("index", .integer).notNull()
|
t.column("index", .integer).notNull()
|
||||||
|
|
||||||
|
@ -98,33 +102,23 @@ extension ContentDatabase {
|
||||||
|
|
||||||
try db.create(table: "accountPinnedStatusJoin") { t in
|
try db.create(table: "accountPinnedStatusJoin") { t in
|
||||||
t.column("accountId", .text).indexed().notNull()
|
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()
|
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.column("index", .integer).notNull()
|
||||||
|
|
||||||
t.primaryKey(["accountId", "statusId"], onConflict: .replace)
|
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
|
try db.create(table: "accountList") { t in
|
||||||
t.column("id", .text).primaryKey(onConflict: .replace)
|
t.column("id", .text).primaryKey(onConflict: .replace)
|
||||||
}
|
}
|
||||||
|
|
||||||
try db.create(table: "accountListJoin") { t in
|
try db.create(table: "accountListJoin") { t in
|
||||||
t.column("accountId", .text).indexed().notNull()
|
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()
|
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.column("index", .integer).notNull()
|
||||||
|
|
||||||
t.primaryKey(["accountId", "listId"], onConflict: .replace)
|
t.primaryKey(["accountId", "listId"], onConflict: .replace)
|
||||||
|
|
|
@ -99,21 +99,6 @@ public extension ContentDatabase {
|
||||||
.eraseToAnyPublisher()
|
.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> {
|
func append(accounts: [Account], toList list: AccountList) -> AnyPublisher<Never, Error> {
|
||||||
databaseWriter.writePublisher {
|
databaseWriter.writePublisher {
|
||||||
try list.save($0)
|
try list.save($0)
|
||||||
|
@ -135,9 +120,9 @@ public extension ContentDatabase {
|
||||||
try Timeline.list(list).save($0)
|
try Timeline.list(list).save($0)
|
||||||
}
|
}
|
||||||
|
|
||||||
try Timeline
|
try TimelineRecord
|
||||||
.filter(!(Timeline.authenticatedDefaults.map(\.id) + lists.map(\.id)).contains(Timeline.Columns.id)
|
.filter(!lists.map(\.id).contains(TimelineRecord.Columns.listId)
|
||||||
&& Timeline.Columns.listTitle != nil)
|
&& TimelineRecord.Columns.listTitle != nil)
|
||||||
.deleteAll($0)
|
.deleteAll($0)
|
||||||
}
|
}
|
||||||
.ignoreOutput()
|
.ignoreOutput()
|
||||||
|
@ -151,7 +136,7 @@ public extension ContentDatabase {
|
||||||
}
|
}
|
||||||
|
|
||||||
func deleteList(id: String) -> AnyPublisher<Never, Error> {
|
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()
|
.ignoreOutput()
|
||||||
.eraseToAnyPublisher()
|
.eraseToAnyPublisher()
|
||||||
}
|
}
|
||||||
|
@ -181,11 +166,22 @@ public extension ContentDatabase {
|
||||||
}
|
}
|
||||||
|
|
||||||
func statusesObservation(timeline: Timeline) -> AnyPublisher<[[Status]], Error> {
|
func statusesObservation(timeline: Timeline) -> AnyPublisher<[[Status]], Error> {
|
||||||
ValueObservation.tracking(timeline.statuses.fetchAll)
|
ValueObservation.tracking { db -> [[StatusInfo]] in
|
||||||
.removeDuplicates()
|
let statuses = try TimelineRecord(timeline: timeline).statuses.fetchAll(db)
|
||||||
.publisher(in: databaseWriter)
|
|
||||||
.map { [$0.map(Status.init(info:))] }
|
if case let .profile(accountId, profileCollection) = timeline, profileCollection == .statuses {
|
||||||
.eraseToAnyPublisher()
|
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)
|
||||||
|
.eraseToAnyPublisher()
|
||||||
}
|
}
|
||||||
|
|
||||||
func contextObservation(parentID: String) -> AnyPublisher<[[Status]], Error> {
|
func contextObservation(parentID: String) -> AnyPublisher<[[Status]], Error> {
|
||||||
|
@ -201,40 +197,17 @@ public extension ContentDatabase {
|
||||||
return [ancestors, [parent], descendants]
|
return [ancestors, [parent], descendants]
|
||||||
}
|
}
|
||||||
.removeDuplicates()
|
.removeDuplicates()
|
||||||
.publisher(in: databaseWriter)
|
|
||||||
.map { $0.map { $0.map(Status.init(info:)) } }
|
.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)
|
.publisher(in: databaseWriter)
|
||||||
.map { $0.map { $0.map(Status.init(info:)) } }
|
|
||||||
.eraseToAnyPublisher()
|
.eraseToAnyPublisher()
|
||||||
}
|
}
|
||||||
|
|
||||||
func listsObservation() -> AnyPublisher<[Timeline], Error> {
|
func listsObservation() -> AnyPublisher<[Timeline], Error> {
|
||||||
ValueObservation.tracking(Timeline.filter(Timeline.Columns.listTitle != nil)
|
ValueObservation.tracking(TimelineRecord.filter(TimelineRecord.Columns.listId != nil)
|
||||||
.order(Timeline.Columns.listTitle.asc)
|
.order(TimelineRecord.Columns.listTitle.asc)
|
||||||
.fetchAll)
|
.fetchAll)
|
||||||
.removeDuplicates()
|
.removeDuplicates()
|
||||||
|
.map { $0.map(Timeline.init(record:)).compactMap { $0 } }
|
||||||
.publisher(in: databaseWriter)
|
.publisher(in: databaseWriter)
|
||||||
.eraseToAnyPublisher()
|
.eraseToAnyPublisher()
|
||||||
}
|
}
|
||||||
|
@ -243,12 +216,12 @@ public extension ContentDatabase {
|
||||||
ValueObservation.tracking(
|
ValueObservation.tracking(
|
||||||
Filter.filter(Filter.Columns.expiresAt == nil || Filter.Columns.expiresAt > date).fetchAll)
|
Filter.filter(Filter.Columns.expiresAt == nil || Filter.Columns.expiresAt > date).fetchAll)
|
||||||
.removeDuplicates()
|
.removeDuplicates()
|
||||||
.publisher(in: databaseWriter)
|
|
||||||
.map {
|
.map {
|
||||||
guard let context = context else { return $0 }
|
guard let context = context else { return $0 }
|
||||||
|
|
||||||
return $0.filter { $0.context.contains(context) }
|
return $0.filter { $0.context.contains(context) }
|
||||||
}
|
}
|
||||||
|
.publisher(in: databaseWriter)
|
||||||
.eraseToAnyPublisher()
|
.eraseToAnyPublisher()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -262,7 +235,6 @@ public extension ContentDatabase {
|
||||||
func accountObservation(id: String) -> AnyPublisher<Account?, Error> {
|
func accountObservation(id: String) -> AnyPublisher<Account?, Error> {
|
||||||
ValueObservation.tracking(AccountInfo.request(AccountRecord.filter(AccountRecord.Columns.id == id)).fetchOne)
|
ValueObservation.tracking(AccountInfo.request(AccountRecord.filter(AccountRecord.Columns.id == id)).fetchOne)
|
||||||
.removeDuplicates()
|
.removeDuplicates()
|
||||||
.publisher(in: databaseWriter)
|
|
||||||
.map {
|
.map {
|
||||||
if let info = $0 {
|
if let info = $0 {
|
||||||
return Account(info: info)
|
return Account(info: info)
|
||||||
|
@ -270,14 +242,15 @@ public extension ContentDatabase {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.publisher(in: databaseWriter)
|
||||||
.eraseToAnyPublisher()
|
.eraseToAnyPublisher()
|
||||||
}
|
}
|
||||||
|
|
||||||
func accountListObservation(_ list: AccountList) -> AnyPublisher<[Account], Error> {
|
func accountListObservation(_ list: AccountList) -> AnyPublisher<[Account], Error> {
|
||||||
ValueObservation.tracking(list.accounts.fetchAll)
|
ValueObservation.tracking(list.accounts.fetchAll)
|
||||||
.removeDuplicates()
|
.removeDuplicates()
|
||||||
.publisher(in: databaseWriter)
|
|
||||||
.map { $0.map(Account.init(info:)) }
|
.map { $0.map(Account.init(info:)) }
|
||||||
|
.publisher(in: databaseWriter)
|
||||||
.eraseToAnyPublisher()
|
.eraseToAnyPublisher()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -289,7 +262,7 @@ private extension ContentDatabase {
|
||||||
|
|
||||||
func clean() throws {
|
func clean() throws {
|
||||||
try databaseWriter.write {
|
try databaseWriter.write {
|
||||||
try Timeline.deleteAll($0)
|
try TimelineRecord.deleteAll($0)
|
||||||
try StatusRecord.deleteAll($0)
|
try StatusRecord.deleteAll($0)
|
||||||
try AccountRecord.deleteAll($0)
|
try AccountRecord.deleteAll($0)
|
||||||
try AccountList.deleteAll($0)
|
try AccountList.deleteAll($0)
|
||||||
|
|
77
DB/Sources/DB/Content/TimelineRecord.swift
Normal file
77
DB/Sources/DB/Content/TimelineRecord.swift
Normal file
|
@ -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.
|
// Copyright © 2020 Metabolist. All rights reserved.
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
import Mastodon
|
||||||
|
|
||||||
public enum Timeline: Hashable {
|
public enum Timeline: Hashable {
|
||||||
case home
|
case home
|
||||||
|
@ -8,6 +9,7 @@ public enum Timeline: Hashable {
|
||||||
case federated
|
case federated
|
||||||
case list(List)
|
case list(List)
|
||||||
case tag(String)
|
case tag(String)
|
||||||
|
case profile(accountId: String, profileCollection: ProfileCollection)
|
||||||
}
|
}
|
||||||
|
|
||||||
public extension Timeline {
|
public extension Timeline {
|
||||||
|
@ -25,9 +27,11 @@ extension Timeline: Identifiable {
|
||||||
case .federated:
|
case .federated:
|
||||||
return "federated"
|
return "federated"
|
||||||
case let .list(list):
|
case let .list(list):
|
||||||
return list.id
|
return "list-".appending(list.id)
|
||||||
case let .tag(tag):
|
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 GRDB
|
||||||
import Mastodon
|
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 {
|
extension Timeline {
|
||||||
static let statusJoins = hasMany(TimelineStatusJoin.self)
|
func save(_ db: Database) throws {
|
||||||
static let statuses = hasMany(
|
try TimelineRecord(timeline: self).save(db)
|
||||||
StatusRecord.self,
|
}
|
||||||
through: statusJoins,
|
|
||||||
using: TimelineStatusJoin.status)
|
|
||||||
.order(StatusRecord.Columns.createdAt.desc)
|
|
||||||
|
|
||||||
var statuses: QueryInterfaceRequest<StatusInfo> {
|
init?(record: TimelineRecord) {
|
||||||
StatusInfo.request(request(for: Self.statuses))
|
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
|
import MastodonAPI
|
||||||
|
|
||||||
public struct ProfileService {
|
public struct ProfileService {
|
||||||
public let accountService: AnyPublisher<AccountService, Error>
|
public let accountServicePublisher: AnyPublisher<AccountService, Error>
|
||||||
|
|
||||||
private let accountID: String
|
private let accountID: String
|
||||||
private let mastodonAPIClient: MastodonAPIClient
|
private let mastodonAPIClient: MastodonAPIClient
|
||||||
|
@ -45,18 +45,16 @@ public struct ProfileService {
|
||||||
.eraseToAnyPublisher()
|
.eraseToAnyPublisher()
|
||||||
}
|
}
|
||||||
|
|
||||||
accountService = accountPublisher
|
accountServicePublisher = accountPublisher
|
||||||
.map { AccountService(account: $0, mastodonAPIClient: mastodonAPIClient, contentDatabase: contentDatabase) }
|
.map { AccountService(account: $0, mastodonAPIClient: mastodonAPIClient, contentDatabase: contentDatabase) }
|
||||||
.eraseToAnyPublisher()
|
.eraseToAnyPublisher()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public extension ProfileService {
|
public extension ProfileService {
|
||||||
func statusListService(
|
func statusListService(profileCollection: ProfileCollection) -> StatusListService {
|
||||||
collectionPublisher: CurrentValueSubject<ProfileCollection, Never>) -> StatusListService {
|
|
||||||
StatusListService(
|
StatusListService(
|
||||||
accountID: accountID,
|
timeline: .profile(accountId: accountID, profileCollection: profileCollection),
|
||||||
collection: collectionPublisher,
|
|
||||||
mastodonAPIClient: mastodonAPIClient,
|
mastodonAPIClient: mastodonAPIClient,
|
||||||
contentDatabase: contentDatabase)
|
contentDatabase: contentDatabase)
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,15 +21,6 @@ public struct StatusListService {
|
||||||
|
|
||||||
extension StatusListService {
|
extension StatusListService {
|
||||||
init(timeline: Timeline, mastodonAPIClient: MastodonAPIClient, contentDatabase: ContentDatabase) {
|
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?
|
var title: String?
|
||||||
|
|
||||||
if case let .tag(tag) = timeline {
|
if case let .tag(tag) = timeline {
|
||||||
|
@ -46,7 +37,7 @@ extension StatusListService {
|
||||||
status: nil,
|
status: nil,
|
||||||
mastodonAPIClient: mastodonAPIClient,
|
mastodonAPIClient: mastodonAPIClient,
|
||||||
contentDatabase: contentDatabase),
|
contentDatabase: contentDatabase),
|
||||||
filterContext: filterContext,
|
filterContext: timeline.filterContext,
|
||||||
mastodonAPIClient: mastodonAPIClient,
|
mastodonAPIClient: mastodonAPIClient,
|
||||||
contentDatabase: contentDatabase) { maxID, minID in
|
contentDatabase: contentDatabase) { maxID, minID in
|
||||||
mastodonAPIClient.pagedRequest(timeline.endpoint, maxID: maxID, minID: minID)
|
mastodonAPIClient.pagedRequest(timeline.endpoint, maxID: maxID, minID: minID)
|
||||||
|
@ -78,59 +69,6 @@ extension StatusListService {
|
||||||
.eraseToAnyPublisher()
|
.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 {
|
public extension StatusListService {
|
||||||
|
@ -142,3 +80,52 @@ public extension StatusListService {
|
||||||
contentDatabase.activeFiltersObservation(date: Date(), context: filterContext)
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
5
ViewModels/Sources/ViewModels/Entities/Timeline.swift
Normal file
5
ViewModels/Sources/ViewModels/Entities/Timeline.swift
Normal file
|
@ -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 {
|
switch timeline {
|
||||||
case .home, .list:
|
case .home, .list:
|
||||||
return identification.identity.handle
|
return identification.identity.handle
|
||||||
case .local, .federated, .tag:
|
case .local, .federated, .tag, .profile:
|
||||||
return identification.identity.instance?.uri ?? ""
|
return identification.identity.instance?.uri ?? ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,57 +5,86 @@ import Foundation
|
||||||
import Mastodon
|
import Mastodon
|
||||||
import ServiceLayer
|
import ServiceLayer
|
||||||
|
|
||||||
public class ProfileViewModel: StatusListViewModel {
|
final public class ProfileViewModel {
|
||||||
@Published public private(set) var accountViewModel: AccountViewModel?
|
@Published public private(set) var accountViewModel: AccountViewModel?
|
||||||
@Published public var collection = ProfileCollection.statuses
|
@Published public var collection = ProfileCollection.statuses
|
||||||
|
@Published public var alertItem: AlertItem?
|
||||||
|
|
||||||
private let profileService: ProfileService
|
private let profileService: ProfileService
|
||||||
|
private let collectionViewModel: CurrentValueSubject<StatusListViewModel, Never>
|
||||||
private var cancellables = Set<AnyCancellable>()
|
private var cancellables = Set<AnyCancellable>()
|
||||||
|
|
||||||
init(profileService: ProfileService) {
|
init(profileService: ProfileService) {
|
||||||
self.profileService = profileService
|
self.profileService = profileService
|
||||||
|
|
||||||
let collectionSubject = CurrentValueSubject<ProfileCollection, Never>(.statuses)
|
collectionViewModel = CurrentValueSubject(
|
||||||
|
StatusListViewModel(statusListService: profileService.statusListService(profileCollection: .statuses)))
|
||||||
|
|
||||||
super.init(
|
profileService.accountServicePublisher
|
||||||
statusListService: profileService.statusListService(
|
|
||||||
collectionPublisher: collectionSubject))
|
|
||||||
|
|
||||||
$collection.sink(receiveValue: collectionSubject.send).store(in: &cancellables)
|
|
||||||
|
|
||||||
profileService.accountService
|
|
||||||
.map(AccountViewModel.init(accountService:))
|
.map(AccountViewModel.init(accountService:))
|
||||||
.assignErrorsToAlertItem(to: \.alertItem, on: self)
|
.assignErrorsToAlertItem(to: \.alertItem, on: self)
|
||||||
.assign(to: &$accountViewModel)
|
.assign(to: &$accountViewModel)
|
||||||
}
|
|
||||||
|
|
||||||
public override var collectionItems: AnyPublisher<[[CollectionItem]], Never> {
|
$collection.dropFirst()
|
||||||
// The pinned key is added to the info of collection items in the first section
|
.map(profileService.statusListService(profileCollection:))
|
||||||
// so a diffable data source can potentially render it in both sections
|
.map(StatusListViewModel.init(statusListService:))
|
||||||
super.collectionItems
|
.sink { [weak self] in
|
||||||
.map {
|
guard let self = self else { return }
|
||||||
$0.enumerated().map { [weak self] in
|
|
||||||
if let self = self, self.collection == .statuses, $0 == 0 {
|
self.collectionViewModel.send($0)
|
||||||
return $1.map { .init(id: $0.id, kind: $0.kind, info: [.pinned: true]) }
|
$0.$alertItem.assign(to: &self.$alertItem)
|
||||||
} else {
|
}
|
||||||
return $1
|
.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 }
|
$accountViewModel.compactMap { $0 }
|
||||||
.flatMap(\.events)
|
.flatMap(\.events)
|
||||||
.flatMap { $0 }
|
.flatMap { $0 }
|
||||||
.map(NavigationEvent.init)
|
.map(NavigationEvent.init)
|
||||||
.compactMap { $0 }
|
.compactMap { $0 }
|
||||||
.assignErrorsToAlertItem(to: \.alertItem, on: self)
|
.assignErrorsToAlertItem(to: \.alertItem, on: self)
|
||||||
.merge(with: super.navigationEvents)
|
.merge(with: collectionViewModel.flatMap(\.navigationEvents))
|
||||||
.eraseToAnyPublisher()
|
.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 {
|
if case .statuses = collection, maxID == nil {
|
||||||
profileService.fetchPinnedStatuses()
|
profileService.fetchPinnedStatuses()
|
||||||
.assignErrorsToAlertItem(to: \.alertItem, on: self)
|
.assignErrorsToAlertItem(to: \.alertItem, on: self)
|
||||||
|
@ -63,10 +92,18 @@ public class ProfileViewModel: StatusListViewModel {
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
}
|
}
|
||||||
|
|
||||||
super.request(maxID: maxID, minID: minID)
|
collectionViewModel.value.request(maxID: maxID, minID: minID)
|
||||||
}
|
}
|
||||||
|
|
||||||
public override var title: AnyPublisher<String?, Never> {
|
public func itemSelected(_ item: CollectionItem) {
|
||||||
$accountViewModel.map { $0?.accountName }.eraseToAnyPublisher()
|
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 Mastodon
|
||||||
import ServiceLayer
|
import ServiceLayer
|
||||||
|
|
||||||
public class StatusListViewModel: ObservableObject {
|
final public class StatusListViewModel: ObservableObject {
|
||||||
@Published public private(set) var items = [[CollectionItem]]()
|
@Published public private(set) var items = [[CollectionItem]]()
|
||||||
@Published public var alertItem: AlertItem?
|
@Published public var alertItem: AlertItem?
|
||||||
public private(set) var nextPageMaxID: String?
|
public private(set) var nextPageMaxID: String?
|
||||||
|
@ -40,13 +40,19 @@ public class StatusListViewModel: ObservableObject {
|
||||||
.sink { [weak self] in self?.nextPageMaxID = $0 }
|
.sink { [weak self] in self?.nextPageMaxID = $0 }
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension StatusListViewModel: CollectionViewModel {
|
||||||
public var collectionItems: AnyPublisher<[[CollectionItem]], Never> { $items.eraseToAnyPublisher() }
|
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 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) {
|
public func request(maxID: String? = nil, minID: String? = nil) {
|
||||||
statusListService.request(maxID: maxID, minID: minID)
|
statusListService.request(maxID: maxID, minID: minID)
|
||||||
.receive(on: DispatchQueue.main)
|
.receive(on: DispatchQueue.main)
|
||||||
|
@ -57,12 +63,6 @@ public class StatusListViewModel: ObservableObject {
|
||||||
.sink { _ in }
|
.sink { _ in }
|
||||||
.store(in: &cancellables)
|
.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) {
|
public func itemSelected(_ item: CollectionItem) {
|
||||||
switch item.kind {
|
switch item.kind {
|
||||||
|
|
|
@ -81,7 +81,7 @@ private extension AccountHeaderView {
|
||||||
segmentedControl.insertSegment(
|
segmentedControl.insertSegment(
|
||||||
action: UIAction(title: collection.title) { [weak self] _ in
|
action: UIAction(title: collection.title) { [weak self] _ in
|
||||||
self?.viewModel?.collection = collection
|
self?.viewModel?.collection = collection
|
||||||
self?.viewModel?.request()
|
self?.viewModel?.request(maxID: nil, minID: nil)
|
||||||
},
|
},
|
||||||
at: index,
|
at: index,
|
||||||
animated: false)
|
animated: false)
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
// Copyright © 2020 Metabolist. All rights reserved.
|
// Copyright © 2020 Metabolist. All rights reserved.
|
||||||
|
|
||||||
import KingfisherSwiftUI
|
import KingfisherSwiftUI
|
||||||
import enum Mastodon.Timeline
|
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
import ViewModels
|
import ViewModels
|
||||||
|
|
||||||
|
@ -138,6 +137,8 @@ private extension Timeline {
|
||||||
return list.title
|
return list.title
|
||||||
case let .tag(tag):
|
case let .tag(tag):
|
||||||
return "#" + tag
|
return "#" + tag
|
||||||
|
case .profile:
|
||||||
|
return ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -148,6 +149,7 @@ private extension Timeline {
|
||||||
case .federated: return "globe"
|
case .federated: return "globe"
|
||||||
case .list: return "scroll"
|
case .list: return "scroll"
|
||||||
case .tag: return "number"
|
case .tag: return "number"
|
||||||
|
case .profile: return "person"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue