Begin replacing CoreData for Feed, MastodonUser, Notification, Status

This commit is contained in:
Marcus Kida 2023-10-04 13:23:08 +02:00
parent c80a590306
commit ee51520bff
No known key found for this signature in database
GPG Key ID: 19FF64E08013CA40
21 changed files with 272 additions and 122 deletions

View File

@ -5,12 +5,11 @@
// Created by sxiaojian on 2021/4/13. // Created by sxiaojian on 2021/4/13.
// //
import CoreData
import Foundation import Foundation
import CoreDataStack import MastodonSDK
enum NotificationItem: Hashable { enum NotificationItem: Hashable {
case feed(record: ManagedObjectRecord<Feed>) case feed(record: FeedNxt)
case feedLoader(record: ManagedObjectRecord<Feed>) case feedLoader(record: FeedNxt)
case bottomLoader case bottomLoader
} }

View File

@ -41,10 +41,9 @@ extension NotificationSection {
return UITableViewDiffableDataSource(tableView: tableView) { tableView, indexPath, item -> UITableViewCell? in return UITableViewDiffableDataSource(tableView: tableView) { tableView, indexPath, item -> UITableViewCell? in
switch item { switch item {
case .feed(let record): case .feed(let feed):
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: NotificationTableViewCell.self), for: indexPath) as! NotificationTableViewCell let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: NotificationTableViewCell.self), for: indexPath) as! NotificationTableViewCell
context.managedObjectContext.performAndWait { context.managedObjectContext.performAndWait {
guard let feed = record.object(in: context.managedObjectContext) else { return }
configure( configure(
context: context, context: context,
tableView: tableView, tableView: tableView,

View File

@ -8,6 +8,7 @@
import Foundation import Foundation
import CoreDataStack import CoreDataStack
import MastodonUI import MastodonUI
import MastodonSDK
enum StatusItem: Hashable { enum StatusItem: Hashable {
case feed(record: ManagedObjectRecord<Feed>) case feed(record: ManagedObjectRecord<Feed>)
@ -24,7 +25,7 @@ extension StatusItem {
case reply(context: Context) case reply(context: Context)
case leaf(context: Context) case leaf(context: Context)
public var record: ManagedObjectRecord<Status> { public var record: StatusNxt {
switch self { switch self {
case .root(let threadContext), case .root(let threadContext),
.reply(let threadContext), .reply(let threadContext),
@ -37,12 +38,12 @@ extension StatusItem {
extension StatusItem.Thread { extension StatusItem.Thread {
class Context: Hashable { class Context: Hashable {
let status: ManagedObjectRecord<Status> let status: StatusNxt
var displayUpperConversationLink: Bool var displayUpperConversationLink: Bool
var displayBottomConversationLink: Bool var displayBottomConversationLink: Bool
init( init(
status: ManagedObjectRecord<Status>, status: StatusNxt,
displayUpperConversationLink: Bool = false, displayUpperConversationLink: Bool = false,
displayBottomConversationLink: Bool = false displayBottomConversationLink: Bool = false
) { ) {

View File

@ -101,7 +101,7 @@ extension DiscoveryForYouViewController: UITableViewDelegate {
let profileViewModel = CachedProfileViewModel( let profileViewModel = CachedProfileViewModel(
context: context, context: context,
authContext: viewModel.authContext, authContext: viewModel.authContext,
mastodonUser: user mastodonUser: .from(user: user)
) )
_ = coordinator.present( _ = coordinator.present(
scene: .profile(viewModel: profileViewModel), scene: .profile(viewModel: profileViewModel),

View File

@ -8,6 +8,7 @@
import UIKit import UIKit
import Combine import Combine
import CoreDataStack import CoreDataStack
import MastodonSDK
extension NotificationTableViewCell { extension NotificationTableViewCell {
final class ViewModel { final class ViewModel {
@ -18,7 +19,7 @@ extension NotificationTableViewCell {
} }
enum Value { enum Value {
case feed(Feed) case feed(FeedNxt)
} }
} }
} }

View File

@ -23,7 +23,6 @@ extension NotificationTimelineViewController: DataSourceProvider {
case .feed(let record): case .feed(let record):
let managedObjectContext = context.managedObjectContext let managedObjectContext = context.managedObjectContext
let item: DataSourceItem? = try? await managedObjectContext.perform { let item: DataSourceItem? = try? await managedObjectContext.perform {
guard let feed = record.object(in: managedObjectContext) else { return nil }
guard feed.kind == .notificationAll || feed.kind == .notificationMentions else { return nil } guard feed.kind == .notificationAll || feed.kind == .notificationMentions else { return nil }
if let notification = feed.notification { if let notification = feed.notification {
return .notification(record: .init(objectID: notification.objectID)) return .notification(record: .init(objectID: notification.objectID))

View File

@ -279,15 +279,15 @@ extension NotificationTimelineViewController: TableViewControllerNavigateable {
Task { @MainActor in Task { @MainActor in
switch item { switch item {
case .feed(let record): case .feed(let feed):
guard let feed = record.object(in: self.context.managedObjectContext) else { return } // guard let feed = record.object(in: self.context.managedObjectContext) else { return }
guard let notification = feed.notification else { return } guard let notification = feed.notification else { return }
if let stauts = notification.status { if let status = notification.status {
let threadViewModel = ThreadViewModel( let threadViewModel = ThreadViewModel(
context: self.context, context: self.context,
authContext: self.viewModel.authContext, authContext: self.viewModel.authContext,
optionalRoot: .root(context: .init(status: .init(objectID: stauts.objectID))) optionalRoot: .root(context: .init(status: status))
) )
_ = self.coordinator.present( _ = self.coordinator.present(
scene: .thread(viewModel: threadViewModel), scene: .thread(viewModel: threadViewModel),

View File

@ -30,7 +30,7 @@ extension NotificationTimelineViewModel {
snapshot.appendSections([.main]) snapshot.appendSections([.main])
diffableDataSource?.apply(snapshot) diffableDataSource?.apply(snapshot)
feedFetchedResultsController.$records $notifications
.receive(on: DispatchQueue.main) .receive(on: DispatchQueue.main)
.sink { [weak self] records in .sink { [weak self] records in
guard let self = self else { return } guard let self = self else { return }
@ -48,42 +48,42 @@ extension NotificationTimelineViewModel {
return snapshot return snapshot
}() }()
let parentManagedObjectContext = self.context.managedObjectContext // let parentManagedObjectContext = self.context.managedObjectContext
let managedObjectContext = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType) // let managedObjectContext = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType)
managedObjectContext.parent = parentManagedObjectContext // managedObjectContext.parent = parentManagedObjectContext
try? await managedObjectContext.perform { // try? await managedObjectContext.perform {
let anchors: [Feed] = { // let anchors: [Feed] = {
let request = Feed.sortedFetchRequest // let request = Feed.sortedFetchRequest
request.predicate = NSCompoundPredicate(andPredicateWithSubpredicates: [ // request.predicate = NSCompoundPredicate(andPredicateWithSubpredicates: [
Feed.hasMorePredicate(), // Feed.hasMorePredicate(),
self.feedFetchedResultsController.predicate, // self.feedFetchedResultsController.predicate,
]) // ])
do { // do {
return try managedObjectContext.fetch(request) // return try managedObjectContext.fetch(request)
} catch { // } catch {
assertionFailure(error.localizedDescription) // assertionFailure(error.localizedDescription)
return [] // return []
} // }
}() // }()
//
let itemIdentifiers = newSnapshot.itemIdentifiers // let itemIdentifiers = newSnapshot.itemIdentifiers
for (index, item) in itemIdentifiers.enumerated() { // for (index, item) in itemIdentifiers.enumerated() {
guard case let .feed(record) = item else { continue } // guard case let .feed(record) = item else { continue }
guard anchors.contains(where: { feed in feed.objectID == record.objectID }) else { continue } // guard anchors.contains(where: { feed in feed.objectID == record.objectID }) else { continue }
let isLast = index + 1 == itemIdentifiers.count // let isLast = index + 1 == itemIdentifiers.count
if isLast { // if isLast {
newSnapshot.insertItems([.bottomLoader], afterItem: item) // newSnapshot.insertItems([.bottomLoader], afterItem: item)
} else { // } else {
newSnapshot.insertItems([.feedLoader(record: record)], afterItem: item) // newSnapshot.insertItems([.feedLoader(record: record)], afterItem: item)
} // }
} // }
} // }
//
let hasChanges = newSnapshot.itemIdentifiers != oldSnapshot.itemIdentifiers // let hasChanges = newSnapshot.itemIdentifiers != oldSnapshot.itemIdentifiers
if !hasChanges { // if !hasChanges {
self.didLoadLatest.send() // self.didLoadLatest.send()
return // return
} // }
await self.updateSnapshotUsingReloadData(snapshot: newSnapshot) await self.updateSnapshotUsingReloadData(snapshot: newSnapshot)
self.didLoadLatest.send() self.didLoadLatest.send()

View File

@ -20,11 +20,11 @@ final class NotificationTimelineViewModel {
let context: AppContext let context: AppContext
let authContext: AuthContext let authContext: AuthContext
let scope: Scope let scope: Scope
let feedFetchedResultsController: FeedFetchedResultsController // let feedFetchedResultsController: FeedFetchedResultsController
let listBatchFetchViewModel = ListBatchFetchViewModel() let listBatchFetchViewModel = ListBatchFetchViewModel()
@Published var isLoadingLatest = false @Published var isLoadingLatest = false
@Published var lastAutomaticFetchTimestamp: Date? @Published var lastAutomaticFetchTimestamp: Date?
@Published var notifications = [FeedNxt]()
// output // output
var diffableDataSource: UITableViewDiffableDataSource<NotificationSection, NotificationItem>? var diffableDataSource: UITableViewDiffableDataSource<NotificationSection, NotificationItem>?
var didLoadLatest = PassthroughSubject<Void, Never>() var didLoadLatest = PassthroughSubject<Void, Never>()
@ -51,57 +51,57 @@ final class NotificationTimelineViewModel {
self.context = context self.context = context
self.authContext = authContext self.authContext = authContext
self.scope = scope self.scope = scope
self.feedFetchedResultsController = FeedFetchedResultsController(managedObjectContext: context.managedObjectContext) // self.feedFetchedResultsController = FeedFetchedResultsController(managedObjectContext: context.managedObjectContext)
// end init // end init
feedFetchedResultsController.predicate = NotificationTimelineViewModel.feedPredicate( // feedFetchedResultsController.predicate = NotificationTimelineViewModel.feedPredicate(
authenticationBox: authContext.mastodonAuthenticationBox, // authenticationBox: authContext.mastodonAuthenticationBox,
scope: scope // scope: scope
) // )
} }
} }
extension NotificationTimelineViewModel { extension NotificationTimelineViewModel {
//
typealias Scope = APIService.MastodonNotificationScope typealias Scope = APIService.MastodonNotificationScope
//
static func feedPredicate( // static func feedPredicate(
authenticationBox: MastodonAuthenticationBox, // authenticationBox: MastodonAuthenticationBox,
scope: Scope // scope: Scope
) -> NSPredicate { // ) -> NSPredicate {
let domain = authenticationBox.domain // let domain = authenticationBox.domain
let userID = authenticationBox.userID // let userID = authenticationBox.userID
let acct = Feed.Acct.mastodon( // let acct = Feed.Acct.mastodon(
domain: domain, // domain: domain,
userID: userID // userID: userID
) // )
//
let predicate: NSPredicate = { // let predicate: NSPredicate = {
switch scope { // switch scope {
case .everything: // case .everything:
return NSCompoundPredicate(andPredicateWithSubpredicates: [ // return NSCompoundPredicate(andPredicateWithSubpredicates: [
Feed.hasNotificationPredicate(), // Feed.hasNotificationPredicate(),
Feed.predicate( // Feed.predicate(
kind: .notificationAll, // kind: .notificationAll,
acct: acct // acct: acct
) // )
]) // ])
case .mentions: // case .mentions:
return NSCompoundPredicate(andPredicateWithSubpredicates: [ // return NSCompoundPredicate(andPredicateWithSubpredicates: [
Feed.hasNotificationPredicate(), // Feed.hasNotificationPredicate(),
Feed.predicate( // Feed.predicate(
kind: .notificationMentions, // kind: .notificationMentions,
acct: acct // acct: acct
), // ),
Feed.notificationTypePredicate(types: scope.includeTypes ?? []) // Feed.notificationTypePredicate(types: scope.includeTypes ?? [])
]) // ])
} // }
}() // }()
return predicate // return predicate
} // }
//
} }
extension NotificationTimelineViewModel { extension NotificationTimelineViewModel {
@ -112,11 +112,15 @@ extension NotificationTimelineViewModel {
defer { isLoadingLatest = false } defer { isLoadingLatest = false }
do { do {
_ = try await context.apiService.notifications( let result = try await context.apiService.notifications(
maxID: nil, maxID: nil,
scope: scope, scope: scope,
authenticationBox: authContext.mastodonAuthenticationBox authenticationBox: authContext.mastodonAuthenticationBox
) )
notifications = result.value.map {
FeedNxt.from(notification: $0, as: .notificationAll)
}
} catch { } catch {
didLoadLatest.send() didLoadLatest.send()
} }
@ -127,18 +131,18 @@ extension NotificationTimelineViewModel {
guard case let .feedLoader(record) = item else { return } guard case let .feedLoader(record) = item else { return }
let managedObjectContext = context.managedObjectContext let managedObjectContext = context.managedObjectContext
let key = "LoadMore@\(record.objectID)" // let key = "LoadMore@\(record.objectID)"
// return when already loading state // return when already loading state
guard managedObjectContext.cache(froKey: key) == nil else { return } // guard managedObjectContext.cache(froKey: key) == nil else { return }
guard let feed = record.object(in: managedObjectContext) else { return } // guard let feed = record.object(in: managedObjectContext) else { return }
guard let maxID = feed.notification?.id else { return } guard let maxID = record.notification?.id else { return }
// keep transient property live // keep transient property live
managedObjectContext.cache(feed, key: key) // managedObjectContext.cache(feed, key: key)
defer { // defer {
managedObjectContext.cache(nil, key: key) // managedObjectContext.cache(nil, key: key)
} // }
// fetch data // fetch data
do { do {

View File

@ -8,10 +8,11 @@
import Foundation import Foundation
import CoreDataStack import CoreDataStack
import MastodonCore import MastodonCore
import MastodonSDK
final class CachedProfileViewModel: ProfileViewModel { final class CachedProfileViewModel: ProfileViewModel {
init(context: AppContext, authContext: AuthContext, mastodonUser: MastodonUser) { init(context: AppContext, authContext: AuthContext, mastodonUser: MastodonUserNxt) {
super.init(context: context, authContext: authContext, optionalMastodonUser: mastodonUser) super.init(context: context, authContext: authContext, optionalMastodonUser: mastodonUser)
} }
} }

View File

@ -33,8 +33,8 @@ class ProfileViewModel: NSObject {
// input // input
let context: AppContext let context: AppContext
let authContext: AuthContext let authContext: AuthContext
@Published var me: MastodonUser? @Published var me: MastodonUserNxt?
@Published var user: MastodonUser? @Published var user: MastodonUserNxt?
let viewDidAppear = PassthroughSubject<Void, Never>() let viewDidAppear = PassthroughSubject<Void, Never>()
@ -56,7 +56,7 @@ class ProfileViewModel: NSObject {
// @Published var protected: Bool? = nil // @Published var protected: Bool? = nil
// let needsPagePinToTop = CurrentValueSubject<Bool, Never>(false) // let needsPagePinToTop = CurrentValueSubject<Bool, Never>(false)
init(context: AppContext, authContext: AuthContext, optionalMastodonUser mastodonUser: MastodonUser?) { init(context: AppContext, authContext: AuthContext, optionalMastodonUser mastodonUser: MastodonUserNxt?) {
self.context = context self.context = context
self.authContext = authContext self.authContext = authContext
self.user = mastodonUser self.user = mastodonUser

View File

@ -16,9 +16,10 @@ import MastodonAsset
import MastodonCore import MastodonCore
import MastodonLocalization import MastodonLocalization
import class CoreDataStack.Notification import class CoreDataStack.Notification
import MastodonSDK
extension NotificationView { extension NotificationView {
public func configure(feed: Feed) { public func configure(feed: FeedNxt) {
guard let notification = feed.notification else { guard let notification = feed.notification else {
assertionFailure() assertionFailure()
return return
@ -29,7 +30,7 @@ extension NotificationView {
} }
extension NotificationView { extension NotificationView {
public func configure(notification: Notification) { public func configure(notification: NotificationNxt) {
viewModel.objects.insert(notification) viewModel.objects.insert(notification)
configureAuthor(notification: notification) configureAuthor(notification: notification)
@ -63,7 +64,7 @@ extension NotificationView {
} }
extension NotificationView { extension NotificationView {
private func configureAuthor(notification: Notification) { private func configureAuthor(notification: NotificationNxt) {
let author = notification.account let author = notification.account
// author avatar // author avatar

View File

@ -11,7 +11,7 @@ import MastodonCore
final class CachedThreadViewModel: ThreadViewModel { final class CachedThreadViewModel: ThreadViewModel {
init(context: AppContext, authContext: AuthContext, status: Status) { init(context: AppContext, authContext: AuthContext, status: Status) {
let threadContext = StatusItem.Thread.Context(status: .init(objectID: status.objectID)) let threadContext = StatusItem.Thread.Context(status: .from(status: status))
super.init( super.init(
context: context, context: context,
authContext: authContext, authContext: authContext,

View File

@ -0,0 +1,30 @@
// Copyright © 2023 Mastodon gGmbH. All rights reserved.
import Foundation
public class FeedNxt: Equatable {
public enum Kind: String, CaseIterable, Hashable {
case none
case home
case notificationAll
case notificationMentions
}
public let kind: Kind
public let notification: NotificationNxt?
init(kind: Kind, notification: NotificationNxt?) {
self.kind = kind
self.notification = notification
}
public static func == (lhs: FeedNxt, rhs: FeedNxt) -> Bool {
lhs.kind == rhs.kind && lhs.notification == rhs.notification
}
}
public extension FeedNxt {
static func from(notification: Mastodon.Entity.Notification, as kind: Kind) -> FeedNxt {
FeedNxt(kind: kind, notification: .from(notification: notification))
}
}

View File

@ -0,0 +1,32 @@
// Copyright © 2023 Mastodon gGmbH. All rights reserved.
import Foundation
import CoreDataStack
public class MastodonUserNxt: ObservableObject, Hashable {
public let id: String
init(id: String) {
self.id = id
}
public static func == (lhs: MastodonUserNxt, rhs: MastodonUserNxt) -> Bool {
lhs.id == rhs.id
}
public func hash(into hasher: inout Hasher) {
hasher.combine(id)
}
}
public extension MastodonUserNxt {
static func from(account: Mastodon.Entity.Account) -> MastodonUserNxt {
MastodonUserNxt(id: account.id)
}
}
public extension MastodonUserNxt {
static func from(user: MastodonUser) -> MastodonUserNxt {
MastodonUserNxt(id: user.id)
}
}

View File

@ -0,0 +1,36 @@
// Copyright © 2023 Mastodon gGmbH. All rights reserved.
import Foundation
public class NotificationNxt: Hashable {
public let id: String
public let status: StatusNxt?
public let account: MastodonUserNxt
public let typeRaw: String
init(id: String, status: StatusNxt?, account: MastodonUserNxt, typeRaw: String) {
self.id = id
self.status = status
self.account = account
self.typeRaw = typeRaw
}
public static func == (lhs: NotificationNxt, rhs: NotificationNxt) -> Bool {
lhs.id == rhs.id
}
public func hash(into hasher: inout Hasher) {
hasher.combine(id)
}
}
public extension NotificationNxt {
static func from(notification: Mastodon.Entity.Notification) -> NotificationNxt {
NotificationNxt(
id: notification.id,
status: notification.status != nil ? StatusNxt.from(status: notification.status!) : nil,
account: MastodonUserNxt.from(account: notification.account),
typeRaw: notification.type.rawValue
)
}
}

View File

@ -0,0 +1,47 @@
// Copyright © 2023 Mastodon gGmbH. All rights reserved.
import Foundation
import CoreDataStack
public class StatusNxt: Hashable {
public let id: String
public let reblog: StatusNxt?
public let author: MastodonUserNxt
public let createdAt: Date
init(id: String, reblog: StatusNxt?, author: MastodonUserNxt, createdAt: Date) {
self.id = id
self.reblog = reblog
self.author = author
self.createdAt = createdAt
}
public static func == (lhs: StatusNxt, rhs: StatusNxt) -> Bool {
lhs.id == rhs.id && lhs.author.id == rhs.author.id
}
public func hash(into hasher: inout Hasher) {
hasher.combine(id)
hasher.combine(createdAt)
}
}
public extension StatusNxt {
static func from(status: Mastodon.Entity.Status) -> StatusNxt {
StatusNxt(
id: status.id,
reblog: status.reblog != nil ? .from(status: status.reblog!) : nil,
author: .from(account: status.account),
createdAt: status.createdAt
)
}
static func from(status: Status) -> StatusNxt {
StatusNxt(
id: status.id,
reblog: status.reblog != nil ? .from(status: status.reblog!) : nil,
author: .from(user: status.author),
createdAt: status.createdAt
)
}
}

View File

@ -62,7 +62,7 @@ extension ComposeContentViewModel {
// configure status // configure status
context.managedObjectContext.performAndWait { context.managedObjectContext.performAndWait {
guard let replyTo = status.object(in: context.managedObjectContext) else { return } guard let replyTo = status.object(in: context.managedObjectContext) else { return }
cell.statusView.configure(status: replyTo) cell.statusView.configure(status: .from(status: replyTo))
} }
} }
} }

View File

@ -19,7 +19,7 @@ import CoreDataStack
extension NotificationView { extension NotificationView {
public final class ViewModel: ObservableObject { public final class ViewModel: ObservableObject {
public var disposeBag = Set<AnyCancellable>() public var disposeBag = Set<AnyCancellable>()
public var objects = Set<NSManagedObject>() public var objects = Set<NotificationNxt>()
@Published public var context: AppContext? @Published public var context: AppContext?
@Published public var authContext: AuthContext? @Published public var authContext: AuthContext?

View File

@ -26,7 +26,7 @@ extension StatusView {
assertionFailure() assertionFailure()
return return
} }
configure(status: status) configure(status: .from(status: status))
case .notificationAll: case .notificationAll:
assertionFailure("TODO") assertionFailure("TODO")
case .notificationMentions: case .notificationMentions:
@ -40,7 +40,7 @@ extension StatusView {
extension StatusView { extension StatusView {
public func configure(status: Status, statusEdit: StatusEdit) { public func configure(status: StatusNxt, statusEdit: StatusEdit) {
viewModel.objects.insert(status) viewModel.objects.insert(status)
if let reblog = status.reblog { if let reblog = status.reblog {
viewModel.objects.insert(reblog) viewModel.objects.insert(reblog)
@ -66,7 +66,7 @@ extension StatusView {
viewModel.isContentReveal = true viewModel.isContentReveal = true
} }
public func configure(status: Status) { public func configure(status: StatusNxt) {
viewModel.objects.insert(status) viewModel.objects.insert(status)
if let reblog = status.reblog { if let reblog = status.reblog {
viewModel.objects.insert(reblog) viewModel.objects.insert(reblog)
@ -99,7 +99,7 @@ extension StatusView {
} }
extension StatusView { extension StatusView {
private func configureHeader(status: Status) { private func configureHeader(status: StatusNxt) {
if let _ = status.reblog { if let _ = status.reblog {
Publishers.CombineLatest( Publishers.CombineLatest(
status.author.publisher(for: \.displayName), status.author.publisher(for: \.displayName),
@ -189,7 +189,7 @@ extension StatusView {
} }
} }
public func configureAuthor(author: MastodonUser) { public func configureAuthor(author: MastodonUserNxt) {
// author avatar // author avatar
Publishers.CombineLatest( Publishers.CombineLatest(
author.publisher(for: \.avatar), author.publisher(for: \.avatar),

View File

@ -22,7 +22,7 @@ extension StatusView {
public final class ViewModel: ObservableObject { public final class ViewModel: ObservableObject {
var disposeBag = Set<AnyCancellable>() var disposeBag = Set<AnyCancellable>()
var observations = Set<NSKeyValueObservation>() var observations = Set<NSKeyValueObservation>()
public var objects = Set<NSManagedObject>() public var objects = Set<StatusNxt>()
public var context: AppContext? public var context: AppContext?
public var authContext: AuthContext? public var authContext: AuthContext?