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.
//
import CoreData
import Foundation
import CoreDataStack
import MastodonSDK
enum NotificationItem: Hashable {
case feed(record: ManagedObjectRecord<Feed>)
case feedLoader(record: ManagedObjectRecord<Feed>)
case feed(record: FeedNxt)
case feedLoader(record: FeedNxt)
case bottomLoader
}

View File

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

View File

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

View File

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

View File

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

View File

@ -23,7 +23,6 @@ extension NotificationTimelineViewController: DataSourceProvider {
case .feed(let record):
let managedObjectContext = context.managedObjectContext
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 }
if let notification = feed.notification {
return .notification(record: .init(objectID: notification.objectID))

View File

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

View File

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

View File

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

View File

@ -8,10 +8,11 @@
import Foundation
import CoreDataStack
import MastodonCore
import MastodonSDK
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)
}
}

View File

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

View File

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

View File

@ -11,7 +11,7 @@ import MastodonCore
final class CachedThreadViewModel: ThreadViewModel {
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(
context: context,
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
context.managedObjectContext.performAndWait {
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 {
public final class ViewModel: ObservableObject {
public var disposeBag = Set<AnyCancellable>()
public var objects = Set<NSManagedObject>()
public var objects = Set<NotificationNxt>()
@Published public var context: AppContext?
@Published public var authContext: AuthContext?

View File

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

View File

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