Begin removing MastodonStatus, MastodonUser and related from CoreData (IOS-176, IOS-189)

This commit is contained in:
Marcus Kida 2023-11-16 16:54:25 +01:00
parent 7d41820015
commit 36091e9628
No known key found for this signature in database
GPG Key ID: 19FF64E08013CA40
181 changed files with 3241 additions and 3920 deletions

View File

@ -388,7 +388,6 @@
DB9282B225F3222800823B15 /* PickServerEmptyStateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB9282B125F3222800823B15 /* PickServerEmptyStateView.swift */; };
DB938EE62623F50700E5B6C1 /* ThreadViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB938EE52623F50700E5B6C1 /* ThreadViewController.swift */; };
DB938EED2623F79B00E5B6C1 /* ThreadViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB938EEC2623F79B00E5B6C1 /* ThreadViewModel.swift */; };
DB938F0326240EA300E5B6C1 /* CachedThreadViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB938F0226240EA300E5B6C1 /* CachedThreadViewModel.swift */; };
DB938F0926240F3C00E5B6C1 /* RemoteThreadViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB938F0826240F3C00E5B6C1 /* RemoteThreadViewModel.swift */; };
DB938F0F2624119800E5B6C1 /* ThreadViewModel+LoadThreadState.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB938F0E2624119800E5B6C1 /* ThreadViewModel+LoadThreadState.swift */; };
DB938F1F2624382F00E5B6C1 /* ThreadViewModel+Diffable.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB938F1E2624382F00E5B6C1 /* ThreadViewModel+Diffable.swift */; };
@ -451,7 +450,6 @@
DBCBCBF4267CB070000F5B51 /* Decode85.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBCBCBF3267CB070000F5B51 /* Decode85.swift */; };
DBCBED1726132DB500B49291 /* UserTimelineViewModel+Diffable.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBCBED1626132DB500B49291 /* UserTimelineViewModel+Diffable.swift */; };
DBCC3B30261440A50045B23D /* UITabBarController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBCC3B2F261440A50045B23D /* UITabBarController.swift */; };
DBCC3B8F26148F7B0045B23D /* CachedProfileViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBCC3B8E26148F7B0045B23D /* CachedProfileViewModel.swift */; };
DBD376B2269302A4007FEC24 /* UITableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBD376B1269302A4007FEC24 /* UITableViewCell.swift */; };
DBD5B1F827BCFD9D00BD6B38 /* DataSourceProvider+TableViewControllerNavigateable.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBD5B1F727BCFD9D00BD6B38 /* DataSourceProvider+TableViewControllerNavigateable.swift */; };
DBD5B1FA27BD013700BD6B38 /* DataSourceProvider+StatusTableViewControllerNavigateable.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBD5B1F927BD013700BD6B38 /* DataSourceProvider+StatusTableViewControllerNavigateable.swift */; };
@ -1102,7 +1100,6 @@
DB9282B125F3222800823B15 /* PickServerEmptyStateView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PickServerEmptyStateView.swift; sourceTree = "<group>"; };
DB938EE52623F50700E5B6C1 /* ThreadViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadViewController.swift; sourceTree = "<group>"; };
DB938EEC2623F79B00E5B6C1 /* ThreadViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadViewModel.swift; sourceTree = "<group>"; };
DB938F0226240EA300E5B6C1 /* CachedThreadViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CachedThreadViewModel.swift; sourceTree = "<group>"; };
DB938F0826240F3C00E5B6C1 /* RemoteThreadViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteThreadViewModel.swift; sourceTree = "<group>"; };
DB938F0E2624119800E5B6C1 /* ThreadViewModel+LoadThreadState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ThreadViewModel+LoadThreadState.swift"; sourceTree = "<group>"; };
DB938F1E2624382F00E5B6C1 /* ThreadViewModel+Diffable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ThreadViewModel+Diffable.swift"; sourceTree = "<group>"; };
@ -1190,7 +1187,6 @@
DBCBCBF3267CB070000F5B51 /* Decode85.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Decode85.swift; sourceTree = "<group>"; };
DBCBED1626132DB500B49291 /* UserTimelineViewModel+Diffable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UserTimelineViewModel+Diffable.swift"; sourceTree = "<group>"; };
DBCC3B2F261440A50045B23D /* UITabBarController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UITabBarController.swift; sourceTree = "<group>"; };
DBCC3B8E26148F7B0045B23D /* CachedProfileViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CachedProfileViewModel.swift; sourceTree = "<group>"; };
DBD376B1269302A4007FEC24 /* UITableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UITableViewCell.swift; sourceTree = "<group>"; };
DBD5B1F727BCFD9D00BD6B38 /* DataSourceProvider+TableViewControllerNavigateable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DataSourceProvider+TableViewControllerNavigateable.swift"; sourceTree = "<group>"; };
DBD5B1F927BD013700BD6B38 /* DataSourceProvider+StatusTableViewControllerNavigateable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DataSourceProvider+StatusTableViewControllerNavigateable.swift"; sourceTree = "<group>"; };
@ -2663,7 +2659,6 @@
DB938EEC2623F79B00E5B6C1 /* ThreadViewModel.swift */,
DB938F1E2624382F00E5B6C1 /* ThreadViewModel+Diffable.swift */,
DB938F0E2624119800E5B6C1 /* ThreadViewModel+LoadThreadState.swift */,
DB938F0226240EA300E5B6C1 /* CachedThreadViewModel.swift */,
DB938F0826240F3C00E5B6C1 /* RemoteThreadViewModel.swift */,
DB0FCB7F27968F70006C02E2 /* MastodonStatusThreadViewModel.swift */,
);
@ -2758,7 +2753,6 @@
DBFEEC97279BDC6A004F81DD /* About */,
DB9D6BFE25E4F5940051B173 /* ProfileViewController.swift */,
DBB5255D2611F07A002F1F29 /* ProfileViewModel.swift */,
DBCC3B8E26148F7B0045B23D /* CachedProfileViewModel.swift */,
DBAE3FAE26172FC0004B8251 /* RemoteProfileViewModel.swift */,
DBB525632612C988002F1F29 /* MeProfileViewModel.swift */,
);
@ -3870,7 +3864,6 @@
2D38F20825CD491300561493 /* DisposeBagCollectable.swift in Sources */,
D8F9170D2A4B3C6F008A5370 /* AboutMastodonTableViewCell.swift in Sources */,
DB697DE1278F5296004EF2F7 /* DataSourceFacade+Model.swift in Sources */,
DBCC3B8F26148F7B0045B23D /* CachedProfileViewModel.swift in Sources */,
DB4F097526A037F500D62E92 /* SearchHistoryViewModel.swift in Sources */,
DB3EA8E9281B7A3700598866 /* DiscoveryCommunityViewModel.swift in Sources */,
D87BFC8B291D5C6B00FEE264 /* MastodonLoginView.swift in Sources */,
@ -3882,7 +3875,6 @@
2AB5011C299243FB00346092 /* WidgetExtension.intentdefinition in Sources */,
DB9F58F126EF512300E7BBE9 /* AccountListTableViewCell.swift in Sources */,
2D76319F25C1521200929FB9 /* StatusSection.swift in Sources */,
DB938F0326240EA300E5B6C1 /* CachedThreadViewModel.swift in Sources */,
DBA5A53526F0A36A00CACBAA /* AddAccountTableViewCell.swift in Sources */,
2D35237A26256D920031AF25 /* NotificationSection.swift in Sources */,
DB98EB6927B21A7C0082E365 /* ReportResultActionTableViewCell.swift in Sources */,

View File

@ -7,11 +7,10 @@
import Foundation
import MastodonSDK
import CoreDataStack
enum DiscoveryItem: Hashable {
case hashtag(Mastodon.Entity.Tag)
case link(Mastodon.Entity.Link)
case user(ManagedObjectRecord<MastodonUser>)
case user(Mastodon.Entity.Account)
case bottomLoader
}

View File

@ -57,25 +57,22 @@ extension DiscoverySection {
return cell
case .user(let record):
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: ProfileCardTableViewCell.self), for: indexPath) as! ProfileCardTableViewCell
context.managedObjectContext.performAndWait {
guard let user = record.object(in: context.managedObjectContext) else { return }
cell.configure(
tableView: tableView,
user: user,
profileCardTableViewCellDelegate: configuration.profileCardTableViewCellDelegate
)
// bind familiarFollowers
if let familiarFollowers = configuration.familiarFollowers {
familiarFollowers
.map { array in array.first(where: { $0.id == user.id }) }
.assign(to: \.familiarFollowers, on: cell.profileCardView.viewModel)
.store(in: &cell.disposeBag)
} else {
cell.profileCardView.viewModel.familiarFollowers = nil
}
// bind me
cell.profileCardView.viewModel.relationshipViewModel.me = configuration.authContext.mastodonAuthenticationBox.authentication.user(in: context.managedObjectContext)
cell.configure(
tableView: tableView,
user: record,
profileCardTableViewCellDelegate: configuration.profileCardTableViewCellDelegate
)
// bind familiarFollowers
if let familiarFollowers = configuration.familiarFollowers {
familiarFollowers
.map { array in array.first(where: { $0.id == record.id }) }
.assign(to: \.familiarFollowers, on: cell.profileCardView.viewModel)
.store(in: &cell.disposeBag)
} else {
cell.profileCardView.viewModel.familiarFollowers = nil
}
// bind me
cell.profileCardView.viewModel.relationshipViewModel.me = configuration.authContext.mastodonAuthenticationBox.inMemoryCache.meAccount
return cell
case .bottomLoader:
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: TimelineBottomLoaderTableViewCell.self), for: indexPath) as! TimelineBottomLoaderTableViewCell

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: FeedItem)
case feedLoader(record: FeedItem)
case bottomLoader
}

View File

@ -43,16 +43,13 @@ extension NotificationSection {
switch item {
case .feed(let record):
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,
cell: cell,
viewModel: NotificationTableViewCell.ViewModel(value: .feed(feed)),
configuration: configuration
)
}
configure(
context: context,
tableView: tableView,
cell: cell,
viewModel: NotificationTableViewCell.ViewModel(value: .feed(record)),
configuration: configuration
)
return cell
case .feedLoader:
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: TimelineBottomLoaderTableViewCell.self), for: indexPath) as! TimelineBottomLoaderTableViewCell

View File

@ -6,13 +6,13 @@
//
import Foundation
import CoreDataStack
import MastodonSDK
enum ReportItem: Hashable {
case header(context: HeaderContext)
case status(record: ManagedObjectRecord<Status>)
case status(record: Mastodon.Entity.Status)
case comment(context: CommentContext)
case result(record: ManagedObjectRecord<MastodonUser>)
case result(record: Mastodon.Entity.Account)
case bottomLoader
}

View File

@ -6,13 +6,13 @@
//
import Foundation
import CoreDataStack
import MastodonUI
import MastodonSDK
enum StatusItem: Hashable {
case feed(record: ManagedObjectRecord<Feed>)
case feedLoader(record: ManagedObjectRecord<Feed>)
case status(record: ManagedObjectRecord<Status>)
case feed(record: FeedItem)
case feedLoader(record: FeedItem)
case status(record: Mastodon.Entity.Status)
case thread(Thread)
case topLoader
case bottomLoader
@ -24,7 +24,7 @@ extension StatusItem {
case reply(context: Context)
case leaf(context: Context)
public var record: ManagedObjectRecord<Status> {
public var record: Mastodon.Entity.Status {
switch self {
case .root(let threadContext),
.reply(let threadContext),
@ -37,12 +37,12 @@ extension StatusItem {
extension StatusItem.Thread {
class Context: Hashable {
let status: ManagedObjectRecord<Status>
let status: Mastodon.Entity.Status
var displayUpperConversationLink: Bool
var displayBottomConversationLink: Bool
init(
status: ManagedObjectRecord<Status>,
status: Mastodon.Entity.Status,
displayUpperConversationLink: Bool = false,
displayBottomConversationLink: Bool = false
) {

View File

@ -6,8 +6,6 @@
//
import Combine
import CoreData
import CoreDataStack
import UIKit
import AVKit
import AlamofireImage
@ -46,40 +44,31 @@ extension StatusSection {
switch item {
case .feed(let record):
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: StatusTableViewCell.self), for: indexPath) as! StatusTableViewCell
context.managedObjectContext.performAndWait {
guard let feed = record.object(in: context.managedObjectContext) else { return }
configure(
context: context,
tableView: tableView,
cell: cell,
viewModel: StatusTableViewCell.ViewModel(value: .feed(feed)),
configuration: configuration
)
}
configure(
context: context,
tableView: tableView,
cell: cell,
viewModel: StatusTableViewCell.ViewModel(value: .feed(record)),
configuration: configuration
)
return cell
case .feedLoader(let record):
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: TimelineMiddleLoaderTableViewCell.self), for: indexPath) as! TimelineMiddleLoaderTableViewCell
context.managedObjectContext.performAndWait {
guard let feed = record.object(in: context.managedObjectContext) else { return }
configure(
cell: cell,
feed: feed,
configuration: configuration
)
}
configure(
cell: cell,
feed: record,
configuration: configuration
)
return cell
case .status(let record):
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: StatusTableViewCell.self), for: indexPath) as! StatusTableViewCell
context.managedObjectContext.performAndWait {
guard let status = record.object(in: context.managedObjectContext) else { return }
configure(
context: context,
tableView: tableView,
cell: cell,
viewModel: StatusTableViewCell.ViewModel(value: .status(status)),
configuration: configuration
)
}
configure(
context: context,
tableView: tableView,
cell: cell,
viewModel: StatusTableViewCell.ViewModel(value: .status(record)),
configuration: configuration
)
return cell
case .thread(let thread):
let cell = dequeueConfiguredReusableCell(
@ -124,30 +113,24 @@ extension StatusSection {
switch configuration.thread {
case .root(let threadContext):
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: StatusThreadRootTableViewCell.self), for: indexPath) as! StatusThreadRootTableViewCell
managedObjectContext.performAndWait {
guard let status = threadContext.status.object(in: managedObjectContext) else { return }
StatusSection.configure(
context: context,
tableView: tableView,
cell: cell,
viewModel: StatusThreadRootTableViewCell.ViewModel(value: .status(status)),
configuration: configuration.configuration
)
}
StatusSection.configure(
context: context,
tableView: tableView,
cell: cell,
viewModel: StatusThreadRootTableViewCell.ViewModel(value: .status(threadContext.status)),
configuration: configuration.configuration
)
return cell
case .reply(let threadContext),
.leaf(let threadContext):
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: StatusTableViewCell.self), for: indexPath) as! StatusTableViewCell
managedObjectContext.performAndWait {
guard let status = threadContext.status.object(in: managedObjectContext) else { return }
StatusSection.configure(
context: context,
tableView: tableView,
cell: cell,
viewModel: StatusTableViewCell.ViewModel(value: .status(status)),
configuration: configuration.configuration
)
}
StatusSection.configure(
context: context,
tableView: tableView,
cell: cell,
viewModel: StatusTableViewCell.ViewModel(value: .status(threadContext.status)),
configuration: configuration.configuration
)
return cell
}
}
@ -161,12 +144,11 @@ extension StatusSection {
authContext: AuthContext,
statusView: StatusView
) {
let managedObjectContext = context.managedObjectContext
statusView.pollTableViewDiffableDataSource = UITableViewDiffableDataSource<PollSection, PollItem>(tableView: statusView.pollTableView) { tableView, indexPath, item in
switch item {
case .history:
return nil
case .option(let record):
return UITableViewCell()
case .option(let record, let poll):
// Fix cell reuse animation issue
let cell: PollOptionTableViewCell = {
let _cell = tableView.dequeueReusableCell(withIdentifier: String(describing: PollOptionTableViewCell.self) + "@\(indexPath.row)#\(indexPath.section)") as? PollOptionTableViewCell
@ -176,53 +158,8 @@ extension StatusSection {
cell.pollOptionView.viewModel.authContext = authContext
managedObjectContext.performAndWait {
guard let option = record.object(in: managedObjectContext) else {
assertionFailure()
return
}
cell.pollOptionView.configure(pollOption: option)
// trigger update if needs
let needsUpdatePoll: Bool = {
// check first option in poll to trigger update poll only once
guard
let poll = option.poll,
option.index == 0
else { return false }
cell.pollOptionView.configure(status: statusView.viewModel.originalStatus!, pollOption: record, poll: poll)
guard !poll.expired else {
return false
}
let now = Date()
let timeIntervalSinceUpdate = now.timeIntervalSince(poll.updatedAt)
#if DEBUG
let autoRefreshTimeInterval: TimeInterval = 3 // speedup testing
#else
let autoRefreshTimeInterval: TimeInterval = 30
#endif
guard timeIntervalSinceUpdate > autoRefreshTimeInterval else {
return false
}
return true
}()
if needsUpdatePoll {
guard let poll = option.poll else { return }
let pollRecord: ManagedObjectRecord<Poll> = .init(objectID: poll.objectID)
Task { [weak context] in
guard let context = context else { return }
_ = try await context.apiService.poll(
poll: pollRecord,
authenticationBox: authContext.mastodonAuthenticationBox
)
}
}
} // end managedObjectContext.performAndWait
return cell
}
}
@ -319,7 +256,7 @@ extension StatusSection {
static func configure(
cell: TimelineMiddleLoaderTableViewCell,
feed: Feed,
feed: FeedItem,
configuration: Configuration
) {
cell.configure(

View File

@ -11,7 +11,7 @@ import CoreDataStack
import MastodonSDK
enum UserItem: Hashable {
case user(record: ManagedObjectRecord<MastodonUser>)
case user(record: Mastodon.Entity.Account)
case account(account: Mastodon.Entity.Account, relationship: Mastodon.Entity.Relationship?)
case bottomLoader
case bottomHeader(text: String)

View File

@ -6,13 +6,12 @@
//
import UIKit
import CoreData
import CoreDataStack
import MastodonCore
import MastodonUI
import MastodonMeta
import MetaTextKit
import Combine
import MastodonSDK
enum UserSection: Hashable {
case main
@ -37,7 +36,7 @@ extension UserSection {
case .account(let account, let relationship):
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: UserTableViewCell.self), for: indexPath) as! UserTableViewCell
guard let me = authContext.mastodonAuthenticationBox.authentication.user(in: context.managedObjectContext) else { return cell }
guard let me = authContext.mastodonAuthenticationBox.inMemoryCache.meAccount else { return cell }
cell.userView.setButtonState(.loading)
cell.configure(
@ -53,14 +52,13 @@ extension UserSection {
case .user(let record):
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: UserTableViewCell.self), for: indexPath) as! UserTableViewCell
context.managedObjectContext.performAndWait {
guard let user = record.object(in: context.managedObjectContext) else { return }
configure(
context: context,
authContext: authContext,
tableView: tableView,
cell: cell,
viewModel: UserTableViewCell.ViewModel(
user: user,
user: record,
followedUsers: authContext.mastodonAuthenticationBox.inMemoryCache.$followingUserIds.eraseToAnyPublisher(),
blockedUsers: authContext.mastodonAuthenticationBox.inMemoryCache.$blockedUserIds.eraseToAnyPublisher(),
followRequestedUsers: authContext.mastodonAuthenticationBox.inMemoryCache.$followRequestedUserIDs.eraseToAnyPublisher()
@ -94,7 +92,7 @@ extension UserSection {
userTableViewCellDelegate: UserTableViewCellDelegate?
) {
cell.configure(
me: authContext.mastodonAuthenticationBox.authentication.user(in: context.managedObjectContext),
me: authContext.mastodonAuthenticationBox.inMemoryCache.meAccount,
tableView: tableView,
viewModel: viewModel,
delegate: userTableViewCellDelegate

View File

@ -6,14 +6,13 @@
//
import UIKit
import CoreData
import CoreDataStack
import MastodonSDK
import MastodonCore
extension DataSourceFacade {
public static func responseToStatusBookmarkAction(
provider: UIViewController & NeedsDependency & AuthContextProvider,
status: ManagedObjectRecord<Status>
status: Mastodon.Entity.Status
) async throws {
let selectionFeedbackGenerator = await UISelectionFeedbackGenerator()
await selectionFeedbackGenerator.selectionChanged()

View File

@ -6,14 +6,13 @@
//
import UIKit
import CoreData
import CoreDataStack
import MastodonCore
import MastodonSDK
extension DataSourceFacade {
public static func responseToStatusFavoriteAction(
provider: DataSourceProvider & AuthContextProvider,
status: ManagedObjectRecord<Status>
status: Mastodon.Entity.Status
) async throws {
let selectionFeedbackGenerator = await UISelectionFeedbackGenerator()
await selectionFeedbackGenerator.selectionChanged()

View File

@ -6,8 +6,6 @@
//
import UIKit
import CoreDataStack
import class CoreDataStack.Notification
import MastodonCore
import MastodonSDK
import MastodonLocalization
@ -15,7 +13,7 @@ import MastodonLocalization
extension DataSourceFacade {
static func responseToUserFollowAction(
dependency: NeedsDependency & AuthContextProvider,
user: ManagedObjectRecord<MastodonUser>
user: Mastodon.Entity.Account
) async throws {
let selectionFeedbackGenerator = await UISelectionFeedbackGenerator()
await selectionFeedbackGenerator.selectionChanged()
@ -26,86 +24,61 @@ extension DataSourceFacade {
)
dependency.context.authenticationService.fetchFollowingAndBlockedAsync()
}
static func responseToUserFollowAction(
dependency: NeedsDependency & AuthContextProvider,
user: Mastodon.Entity.Account
) async throws {
let selectionFeedbackGenerator = await UISelectionFeedbackGenerator()
await selectionFeedbackGenerator.selectionChanged()
_ = try await dependency.context.apiService.toggleFollow(
user: user,
authenticationBox: dependency.authContext.mastodonAuthenticationBox
)
dependency.context.authenticationService.fetchFollowingAndBlockedAsync()
}
}
extension DataSourceFacade {
static func responseToUserFollowRequestAction(
dependency: NeedsDependency & AuthContextProvider,
notification: ManagedObjectRecord<Notification>,
notification: Mastodon.Entity.Notification,
query: Mastodon.API.Account.FollowRequestQuery
) async throws {
let selectionFeedbackGenerator = await UISelectionFeedbackGenerator()
await selectionFeedbackGenerator.selectionChanged()
let managedObjectContext = dependency.context.managedObjectContext
let _userID: MastodonUser.ID? = try await managedObjectContext.perform {
guard let notification = notification.object(in: managedObjectContext) else { return nil }
return notification.account.id
}
guard let userID = _userID else {
assertionFailure()
throw APIService.APIError.implicit(.badRequest)
}
let state: MastodonFollowRequestState = try await managedObjectContext.perform {
guard let notification = notification.object(in: managedObjectContext) else { return .init(state: .none) }
return notification.followRequestState
}
guard state.state == .none else {
return
}
try? await managedObjectContext.performChanges {
guard let notification = notification.object(in: managedObjectContext) else { return }
switch query {
case .accept:
notification.transientFollowRequestState = .init(state: .isAccepting)
case .reject:
notification.transientFollowRequestState = .init(state: .isRejecting)
}
}
// let state: MastodonFollowRequestState = try await managedObjectContext.perform {
// guard let notification = notification.object(in: managedObjectContext) else { return .init(state: .none) }
// return notification.followRequestState
// }
//
// guard state.state == .none else {
// return
// }
//
// try? await managedObjectContext.performChanges {
// guard let notification = notification.object(in: managedObjectContext) else { return }
// switch query {
// case .accept:
// notification.transientFollowRequestState = .init(state: .isAccepting)
// case .reject:
// notification.transientFollowRequestState = .init(state: .isRejecting)
// }
// }
do {
_ = try await dependency.context.apiService.followRequest(
userID: userID,
userID: notification.account.id,
query: query,
authenticationBox: dependency.authContext.mastodonAuthenticationBox
)
} catch {
// reset state when failure
try? await managedObjectContext.performChanges {
guard let notification = notification.object(in: managedObjectContext) else { return }
notification.transientFollowRequestState = .init(state: .none)
}
// try? await managedObjectContext.performChanges {
// guard let notification = notification.object(in: managedObjectContext) else { return }
// notification.transientFollowRequestState = .init(state: .none)
// }
if let error = error as? Mastodon.API.Error {
switch error.httpResponseStatus {
case .notFound:
let backgroundManagedObjectContext = dependency.context.backgroundManagedObjectContext
try await backgroundManagedObjectContext.performChanges {
guard let notification = notification.object(in: backgroundManagedObjectContext) else { return }
for feed in notification.feeds {
backgroundManagedObjectContext.delete(feed)
}
backgroundManagedObjectContext.delete(notification)
}
break
// let backgroundManagedObjectContext = dependency.context.backgroundManagedObjectContext
// try await backgroundManagedObjectContext.performChanges {
// guard let notification = notification.object(in: backgroundManagedObjectContext) else { return }
// for feed in notification.feeds {
// backgroundManagedObjectContext.delete(feed)
// }
// backgroundManagedObjectContext.delete(notification)
// }
default:
let alertController = await UIAlertController(for: error, title: nil, preferredStyle: .alert)
let okAction = await UIAlertAction(title: L10n.Common.Controls.Actions.ok, style: .default)
@ -121,38 +94,38 @@ extension DataSourceFacade {
return
}
try? await managedObjectContext.performChanges {
guard let notification = notification.object(in: managedObjectContext) else { return }
switch query {
case .accept:
notification.transientFollowRequestState = .init(state: .isAccept)
case .reject:
// do nothing due to will delete notification
break
}
}
// try? await managedObjectContext.performChanges {
// guard let notification = notification.object(in: managedObjectContext) else { return }
// switch query {
// case .accept:
// notification.transientFollowRequestState = .init(state: .isAccept)
// case .reject:
// // do nothing due to will delete notification
// break
// }
// }
let backgroundManagedObjectContext = dependency.context.backgroundManagedObjectContext
try? await backgroundManagedObjectContext.performChanges {
guard let notification = notification.object(in: backgroundManagedObjectContext) else { return }
switch query {
case .accept:
notification.followRequestState = .init(state: .isAccept)
case .reject:
// delete notification
for feed in notification.feeds {
backgroundManagedObjectContext.delete(feed)
}
backgroundManagedObjectContext.delete(notification)
}
}
// let backgroundManagedObjectContext = dependency.context.backgroundManagedObjectContext
// try? await backgroundManagedObjectContext.performChanges {
// guard let notification = notification.object(in: backgroundManagedObjectContext) else { return }
// switch query {
// case .accept:
// notification.followRequestState = .init(state: .isAccept)
// case .reject:
// // delete notification
// for feed in notification.feeds {
// backgroundManagedObjectContext.delete(feed)
// }
// backgroundManagedObjectContext.delete(notification)
// }
// }
} // end func
}
extension DataSourceFacade {
static func responseToShowHideReblogAction(
dependency: NeedsDependency & AuthContextProvider,
user: ManagedObjectRecord<MastodonUser>
user: Mastodon.Entity.Account
) async throws {
_ = try await dependency.context.apiService.toggleShowReblogs(
for: user,

View File

@ -19,8 +19,6 @@ extension DataSourceFacade {
switch tag {
case .entity(let entity):
await coordinateToHashtagScene(provider: provider, tag: entity)
case .record(let record):
await coordinateToHashtagScene(provider: provider, tag: record)
}
}

View File

@ -9,6 +9,7 @@ import UIKit
import CoreDataStack
import MastodonUI
import MastodonLocalization
import MastodonSDK
extension DataSourceFacade {
@ -61,14 +62,13 @@ extension DataSourceFacade {
@MainActor
static func coordinateToMediaPreviewScene(
dependency: NeedsDependency & MediaPreviewableViewController,
status: ManagedObjectRecord<Status>,
status: Mastodon.Entity.Status,
previewContext: AttachmentPreviewContext
) async throws {
let managedObjectContext = dependency.context.managedObjectContext
let attachments: [MastodonAttachment] = try await managedObjectContext.perform {
guard let _status = status.object(in: managedObjectContext) else { return [] }
let status = _status.reblog ?? _status
return status.attachments
let status = status.reblog ?? status
return status.mastodonAttachments
}
let thumbnails = await previewContext.thumbnails()

View File

@ -6,20 +6,19 @@
//
import Foundation
import CoreDataStack
import MetaTextKit
import MastodonCore
import MastodonSDK
extension DataSourceFacade {
static func responseToMetaTextAction(
provider: DataSourceProvider & AuthContextProvider,
target: StatusTarget,
status: ManagedObjectRecord<Status>,
status: Mastodon.Entity.Status,
meta: Meta
) async throws {
let _redirectRecord = await DataSourceFacade.status(
managedObjectContext: provider.context.managedObjectContext,
let _redirectRecord = DataSourceFacade.status(
status: status,
target: target
)
@ -35,7 +34,7 @@ extension DataSourceFacade {
static func responseToMetaTextAction(
provider: DataSourceProvider & AuthContextProvider,
status: ManagedObjectRecord<Status>,
status: Mastodon.Entity.Status,
meta: Meta
) async {
switch meta {

View File

@ -6,44 +6,14 @@
//
import Foundation
import CoreData
import CoreDataStack
import MastodonUI
import MastodonSDK
extension DataSourceFacade {
static func status(
managedObjectContext: NSManagedObjectContext,
status: ManagedObjectRecord<Status>,
status: Mastodon.Entity.Status,
target: StatusTarget
) async -> ManagedObjectRecord<Status>? {
return try? await managedObjectContext.perform {
guard let object = status.object(in: managedObjectContext) else { return nil }
return DataSourceFacade.status(status: object, target: target)
.flatMap { ManagedObjectRecord<Status>(objectID: $0.objectID) }
}
}
}
extension DataSourceFacade {
static func author(
managedObjectContext: NSManagedObjectContext,
status: ManagedObjectRecord<Status>,
target: StatusTarget
) async -> ManagedObjectRecord<MastodonUser>? {
return try? await managedObjectContext.perform {
guard let object = status.object(in: managedObjectContext) else { return nil }
return DataSourceFacade.status(status: object, target: target)
.flatMap { $0.author }
.flatMap { ManagedObjectRecord<MastodonUser>(objectID: $0.objectID) }
}
}
}
extension DataSourceFacade {
static func status(
status: Status,
target: StatusTarget
) -> Status? {
) -> Mastodon.Entity.Status? {
switch target {
case .status:
return status.reblog ?? status
@ -52,3 +22,13 @@ extension DataSourceFacade {
}
}
}
extension DataSourceFacade {
static func author(
status: Mastodon.Entity.Status,
target: StatusTarget
) -> Mastodon.Entity.Account? {
DataSourceFacade.status(status: status, target: target)
.flatMap { $0.account }
}
}

View File

@ -6,13 +6,13 @@
//
import UIKit
import CoreDataStack
import MastodonCore
import MastodonSDK
extension DataSourceFacade {
static func responseToUserMuteAction(
dependency: NeedsDependency & AuthContextProvider,
user: ManagedObjectRecord<MastodonUser>
user: Mastodon.Entity.Account
) async throws {
let selectionFeedbackGenerator = await UISelectionFeedbackGenerator()
await selectionFeedbackGenerator.selectionChanged()

View File

@ -6,19 +6,18 @@
//
import UIKit
import CoreDataStack
import MastodonCore
import MastodonSDK
import CoreDataStack
extension DataSourceFacade {
static func coordinateToProfileScene(
provider: DataSourceProvider & AuthContextProvider,
target: StatusTarget,
status: ManagedObjectRecord<Status>
status: Mastodon.Entity.Status
) async {
let _redirectRecord = await DataSourceFacade.author(
managedObjectContext: provider.context.managedObjectContext,
let _redirectRecord = DataSourceFacade.author(
status: status,
target: target
)
@ -35,17 +34,12 @@ extension DataSourceFacade {
@MainActor
static func coordinateToProfileScene(
provider: ViewControllerWithDependencies & AuthContextProvider,
user: ManagedObjectRecord<MastodonUser>
user: Mastodon.Entity.Account
) async {
guard let user = user.object(in: provider.context.managedObjectContext) else {
assertionFailure()
return
}
let profileViewModel = CachedProfileViewModel(
let profileViewModel = ProfileViewModel(
context: provider.context,
authContext: provider.authContext,
mastodonUser: user
optionalMastodonUser: user
)
_ = provider.coordinator.present(
@ -71,9 +65,8 @@ extension DataSourceFacade {
authenticationBox: provider.authContext.mastodonAuthenticationBox)
provider.coordinator.hideLoading()
if let user {
await coordinateToProfileScene(provider: provider, user: user.asRecord)
}
await coordinateToProfileScene(provider: provider, user: user)
} catch {
provider.coordinator.hideLoading()
}
@ -85,12 +78,10 @@ extension DataSourceFacade {
static func coordinateToProfileScene(
provider: DataSourceProvider & AuthContextProvider,
status: ManagedObjectRecord<Status>,
status: Mastodon.Entity.Status,
mention: String, // username,
userInfo: [AnyHashable: Any]?
) async {
let domain = provider.authContext.mastodonAuthenticationBox.domain
guard
let href = userInfo?["href"] as? String,
let url = URL(string: href)
@ -98,10 +89,7 @@ extension DataSourceFacade {
return
}
let managedObjectContext = provider.context.managedObjectContext
let mentions = try? await managedObjectContext.perform {
return status.object(in: managedObjectContext)?.mentions ?? []
}
let mentions = status.mentions
guard let mention = mentions?.first(where: { $0.url == href }) else {
_ = await provider.coordinator.present(
@ -119,16 +107,7 @@ extension DataSourceFacade {
return MeProfileViewModel(context: provider.context, authContext: provider.authContext)
}
let request = MastodonUser.sortedFetchRequest
request.fetchLimit = 1
request.predicate = MastodonUser.predicate(domain: domain, id: userID)
let _user = provider.context.managedObjectContext.safeFetch(request).first
if let user = _user {
return CachedProfileViewModel(context: provider.context, authContext: provider.authContext, mastodonUser: user)
} else {
return RemoteProfileViewModel(context: provider.context, authContext: provider.authContext, userID: userID)
}
return RemoteProfileViewModel(context: provider.context, authContext: provider.authContext, userID: userID)
}()
_ = await provider.coordinator.present(
@ -154,11 +133,10 @@ extension DataSourceFacade {
static func createActivityViewController(
dependency: NeedsDependency,
user: ManagedObjectRecord<MastodonUser>
user: Mastodon.Entity.Account
) async throws -> UIActivityViewController? {
let managedObjectContext = dependency.context.managedObjectContext
let activityItems: [Any] = try await managedObjectContext.perform {
guard let user = user.object(in: managedObjectContext) else { return [] }
return user.activityItems
}
guard !activityItems.isEmpty else {
@ -173,7 +151,7 @@ extension DataSourceFacade {
return activityViewController
}
static func createActivityViewControllerForMastodonUser(status: Status, dependency: NeedsDependency) -> UIActivityViewController {
static func createActivityViewControllerForMastodonUser(status: Mastodon.Entity.Status, dependency: NeedsDependency) -> UIActivityViewController {
let activityViewController = UIActivityViewController(
activityItems: status.activityItems,
applicationActivities: [SafariActivity(sceneCoordinator: dependency.coordinator)]

View File

@ -6,14 +6,14 @@
//
import UIKit
import CoreDataStack
import MastodonCore
import MastodonUI
import MastodonSDK
extension DataSourceFacade {
static func responseToStatusReblogAction(
provider: DataSourceProvider & AuthContextProvider,
status: ManagedObjectRecord<Status>
status: Mastodon.Entity.Status
) async throws {
let selectionFeedbackGenerator = await UISelectionFeedbackGenerator()
await selectionFeedbackGenerator.selectionChanged()

View File

@ -22,71 +22,60 @@ extension DataSourceFacade {
break // not create search history for status
case .user(let record):
let authenticationBox = provider.authContext.mastodonAuthenticationBox
let managedObjectContext = provider.context.backgroundManagedObjectContext
try? await managedObjectContext.performChanges {
guard let me = authenticationBox.authentication.user(in: managedObjectContext) else { return }
guard let user = record.object(in: managedObjectContext) else { return }
_ = Persistence.SearchHistory.createOrMerge(
in: managedObjectContext,
context: Persistence.SearchHistory.PersistContext(
entity: .user(user),
me: me,
now: Date()
)
)
} // end try? await managedObjectContext.performChanges { }
assertionFailure("Implement storing search history")
case .hashtag(let tag):
let authenticationBox = provider.authContext.mastodonAuthenticationBox
let managedObjectContext = provider.context.backgroundManagedObjectContext
switch tag {
case .entity(let entity):
try? await managedObjectContext.performChanges {
guard let me = authenticationBox.authentication.user(in: managedObjectContext) else { return }
let now = Date()
let result = Persistence.Tag.createOrMerge(
in: managedObjectContext,
context: Persistence.Tag.PersistContext(
domain: authenticationBox.domain,
entity: entity,
me: me,
networkDate: now
)
)
_ = Persistence.SearchHistory.createOrMerge(
in: managedObjectContext,
context: Persistence.SearchHistory.PersistContext(
entity: .hashtag(result.tag),
me: me,
now: now
)
)
} // end try? await managedObjectContext.performChanges { }
case .record(let record):
try? await managedObjectContext.performChanges {
let authenticationBox = provider.authContext.mastodonAuthenticationBox
guard let me = authenticationBox.authentication.user(in: managedObjectContext) else { return }
guard let tag = record.object(in: managedObjectContext) else { return }
let now = Date()
assertionFailure("Implement storing search history")
_ = Persistence.SearchHistory.createOrMerge(
in: managedObjectContext,
context: Persistence.SearchHistory.PersistContext(
entity: .hashtag(tag),
me: me,
now: now
)
)
} // end try? await managedObjectContext.performChanges { }
// try? await managedObjectContext.performChanges {
// guard let me = authenticationBox.authentication.user(in: managedObjectContext) else { return }
//
// let now = Date()
//
// let result = Persistence.Tag.createOrMerge(
// in: managedObjectContext,
// context: Persistence.Tag.PersistContext(
// domain: authenticationBox.domain,
// entity: entity,
// me: me,
// networkDate: now
// )
// )
//
// _ = Persistence.SearchHistory.createOrMerge(
// in: managedObjectContext,
// context: Persistence.SearchHistory.PersistContext(
// entity: .hashtag(result.tag),
// me: me,
// now: now
// )
// )
// } // end try? await managedObjectContext.performChanges { }
// case .record(let record):
// try? await managedObjectContext.performChanges {
// let authenticationBox = provider.authContext.mastodonAuthenticationBox
// guard let me = authenticationBox.authentication.user(in: managedObjectContext) else { return }
// guard let tag = record.object(in: managedObjectContext) else { return }
//
// let now = Date()
//
// _ = Persistence.SearchHistory.createOrMerge(
// in: managedObjectContext,
// context: Persistence.SearchHistory.PersistContext(
// entity: .hashtag(tag),
// me: me,
// now: now
// )
// )
// } // end try? await managedObjectContext.performChanges { }
} // end switch tag { }
case .notification:
assertionFailure()
} // end switch item { }
} //end switch item { }
} // end func
}

View File

@ -7,7 +7,7 @@ import CoreDataStack
extension DataSourceFacade {
public static func getEditHistory(
forStatus status: Status,
forStatus status: Mastodon.Entity.Status,
provider: NeedsDependency & AuthContextProvider
) async throws -> [Mastodon.Entity.StatusEdit] {
let reponse = try await provider.context.apiService.getHistory(forStatusID: status.id, authenticationBox: provider.authContext.mastodonAuthenticationBox)

View File

@ -6,7 +6,6 @@
//
import UIKit
import CoreDataStack
import Alamofire
import AlamofireImage
import MastodonCore
@ -14,13 +13,14 @@ import MastodonUI
import MastodonLocalization
import LinkPresentation
import UniformTypeIdentifiers
import MastodonSDK
// Delete
extension DataSourceFacade {
static func responseToDeleteStatus(
dependency: NeedsDependency & AuthContextProvider,
status: ManagedObjectRecord<Status>
status: Mastodon.Entity.Status
) async throws {
_ = try await dependency.context.apiService.deleteStatus(
status: status,
@ -36,7 +36,7 @@ extension DataSourceFacade {
@MainActor
public static func responseToStatusShareAction(
provider: DataSourceProvider,
status: ManagedObjectRecord<Status>,
status: Mastodon.Entity.Status,
button: UIButton
) async throws {
let activityViewController = try await createActivityViewController(
@ -56,22 +56,21 @@ extension DataSourceFacade {
private static func createActivityViewController(
dependency: NeedsDependency,
status: ManagedObjectRecord<Status>
status: Mastodon.Entity.Status
) async throws -> UIActivityViewController {
var activityItems: [Any] = try await dependency.context.managedObjectContext.perform {
guard let status = status.object(in: dependency.context.managedObjectContext),
let url = URL(string: status.url ?? status.uri)
var activityItems: [Any] = {
guard let url = URL(string: status.url ?? status.uri)
else { return [] }
return [
URLActivityItemWithMetadata(url: url) { metadata in
metadata.title = "\(status.author.displayName) (@\(status.author.acctWithDomain))"
metadata.title = "\(status.account.displayName) (@\(status.account.acctWithDomain))"
metadata.iconProvider = ImageProvider(
url: status.author.avatarImageURLWithFallback(domain: status.author.domain),
url: status.account.avatarImageURLWithFallback(domain: status.account.domain!),
filter: ScaledToSizeFilter(size: CGSize.authorAvatarButtonSize)
).itemProvider
}
] as [Any]
}
}()
var applicationActivities: [UIActivity] = [
SafariActivity(sceneCoordinator: dependency.coordinator), // open URL
]
@ -94,20 +93,20 @@ extension DataSourceFacade {
@MainActor
static func responseToActionToolbar(
provider: DataSourceProvider & AuthContextProvider,
status: ManagedObjectRecord<Status>,
status: Mastodon.Entity.Status,
action: ActionToolbarContainer.Action,
sender: UIButton
) async throws {
let managedObjectContext = provider.context.managedObjectContext
let _status: ManagedObjectRecord<Status>? = try? await managedObjectContext.perform {
guard let object = status.object(in: managedObjectContext) else { return nil }
let objectID = (object.reblog ?? object).objectID
return .init(objectID: objectID)
}
guard let status = _status else {
assertionFailure()
return
}
// let managedObjectContext = provider.context.managedObjectContext
// let _status: ManagedObjectRecord<Status>? = try? await managedObjectContext.perform {
// guard let object = status.object(in: managedObjectContext) else { return nil }
// let objectID = (object.reblog ?? object).objectID
// return .init(objectID: objectID)
// }
// guard let status = _status else {
// assertionFailure()
// return
// }
switch action {
case .reply:
@ -150,7 +149,7 @@ extension DataSourceFacade {
extension DataSourceFacade {
struct MenuContext {
let author: ManagedObjectRecord<MastodonUser>?
let author: Mastodon.Entity.Account?
let statusViewModel: StatusView.ViewModel?
let button: UIButton?
let barButtonItem: UIBarButtonItem?
@ -178,17 +177,9 @@ extension DataSourceFacade {
title: actionTitle,
style: .destructive
) { [weak dependency] _ in
guard let dependency else { return }
guard let dependency, let user = menuContext.author else { return }
Task {
let managedObjectContext = dependency.context.managedObjectContext
let _user: ManagedObjectRecord<MastodonUser>? = try? await managedObjectContext.perform {
guard let user = menuContext.author?.object(in: managedObjectContext) else { return nil }
return ManagedObjectRecord<MastodonUser>(objectID: user.objectID)
}
guard let user = _user else { return }
try await DataSourceFacade.responseToShowHideReblogAction(
dependency: dependency,
user: user
@ -214,12 +205,7 @@ extension DataSourceFacade {
) { [weak dependency] _ in
guard let dependency = dependency else { return }
Task {
let managedObjectContext = dependency.context.managedObjectContext
let _user: ManagedObjectRecord<MastodonUser>? = try? await managedObjectContext.perform {
guard let user = menuContext.author?.object(in: managedObjectContext) else { return nil }
return ManagedObjectRecord<MastodonUser>(objectID: user.objectID)
}
guard let user = _user else { return }
guard let user = menuContext.author else { return }
try await DataSourceFacade.responseToUserMuteAction(
dependency: dependency,
user: user
@ -242,12 +228,7 @@ extension DataSourceFacade {
) { [weak dependency] _ in
guard let dependency = dependency else { return }
Task {
let managedObjectContext = dependency.context.managedObjectContext
let _user: ManagedObjectRecord<MastodonUser>? = try? await managedObjectContext.perform {
guard let user = menuContext.author?.object(in: managedObjectContext) else { return nil }
return ManagedObjectRecord<MastodonUser>(objectID: user.objectID)
}
guard let user = _user else { return }
guard let user = menuContext.author else { return }
try await DataSourceFacade.responseToUserBlockAction(
dependency: dependency,
user: user
@ -266,7 +247,7 @@ extension DataSourceFacade {
context: dependency.context,
authContext: dependency.authContext,
user: user,
status: menuContext.statusViewModel?.originalStatus?.asRecord
status: menuContext.statusViewModel?.originalStatus
)
_ = dependency.coordinator.present(
@ -297,7 +278,7 @@ extension DataSourceFacade {
)
case .bookmarkStatus:
Task {
guard let status = menuContext.statusViewModel?.originalStatus?.asRecord else {
guard let status = menuContext.statusViewModel?.originalStatus else {
assertionFailure()
return
}
@ -308,13 +289,7 @@ extension DataSourceFacade {
} // end Task
case .shareStatus:
Task {
let managedObjectContext = dependency.context.managedObjectContext
guard let status: ManagedObjectRecord<Status> = try? await managedObjectContext.perform(block: {
guard let object = menuContext.statusViewModel?.originalStatus?.asRecord.object(in: managedObjectContext) else { return nil }
let objectID = (object.reblog ?? object).objectID
return .init(objectID: objectID)
}) else {
assertionFailure()
guard let status = menuContext.statusViewModel?.originalStatus else {
return
}
@ -344,7 +319,7 @@ extension DataSourceFacade {
style: .destructive
) { [weak dependency] _ in
guard let dependency = dependency else { return }
guard let status = menuContext.statusViewModel?.originalStatus?.asRecord else { return }
guard let status = menuContext.statusViewModel?.originalStatus else { return }
Task {
try await DataSourceFacade.responseToDeleteStatus(
dependency: dependency,
@ -358,7 +333,7 @@ extension DataSourceFacade {
dependency.present(alertController, animated: true)
case .translateStatus:
guard let status = menuContext.statusViewModel?.originalStatus?.asRecord else { return }
guard let status = menuContext.statusViewModel?.originalStatus else { return }
do {
let translation = try await DataSourceFacade.translateStatus(provider: dependency,status: status)
@ -371,7 +346,7 @@ extension DataSourceFacade {
}
case .editStatus:
guard let status = menuContext.statusViewModel?.originalStatus?.asRecord.object(in: dependency.context.managedObjectContext) else { return }
guard let status = menuContext.statusViewModel?.originalStatus else { return }
let statusSource = try await dependency.context.apiService.getStatusSource(
forStatusID: status.id,
@ -402,13 +377,13 @@ extension DataSourceFacade {
static func responseToToggleSensitiveAction(
dependency: NeedsDependency,
status: ManagedObjectRecord<Status>
status: Mastodon.Entity.Status
) async throws {
try await dependency.context.managedObjectContext.perform {
guard let _status = status.object(in: dependency.context.managedObjectContext) else { return }
let status = _status.reblog ?? _status
status.update(isSensitiveToggled: !status.isSensitiveToggled)
}
// try await dependency.context.managedObjectContext.perform {
// let _status = status.reblog ?? status
// status.update(isSensitiveToggled: !_status.sensitiveToggled)
// }
assertionFailure("Net yet implemented :-(")
}
}

View File

@ -6,19 +6,17 @@
//
import UIKit
import CoreData
import CoreDataStack
import MastodonCore
import MastodonSDK
extension DataSourceFacade {
static func coordinateToStatusThreadScene(
provider: ViewControllerWithDependencies & AuthContextProvider,
target: StatusTarget,
status: ManagedObjectRecord<Status>
status: Mastodon.Entity.Status
) async {
let _root: StatusItem.Thread? = await {
let _redirectRecord = await DataSourceFacade.status(
managedObjectContext: provider.context.managedObjectContext,
let _root: StatusItem.Thread? = {
let _redirectRecord = DataSourceFacade.status(
status: status,
target: target
)

View File

@ -6,8 +6,6 @@
//
import UIKit
import CoreData
import CoreDataStack
import MastodonCore
import MastodonSDK
@ -20,27 +18,21 @@ extension DataSourceFacade {
public static func translateStatus(
provider: Provider,
status: ManagedObjectRecord<Status>
status: Mastodon.Entity.Status
) async throws -> Mastodon.Entity.Translation? {
let selectionFeedbackGenerator = await UISelectionFeedbackGenerator()
await selectionFeedbackGenerator.selectionChanged()
guard
let status = status.object(in: provider.context.managedObjectContext)
else {
return nil
}
if let reblog = status.reblog {
return try await translateStatus(provider: provider, status: reblog)
return try await _translateStatus(provider: provider, status: reblog)
} else {
return try await translateStatus(provider: provider, status: status)
return try await _translateStatus(provider: provider, status: status)
}
}
}
private extension DataSourceFacade {
static func translateStatus(provider: Provider, status: Status) async throws -> Mastodon.Entity.Translation? {
static func _translateStatus(provider: Provider, status: Mastodon.Entity.Status) async throws -> Mastodon.Entity.Translation? {
do {
let value = try await provider.context
.apiService

View File

@ -9,11 +9,12 @@ import Foundation
import CoreDataStack
import MetaTextKit
import MastodonCore
import MastodonSDK
extension DataSourceFacade {
static func responseToURLAction(
provider: DataSourceProvider & AuthContextProvider,
status: ManagedObjectRecord<Status>,
status: Mastodon.Entity.Status,
url: URL
) async {
let domain = provider.authContext.mastodonAuthenticationBox.domain

View File

@ -2,69 +2,10 @@
import Foundation
import MastodonUI
import CoreDataStack
import MastodonCore
import MastodonSDK
extension DataSourceFacade {
static func responseToUserViewButtonAction(
dependency: NeedsDependency & AuthContextProvider,
user: ManagedObjectRecord<MastodonUser>,
buttonState: UserView.ButtonState
) async throws {
switch buttonState {
case .follow:
try await DataSourceFacade.responseToUserFollowAction(
dependency: dependency,
user: user
)
if let userObject = user.object(in: dependency.context.managedObjectContext) {
dependency.authContext.mastodonAuthenticationBox.inMemoryCache.followingUserIds.append(userObject.id)
}
case .request:
try await DataSourceFacade.responseToUserFollowAction(
dependency: dependency,
user: user
)
if let userObject = user.object(in: dependency.context.managedObjectContext) {
dependency.authContext.mastodonAuthenticationBox.inMemoryCache.followRequestedUserIDs.append(userObject.id)
}
case .unfollow:
try await DataSourceFacade.responseToUserFollowAction(
dependency: dependency,
user: user
)
if let userObject = user.object(in: dependency.context.managedObjectContext) {
dependency.authContext.mastodonAuthenticationBox.inMemoryCache.followingUserIds.removeAll(where: { $0 == userObject.id })
}
case .blocked:
try await DataSourceFacade.responseToUserBlockAction(
dependency: dependency,
user: user
)
if let userObject = user.object(in: dependency.context.managedObjectContext) {
dependency.authContext.mastodonAuthenticationBox.inMemoryCache.blockedUserIds.append(userObject.id)
}
case .pending:
try await DataSourceFacade.responseToUserFollowAction(
dependency: dependency,
user: user
)
if let userObject = user.object(in: dependency.context.managedObjectContext) {
dependency.authContext.mastodonAuthenticationBox.inMemoryCache.followRequestedUserIDs.removeAll(where: { $0 == userObject.id })
}
case .none, .loading:
break //no-op
}
}
static func responseToUserViewButtonAction(
dependency: NeedsDependency & AuthContextProvider,
user: Mastodon.Entity.Account,

View File

@ -10,6 +10,7 @@ import MetaTextKit
import CoreDataStack
import MastodonCore
import MastodonUI
import MastodonSDK
// MARK: - Notification AuthorMenuAction
extension NotificationTableViewCellDelegate where Self: DataSourceProvider & AuthContextProvider {
@ -30,20 +31,11 @@ extension NotificationTableViewCellDelegate where Self: DataSourceProvider & Aut
return
}
let _author: ManagedObjectRecord<MastodonUser>? = try await self.context.managedObjectContext.perform {
guard let notification = notification.object(in: self.context.managedObjectContext) else { return nil }
return .init(objectID: notification.account.objectID)
}
guard let author = _author else {
assertionFailure()
return
}
try await DataSourceFacade.responseToMenuAction(
dependency: self,
action: action,
menuContext: .init(
author: author,
author: notification.account,
statusViewModel: nil,
button: button,
barButtonItem: nil
@ -70,17 +62,9 @@ extension NotificationTableViewCellDelegate where Self: DataSourceProvider & Aut
assertionFailure("only works for status data provider")
return
}
let _author: ManagedObjectRecord<MastodonUser>? = try await self.context.managedObjectContext.perform {
guard let notification = notification.object(in: self.context.managedObjectContext) else { return nil }
return .init(objectID: notification.account.objectID)
}
guard let author = _author else {
assertionFailure()
return
}
await DataSourceFacade.coordinateToProfileScene(
provider: self,
user: author
user: notification.account
)
} // end Task
}
@ -155,7 +139,7 @@ extension NotificationTableViewCellDelegate where Self: DataSourceProvider & Aut
}
private struct NotificationMediaTransitionContext {
let status: ManagedObjectRecord<Status>
let status: Mastodon.Entity.Status
let needsToggleMediaSensitive: Bool
}
@ -175,23 +159,17 @@ extension NotificationTableViewCellDelegate where Self: DataSourceProvider & Med
assertionFailure()
return
}
guard case let .notification(record) = item else {
guard case let .notification(record) = item, let _status = record.status else {
assertionFailure("only works for status data provider")
return
}
let managedObjectContext = self.context.managedObjectContext
let _mediaTransitionContext: NotificationMediaTransitionContext? = try await managedObjectContext.perform {
guard let notification = record.object(in: managedObjectContext) else { return nil }
guard let _status = notification.status else { return nil }
let status = _status.reblog ?? _status
return NotificationMediaTransitionContext(
status: .init(objectID: status.objectID),
needsToggleMediaSensitive: status.isSensitiveToggled ? !status.sensitive : status.sensitive
)
}
guard let mediaTransitionContext = _mediaTransitionContext else { return }
let status = _status.reblog ?? _status
let mediaTransitionContext = NotificationMediaTransitionContext(
status: status,
needsToggleMediaSensitive: status.sensitiveToggled ? !(status.sensitive == true) : status.sensitive == true
)
guard !mediaTransitionContext.needsToggleMediaSensitive else {
try await DataSourceFacade.responseToToggleSensitiveAction(
@ -227,23 +205,18 @@ extension NotificationTableViewCellDelegate where Self: DataSourceProvider & Med
assertionFailure()
return
}
guard case let .notification(record) = item else {
guard case let .notification(record) = item, let _status = record.status else {
assertionFailure("only works for status data provider")
return
}
let managedObjectContext = self.context.managedObjectContext
let _mediaTransitionContext: NotificationMediaTransitionContext? = try await managedObjectContext.perform {
guard let notification = record.object(in: managedObjectContext) else { return nil }
guard let _status = notification.status else { return nil }
let status = _status.reblog ?? _status
return NotificationMediaTransitionContext(
status: .init(objectID: status.objectID),
needsToggleMediaSensitive: status.isMediaSensitive ? !status.isSensitiveToggled : false
)
}
guard let mediaTransitionContext = _mediaTransitionContext else { return }
let status = _status.reblog ?? _status
let mediaTransitionContext = NotificationMediaTransitionContext(
status: status,
needsToggleMediaSensitive: status.sensitiveToggled ? !status.sensitiveToggled : false
)
guard !mediaTransitionContext.needsToggleMediaSensitive else {
try await DataSourceFacade.responseToToggleSensitiveAction(
@ -286,11 +259,8 @@ extension NotificationTableViewCellDelegate where Self: DataSourceProvider & Aut
assertionFailure("only works for status data provider")
return
}
let _status: ManagedObjectRecord<Status>? = try await self.context.managedObjectContext.perform {
guard let notification = notification.object(in: self.context.managedObjectContext) else { return nil }
guard let status = notification.status else { return nil }
return .init(objectID: status.objectID)
}
let _status = notification.status
guard let status = _status else {
assertionFailure()
return
@ -323,12 +293,8 @@ extension NotificationTableViewCellDelegate where Self: DataSourceProvider & Aut
assertionFailure("only works for status data provider")
return
}
let _author: ManagedObjectRecord<MastodonUser>? = try await self.context.managedObjectContext.perform {
guard let notification = notification.object(in: self.context.managedObjectContext) else { return nil }
guard let status = notification.status else { return nil }
return .init(objectID: status.author.objectID)
}
guard let author = _author else {
guard let author = notification.status?.account else {
assertionFailure()
return
}
@ -367,12 +333,8 @@ extension NotificationTableViewCellDelegate where Self: DataSourceProvider & Aut
assertionFailure("only works for notification item")
return
}
let _status: ManagedObjectRecord<Status>? = try await self.context.managedObjectContext.perform {
guard let notification = notification.object(in: self.context.managedObjectContext) else { return nil }
guard let status = notification.status else { return nil }
return .init(objectID: status.objectID)
}
guard let status = _status else {
guard let status = notification.status else {
assertionFailure()
return
}
@ -400,12 +362,8 @@ extension NotificationTableViewCellDelegate where Self: DataSourceProvider & Aut
assertionFailure("only works for notification item")
return
}
let _status: ManagedObjectRecord<Status>? = try await self.context.managedObjectContext.perform {
guard let notification = notification.object(in: self.context.managedObjectContext) else { return nil }
guard let status = notification.status else { return nil }
return .init(objectID: status.objectID)
}
guard let status = _status else {
guard let status = notification.status else {
assertionFailure()
return
}
@ -465,12 +423,8 @@ extension NotificationTableViewCellDelegate where Self: DataSourceProvider & Aut
assertionFailure("only works for notification item")
return
}
let _status: ManagedObjectRecord<Status>? = try await self.context.managedObjectContext.perform {
guard let notification = notification.object(in: self.context.managedObjectContext) else { return nil }
guard let status = notification.status else { return nil }
return .init(objectID: status.objectID)
}
guard let status = _status else {
guard let status = notification.status else {
assertionFailure()
return
}
@ -497,12 +451,8 @@ extension NotificationTableViewCellDelegate where Self: DataSourceProvider & Aut
assertionFailure("only works for notification item")
return
}
let _status: ManagedObjectRecord<Status>? = try await self.context.managedObjectContext.perform {
guard let notification = notification.object(in: self.context.managedObjectContext) else { return nil }
guard let status = notification.status else { return nil }
return .init(objectID: status.objectID)
}
guard let status = _status else {
guard let status = notification.status else {
assertionFailure()
return
}

View File

@ -6,13 +6,13 @@
//
import UIKit
import CoreDataStack
import MetaTextKit
import MastodonCore
import MastodonUI
import MastodonLocalization
import MastodonAsset
import LinkPresentation
import MastodonSDK
// MARK: - header
extension StatusTableViewCellDelegate where Self: DataSourceProvider & AuthContextProvider {
@ -37,22 +37,15 @@ extension StatusTableViewCellDelegate where Self: DataSourceProvider & AuthConte
case .none:
break
case .reply:
let _replyToAuthor: ManagedObjectRecord<MastodonUser>? = try? await context.managedObjectContext.perform {
guard let status = status.object(in: self.context.managedObjectContext) else { return nil }
guard let inReplyToAccountID = status.inReplyToAccountID else { return nil }
let request = MastodonUser.sortedFetchRequest
request.predicate = MastodonUser.predicate(domain: status.author.domain, id: inReplyToAccountID)
request.fetchLimit = 1
guard let author = self.context.managedObjectContext.safeFetch(request).first else { return nil }
return .init(objectID: author.objectID)
}
guard let replyToAuthor = _replyToAuthor else {
return
}
let account = try await context.apiService.accountLookup(
domain: authContext.mastodonAuthenticationBox.domain,
query: .init(acct: status.account.id),
authorization: authContext.mastodonAuthenticationBox.userAuthorization
).singleOutput().value
await DataSourceFacade.coordinateToProfileScene(
provider: self,
user: replyToAuthor
user: account
)
case .repost:
@ -184,7 +177,7 @@ extension StatusTableViewCellDelegate where Self: DataSourceProvider & AuthConte
cardControlMenu statusCardControl: StatusCardControl
) -> [LabeledAction]? {
guard let card = statusView.viewModel.card,
let url = card.url else {
let url = URL(string: card.url) else {
return nil
}
@ -206,8 +199,8 @@ extension StatusTableViewCellDelegate where Self: DataSourceProvider & AuthConte
URLActivityItemWithMetadata(url: url) { metadata in
metadata.title = card.title
if let image = card.imageURL {
metadata.iconProvider = ImageProvider(url: image, filter: nil).itemProvider
if let image = card.image, let url = URL(string: image) {
metadata.iconProvider = ImageProvider(url: url, filter: nil).itemProvider
}
}
],
@ -313,54 +306,51 @@ extension StatusTableViewCellDelegate where Self: DataSourceProvider & AuthConte
return
}
var _poll: ManagedObjectRecord<Poll>?
var _isMultiple: Bool?
var _choice: Int?
try await managedObjectContext.performChanges {
guard let pollOption = pollOption.object(in: managedObjectContext) else { return }
guard let poll = pollOption.poll else { return }
_poll = .init(objectID: poll.objectID)
_isMultiple = poll.multiple
guard !poll.isVoting else { return }
if !poll.multiple {
for option in poll.options where option != pollOption {
option.update(isSelected: false)
}
// mark voting
poll.update(isVoting: true)
// set choice
_choice = Int(pollOption.index)
}
pollOption.update(isSelected: !pollOption.isSelected)
poll.update(updatedAt: Date())
}
// var _poll: ManagedObjectRecord<Poll>?
// var _isMultiple: Bool?
// var _choice: Int?
//
// try await managedObjectContext.performChanges {
// guard let pollOption = pollOption.object(in: managedObjectContext) else { return }
// guard let poll = pollOption.poll else { return }
// _poll = .init(objectID: poll.objectID)
//
// _isMultiple = poll.multiple
// guard !poll.isVoting else { return }
//
// if !poll.multiple {
// for option in poll.options where option != pollOption {
// option.update(isSelected: false)
// }
//
// // mark voting
// poll.update(isVoting: true)
// // set choice
// _choice = Int(pollOption.index)
// }
//
// pollOption.update(isSelected: !pollOption.isSelected)
// poll.update(updatedAt: Date())
// }
// Trigger vote API request for
guard let poll = _poll,
_isMultiple == false,
let choice = _choice
else { return }
guard pollOption.poll.multiple == false else { return }
do {
_ = try await context.apiService.vote(
poll: poll,
choices: [choice],
poll: pollOption.poll,
choices: [indexPath.row],
authenticationBox: authContext.mastodonAuthenticationBox
)
} catch {
// restore voting state
try await managedObjectContext.performChanges {
guard
let pollOption = pollOption.object(in: managedObjectContext),
let poll = pollOption.poll
else { return }
poll.update(isVoting: false)
}
// try await managedObjectContext.performChanges {
// guard
// let pollOption = pollOption.object(in: managedObjectContext),
// let poll = pollOption.poll
// else { return }
// poll.update(isVoting: false)
// }
}
} // end Task
@ -373,48 +363,27 @@ extension StatusTableViewCellDelegate where Self: DataSourceProvider & AuthConte
) {
guard let pollTableViewDiffableDataSource = statusView.pollTableViewDiffableDataSource else { return }
guard let firstPollItem = pollTableViewDiffableDataSource.snapshot().itemIdentifiers.first else { return }
guard case let .option(firstPollOption) = firstPollItem else { return }
guard case let .option(option, poll) = firstPollItem else { return }
let managedObjectContext = context.managedObjectContext
Task {
var _poll: ManagedObjectRecord<Poll>?
var _choices: [Int]?
try await managedObjectContext.performChanges {
guard let poll = firstPollOption.object(in: managedObjectContext)?.poll else { return }
_poll = .init(objectID: poll.objectID)
guard poll.multiple else { return }
// mark voting
poll.update(isVoting: true)
// set choice
_choices = poll.options
.filter { $0.isSelected }
.map { Int($0.index) }
poll.update(updatedAt: Date())
}
// Trigger vote API request for
guard let poll = _poll,
let choices = _choices
else { return }
do {
_ = try await context.apiService.vote(
poll: poll,
choices: choices,
authenticationBox: authContext.mastodonAuthenticationBox
)
} catch {
// restore voting state
try await managedObjectContext.performChanges {
guard let poll = poll.object(in: managedObjectContext) else { return }
poll.update(isVoting: false)
}
}
assertionFailure("Re-implement this")
// do {
// _ = try await context.apiService.vote(
// poll: poll,
// choices: choices,
// authenticationBox: authContext.mastodonAuthenticationBox
// )
// } catch {
// // restore voting state
// try await managedObjectContext.performChanges {
// guard let poll = poll.object(in: managedObjectContext) else { return }
// poll.update(isVoting: false)
// }
// }
} // end Task
}
@ -470,16 +439,7 @@ extension StatusTableViewCellDelegate where Self: DataSourceProvider & AuthConte
assertionFailure("only works for status data provider")
return
}
let _author: ManagedObjectRecord<MastodonUser>? = try await self.context.managedObjectContext.perform {
guard let _status = status.object(in: self.context.managedObjectContext) else { return nil }
let author = (_status.reblog ?? _status).author
return .init(objectID: author.objectID)
}
guard let author = _author else {
assertionFailure()
return
}
if case .translateStatus = action {
DispatchQueue.main.async {
if let cell = cell as? StatusTableViewCell {
@ -513,7 +473,7 @@ extension StatusTableViewCellDelegate where Self: DataSourceProvider & AuthConte
dependency: self,
action: action,
menuContext: .init(
author: author,
author: status.account,
statusViewModel: statusViewModel,
button: button,
barButtonItem: nil
@ -679,11 +639,7 @@ extension StatusTableViewCellDelegate where Self: DataSourceProvider & AuthConte
assertionFailure("only works for status data provider")
return
}
guard let status = status.object(in: context.managedObjectContext) else {
return await coordinator.hideLoading()
}
do {
let edits = try await context.apiService.getHistory(forStatusID: status.id, authenticationBox: authContext.mastodonAuthenticationBox).value

View File

@ -6,8 +6,8 @@
//
import UIKit
import CoreDataStack
import MastodonCore
import MastodonSDK
extension StatusTableViewControllerNavigateableCore where Self: DataSourceProvider & StatusTableViewControllerNavigateableRelay {
@ -55,7 +55,7 @@ extension StatusTableViewControllerNavigateableCore where Self: DataSourceProvid
extension StatusTableViewControllerNavigateableCore where Self: DataSourceProvider & AuthContextProvider {
@MainActor
private func statusRecord() async -> ManagedObjectRecord<Status>? {
private func statusRecord() async -> Mastodon.Entity.Status? {
guard let indexPathForSelectedRow = tableView.indexPathForSelectedRow else { return nil }
let source = DataSourceItem.Source(indexPath: indexPathForSelectedRow)
guard let item = await item(from: source) else { return nil }
@ -64,15 +64,7 @@ extension StatusTableViewControllerNavigateableCore where Self: DataSourceProvid
case .status(let record):
return record
case .notification(let record):
let _statusRecord: ManagedObjectRecord<Status>? = try? await context.managedObjectContext.perform {
guard let notification = record.object(in: self.context.managedObjectContext) else { return nil }
guard let status = notification.status else { return nil }
return .init(objectID: status.objectID)
}
guard let statusRecord = _statusRecord else {
return nil
}
return statusRecord
return record.status
default:
return nil
}

View File

@ -40,30 +40,17 @@ extension UITableViewDelegate where Self: DataSourceProvider & AuthContextProvid
tag: tag
)
case .notification(let notification):
let managedObjectContext = context.managedObjectContext
let _status: ManagedObjectRecord<Status>? = try await managedObjectContext.perform {
guard let notification = notification.object(in: managedObjectContext) else { return nil }
guard let status = notification.status else { return nil }
return .init(objectID: status.objectID)
}
if let status = _status {
if let status = notification.status {
await DataSourceFacade.coordinateToStatusThreadScene(
provider: self,
target: .status, // remove reblog wrapper
status: status
)
} else {
let _author: ManagedObjectRecord<MastodonUser>? = try await managedObjectContext.perform {
guard let notification = notification.object(in: managedObjectContext) else { return nil }
return .init(objectID: notification.account.objectID)
}
if let author = _author {
await DataSourceFacade.coordinateToProfileScene(
provider: self,
user: author
)
}
await DataSourceFacade.coordinateToProfileScene(
provider: self,
user: notification.account
)
}
}
} // end Task

View File

@ -7,22 +7,19 @@
//
import UIKit
import CoreDataStack
import MastodonSDK
import class CoreDataStack.Notification
enum DataSourceItem: Hashable {
case status(record: ManagedObjectRecord<Status>)
case user(record: ManagedObjectRecord<MastodonUser>)
case status(record: Mastodon.Entity.Status)
case user(record: Mastodon.Entity.Account)
case hashtag(tag: TagKind)
case notification(record: ManagedObjectRecord<Notification>)
case notification(record: Mastodon.Entity.Notification)
case account(account: Mastodon.Entity.Account, relationship: Mastodon.Entity.Relationship?)
}
extension DataSourceItem {
enum TagKind: Hashable {
case entity(Mastodon.Entity.Tag)
case record(ManagedObjectRecord<Tag>)
}
}

View File

@ -21,7 +21,7 @@ final class ComposeViewModel {
enum Context {
case composeStatus
case editStatus(status: Status, statusSource: Mastodon.Entity.StatusSource)
case editStatus(status: Mastodon.Entity.Status, statusSource: Mastodon.Entity.StatusSource)
}
var disposeBag = Set<AnyCancellable>()

View File

@ -29,7 +29,7 @@ extension DiscoveryCommunityViewModel {
stateMachine.enter(State.Reloading.self)
statusFetchedResultsController.$records
$records
.receive(on: DispatchQueue.main)
.sink { [weak self] records in
guard let self = self else { return }

View File

@ -145,10 +145,10 @@ extension DiscoveryCommunityViewModel.State {
self.maxID = newMaxID
var hasNewStatusesAppend = false
var statusIDs = isReloading ? [] : viewModel.statusFetchedResultsController.statusIDs
var newRecords = isReloading ? [] : viewModel.records
for status in response.value {
guard !statusIDs.contains(status.id) else { continue }
statusIDs.append(status.id)
guard !newRecords.contains(where: { $0.id == status.id }) else { continue }
newRecords.append(status)
hasNewStatusesAppend = true
}
@ -158,7 +158,7 @@ extension DiscoveryCommunityViewModel.State {
} else {
await enter(state: NoMore.self)
}
viewModel.statusFetchedResultsController.statusIDs = statusIDs
viewModel.records = newRecords
viewModel.didLoadLatest.send()
} catch {

View File

@ -21,7 +21,7 @@ final class DiscoveryCommunityViewModel {
let context: AppContext
let authContext: AuthContext
let viewDidAppeared = PassthroughSubject<Void, Never>()
let statusFetchedResultsController: StatusFetchedResultsController
@Published var records = [Mastodon.Entity.Status]()
let listBatchFetchViewModel = ListBatchFetchViewModel()
// output
@ -44,11 +44,5 @@ final class DiscoveryCommunityViewModel {
init(context: AppContext, authContext: AuthContext) {
self.context = context
self.authContext = authContext
self.statusFetchedResultsController = StatusFetchedResultsController(
managedObjectContext: context.managedObjectContext,
domain: authContext.mastodonAuthenticationBox.domain,
additionalTweetPredicate: nil
)
// end init
}
}

View File

@ -97,11 +97,10 @@ extension DiscoveryForYouViewController: UITableViewDelegate {
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
guard case let .user(record) = viewModel.diffableDataSource?.itemIdentifier(for: indexPath) else { return }
guard let user = record.object(in: context.managedObjectContext) else { return }
let profileViewModel = CachedProfileViewModel(
let profileViewModel = ProfileViewModel(
context: context,
authContext: viewModel.authContext,
mastodonUser: user
optionalMastodonUser: record
)
_ = coordinator.present(
scene: .profile(viewModel: profileViewModel),
@ -137,9 +136,8 @@ extension DiscoveryForYouViewController: ProfileCardTableViewCellDelegate {
) {
guard let indexPath = tableView.indexPath(for: cell) else { return }
guard case let .user(record) = viewModel.diffableDataSource?.itemIdentifier(for: indexPath) else { return }
guard let user = record.object(in: context.managedObjectContext) else { return }
let userID = user.id
let userID = record.id
let _familiarFollowers = viewModel.familiarFollowers.first(where: { $0.id == userID })
guard let familiarFollowers = _familiarFollowers else {
assertionFailure()

View File

@ -29,7 +29,7 @@ extension DiscoveryForYouViewModel {
try await fetch()
}
userFetchedResultsController.$records
$records
.receive(on: DispatchQueue.main)
.sink { [weak self] records in
guard let self = self else { return }

View File

@ -20,12 +20,13 @@ final class DiscoveryForYouViewModel {
// input
let context: AppContext
let authContext: AuthContext
let userFetchedResultsController: UserFetchedResultsController
// let userFetchedResultsController: UserFetchedResultsController
@MainActor
@Published var familiarFollowers: [Mastodon.Entity.FamiliarFollowers] = []
@Published var isFetching = false
@Published var records = [Mastodon.Entity.Account]()
// output
var diffableDataSource: UITableViewDiffableDataSource<DiscoverySection, DiscoveryItem>?
let didLoadLatest = PassthroughSubject<Void, Never>()
@ -33,11 +34,11 @@ final class DiscoveryForYouViewModel {
init(context: AppContext, authContext: AuthContext) {
self.context = context
self.authContext = authContext
self.userFetchedResultsController = UserFetchedResultsController(
managedObjectContext: context.managedObjectContext,
domain: authContext.mastodonAuthenticationBox.domain,
additionalPredicate: nil
)
// self.userFetchedResultsController = UserFetchedResultsController(
// managedObjectContext: context.managedObjectContext,
// domain: authContext.mastodonAuthenticationBox.domain,
// additionalPredicate: nil
// )
// end init
}
}
@ -58,7 +59,7 @@ extension DiscoveryForYouViewModel {
authenticationBox: authContext.mastodonAuthenticationBox
)
familiarFollowers = _familiarFollowersResponse?.value ?? []
userFetchedResultsController.userIDs = userIDs
// userFetchedResultsController.userIDs = userIDs
} catch {
// do nothing
}

View File

@ -29,7 +29,7 @@ extension DiscoveryPostsViewModel {
stateMachine.enter(State.Reloading.self)
statusFetchedResultsController.$records
$records
.receive(on: DispatchQueue.main)
.sink { [weak self] records in
guard let self = self else { return }

View File

@ -143,10 +143,10 @@ extension DiscoveryPostsViewModel.State {
self.offset = newOffset
var hasNewStatusesAppend = false
var statusIDs = isReloading ? [] : viewModel.statusFetchedResultsController.statusIDs
var newStatuses = isReloading ? [] : viewModel.records
for status in response.value {
guard !statusIDs.contains(status.id) else { continue }
statusIDs.append(status.id)
guard !newStatuses.contains(where: { $0.id == status.id }) else { continue }
newStatuses.append(status)
hasNewStatusesAppend = true
}
@ -155,7 +155,7 @@ extension DiscoveryPostsViewModel.State {
} else {
await enter(state: NoMore.self)
}
viewModel.statusFetchedResultsController.statusIDs = statusIDs
viewModel.records = newStatuses
viewModel.didLoadLatest.send()
} catch {

View File

@ -20,7 +20,7 @@ final class DiscoveryPostsViewModel {
// input
let context: AppContext
let authContext: AuthContext
let statusFetchedResultsController: StatusFetchedResultsController
@Published var records = [Mastodon.Entity.Status]()
let listBatchFetchViewModel = ListBatchFetchViewModel()
// output
@ -44,12 +44,6 @@ final class DiscoveryPostsViewModel {
init(context: AppContext, authContext: AuthContext) {
self.context = context
self.authContext = authContext
self.statusFetchedResultsController = StatusFetchedResultsController(
managedObjectContext: context.managedObjectContext,
domain: authContext.mastodonAuthenticationBox.domain,
additionalTweetPredicate: nil
)
// end init
Task {
await checkServerEndpoint()

View File

@ -34,7 +34,7 @@ extension HashtagTimelineViewModel {
snapshot.appendSections([.main])
diffableDataSource?.apply(snapshot)
fetchedResultsController.$records
$records
.receive(on: DispatchQueue.main)
.sink { [weak self] records in
guard let self = self else { return }

View File

@ -7,7 +7,7 @@
import Foundation
import GameplayKit
import CoreDataStack
import MastodonSDK
extension HashtagTimelineViewModel {
class State: GKState {
@ -93,7 +93,7 @@ extension HashtagTimelineViewModel.State {
}
class Loading: HashtagTimelineViewModel.State {
var maxID: Status.ID?
var maxID: Mastodon.Entity.Status.ID?
override func isValidNextState(_ stateClass: AnyClass) -> Bool {
switch stateClass {
@ -145,10 +145,10 @@ extension HashtagTimelineViewModel.State {
self.maxID = newMaxID
var hasNewStatusesAppend = false
var statusIDs = isReloading ? [] : viewModel.fetchedResultsController.statusIDs
var newRecords = isReloading ? [] : viewModel.records
for status in response.value {
guard !statusIDs.contains(status.id) else { continue }
statusIDs.append(status.id)
guard !newRecords.contains(where: { $0.id == status.id }) else { continue }
newRecords.append(status)
hasNewStatusesAppend = true
}
@ -158,7 +158,7 @@ extension HashtagTimelineViewModel.State {
await enter(state: NoMore.self)
}
viewModel.fetchedResultsController.append(statusIDs: statusIDs)
viewModel.records = newRecords
viewModel.didLoadLatest.send()
} catch {
await enter(state: Fail.self)

View File

@ -7,8 +7,6 @@
import UIKit
import Combine
import CoreData
import CoreDataStack
import GameplayKit
import MastodonSDK
import MastodonCore
@ -24,7 +22,7 @@ final class HashtagTimelineViewModel {
// input
let context: AppContext
let authContext: AuthContext
let fetchedResultsController: StatusFetchedResultsController
// let fetchedResultsController: StatusFetchedResultsController
let isFetchingLatestTimeline = CurrentValueSubject<Bool, Never>(false)
let timelinePredicate = CurrentValueSubject<NSPredicate?, Never>(nil)
let hashtagEntity = CurrentValueSubject<Mastodon.Entity.Tag?, Never>(nil)
@ -34,6 +32,8 @@ final class HashtagTimelineViewModel {
var diffableDataSource: UITableViewDiffableDataSource<StatusSection, StatusItem>?
let didLoadLatest = PassthroughSubject<Void, Never>()
let hashtagDetails = CurrentValueSubject<Mastodon.Entity.Tag?, Never>(nil)
@Published var records = [Mastodon.Entity.Status]()
// bottom loader
private(set) lazy var stateMachine: GKStateMachine = {
@ -54,28 +54,30 @@ final class HashtagTimelineViewModel {
self.context = context
self.authContext = authContext
self.hashtag = hashtag
self.fetchedResultsController = StatusFetchedResultsController(
managedObjectContext: context.managedObjectContext,
domain: authContext.mastodonAuthenticationBox.domain,
additionalTweetPredicate: nil
)
// self.fetchedResultsController = StatusFetchedResultsController(
// managedObjectContext: context.managedObjectContext,
// domain: authContext.mastodonAuthenticationBox.domain,
// additionalTweetPredicate: nil
// )
updateTagInformation()
// end init
}
func viewWillAppear() {
let predicate = Tag.predicate(
domain: authContext.mastodonAuthenticationBox.domain,
name: hashtag
)
// let predicate = Tag.predicate(
// domain: authContext.mastodonAuthenticationBox.domain,
// name: hashtag
// )
#warning("Re-Implement this")
guard
let object = Tag.findOrFetch(in: context.managedObjectContext, matching: predicate)
false//let object = Tag.findOrFetch(in: context.managedObjectContext, matching: predicate)
else {
return hashtagDetails.send(hashtagDetails.value?.copy(following: false))
}
hashtagDetails.send(hashtagDetails.value?.copy(following: object.following))
// hashtagDetails.send(hashtagDetails.value?.copy(following: object.following))
}
}

View File

@ -20,17 +20,11 @@ extension HomeTimelineViewController: DataSourceProvider {
}
switch item {
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 == .home else { return nil }
if let status = feed.status {
return .status(record: .init(objectID: status.objectID))
} else {
return nil
}
}
case .feed(let record):
let item: DataSourceItem? = {
guard let status = record.status else { return nil }
return .status(record: status)
}()
return item
default:
return nil

View File

@ -9,6 +9,7 @@ import UIKit
import CoreData
import CoreDataStack
import MastodonUI
import MastodonSDK
extension HomeTimelineViewModel {
@ -35,7 +36,7 @@ extension HomeTimelineViewModel {
snapshot.appendSections([.main])
diffableDataSource?.apply(snapshot)
fetchedResultsController.$records
$records
.receive(on: DispatchQueue.main)
.sink { [weak self] records in
guard let self = self else { return }
@ -45,7 +46,7 @@ extension HomeTimelineViewModel {
let oldSnapshot = diffableDataSource.snapshot()
var newSnapshot: NSDiffableDataSourceSnapshot<StatusSection, StatusItem> = {
let newItems = records.map { record in
StatusItem.feed(record: record)
StatusItem.feed(record: .init(status: record, notification: nil, hasMore: false, isLoadingMore: false))
}
var snapshot = NSDiffableDataSourceSnapshot<StatusSection, StatusItem>()
snapshot.appendSections([.main])
@ -53,36 +54,37 @@ extension HomeTimelineViewModel {
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.fetchedResultsController.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)
}
}
}
#warning("Code below needs to be re-implemented")
// 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.fetchedResultsController.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.hasPendingStatusEditReload {

View File

@ -7,10 +7,9 @@
import func QuartzCore.CACurrentMediaTime
import Foundation
import CoreData
import CoreDataStack
import GameplayKit
import MastodonCore
import MastodonSDK
extension HomeTimelineViewModel {
class LoadLatestState: GKState {
@ -83,15 +82,11 @@ extension HomeTimelineViewModel.LoadLatestState {
guard let viewModel else { return }
let latestFeedRecords = viewModel.fetchedResultsController.records.prefix(APIService.onceRequestStatusMaxCount)
let parentManagedObjectContext = viewModel.fetchedResultsController.managedObjectContext
let managedObjectContext = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType)
managedObjectContext.parent = parentManagedObjectContext
let latestFeedRecords = viewModel.records.prefix(APIService.onceRequestStatusMaxCount)
Task {
let latestStatusIDs: [Status.ID] = latestFeedRecords.compactMap { record in
guard let feed = record.object(in: managedObjectContext) else { return nil }
return feed.status?.id
let latestStatusIDs: [Mastodon.Entity.Status.ID] = latestFeedRecords.compactMap { record in
return record.id
}
do {

View File

@ -31,7 +31,7 @@ extension HomeTimelineViewModel.LoadOldestState {
class Initial: HomeTimelineViewModel.LoadOldestState {
override func isValidNextState(_ stateClass: AnyClass) -> Bool {
guard let viewModel = viewModel else { return false }
guard !viewModel.fetchedResultsController.records.isEmpty else { return false }
guard !viewModel.records.isEmpty else { return false }
return stateClass == Loading.self
}
}
@ -46,19 +46,13 @@ extension HomeTimelineViewModel.LoadOldestState {
guard let viewModel = viewModel, let stateMachine = stateMachine else { return }
guard let lastFeedRecord = viewModel.fetchedResultsController.records.last else {
guard let lastFeedRecord = viewModel.records.last else {
stateMachine.enter(Idle.self)
return
}
Task {
let managedObjectContext = viewModel.fetchedResultsController.managedObjectContext
let _maxID: Mastodon.Entity.Status.ID? = try await managedObjectContext.perform {
guard let feed = lastFeedRecord.object(in: managedObjectContext),
let status = feed.status
else { return nil }
return status.id
}
let _maxID: Mastodon.Entity.Status.ID? = viewModel.records.last?.id
guard let maxID = _maxID else {
await self.enter(state: Fail.self)

View File

@ -9,12 +9,11 @@ import func AVFoundation.AVMakeRect
import UIKit
import AVKit
import Combine
import CoreData
import CoreDataStack
import GameplayKit
import AlamofireImage
import MastodonCore
import MastodonUI
import MastodonSDK
final class HomeTimelineViewModel: NSObject {
@ -24,7 +23,7 @@ final class HomeTimelineViewModel: NSObject {
// input
let context: AppContext
let authContext: AuthContext
let fetchedResultsController: FeedFetchedResultsController
// let fetchedResultsController: FeedFetchedResultsController
let homeTimelineNavigationBarTitleViewModel: HomeTimelineNavigationBarTitleViewModel
let listBatchFetchViewModel = ListBatchFetchViewModel()
@ -34,6 +33,7 @@ final class HomeTimelineViewModel: NSObject {
@Published var scrollPositionRecord: ScrollPositionRecord? = nil
@Published var displaySettingBarButtonItem = true
@Published var hasPendingStatusEditReload = false
@Published var records = [Mastodon.Entity.Status]()
weak var tableView: UITableView?
weak var timelineMiddleLoaderTableViewCellDelegate: TimelineMiddleLoaderTableViewCellDelegate?
@ -80,14 +80,19 @@ final class HomeTimelineViewModel: NSObject {
init(context: AppContext, authContext: AuthContext) {
self.context = context
self.authContext = authContext
self.fetchedResultsController = FeedFetchedResultsController(managedObjectContext: context.managedObjectContext)
// self.fetchedResultsController = FeedFetchedResultsController(managedObjectContext: context.managedObjectContext)
self.homeTimelineNavigationBarTitleViewModel = HomeTimelineNavigationBarTitleViewModel(context: context)
super.init()
fetchedResultsController.predicate = Feed.predicate(
kind: .home,
acct: .mastodon(domain: authContext.mastodonAuthenticationBox.domain, userID: authContext.mastodonAuthenticationBox.userID)
)
// fetchedResultsController.predicate = Feed.predicate(
// kind: .home,
// acct: .mastodon(domain: authContext.mastodonAuthenticationBox.domain, userID: authContext.mastodonAuthenticationBox.userID)
// )
Task {
records = try await context.apiService.homeTimeline(authenticationBox: authContext.mastodonAuthenticationBox)
.value
}
homeTimelineNeedRefresh
.sink { [weak self] _ in
@ -116,7 +121,15 @@ extension HomeTimelineViewModel {
extension HomeTimelineViewModel {
func timelineDidReachEnd() {
fetchedResultsController.fetchNextBatch()
// fetchedResultsController.fetchNextBatch()
Task {
let newRecords = try await context.apiService.homeTimeline(
sinceID: records.last?.id,
authenticationBox: authContext.mastodonAuthenticationBox
).value
records += newRecords
}
}
}
@ -124,29 +137,29 @@ extension HomeTimelineViewModel {
// load timeline gap
func loadMore(item: StatusItem) async {
guard case let .feedLoader(record) = item else { return }
guard case let .feedLoader(record) = item, let status = record.status else { return }
guard let diffableDataSource = diffableDataSource else { return }
var snapshot = diffableDataSource.snapshot()
let managedObjectContext = context.managedObjectContext
let key = "LoadMore@\(record.objectID)"
guard let feed = record.object(in: managedObjectContext) else { return }
guard let status = feed.status else { return }
// let managedObjectContext = context.managedObjectContext
let key = "LoadMore@\(status.id)"
// keep transient property live
managedObjectContext.cache(feed, key: key)
defer {
managedObjectContext.cache(nil, key: key)
}
do {
// update state
try await managedObjectContext.performChanges {
feed.update(isLoadingMore: true)
}
} catch {
assertionFailure(error.localizedDescription)
}
// guard let feed = record.object(in: managedObjectContext) else { return }
// guard let status = feed.status else { return }
// keep transient property live
// managedObjectContext.cache(feed, key: key)
// defer {
// managedObjectContext.cache(nil, key: key)
// }
// do {
// // update state
// try await managedObjectContext.performChanges {
// feed.update(isLoadingMore: true)
// }
// } catch {
// assertionFailure(error.localizedDescription)
// }
// reconfigure item
snapshot.reconfigureItems([item])
@ -160,14 +173,14 @@ extension HomeTimelineViewModel {
authenticationBox: authContext.mastodonAuthenticationBox
)
} catch {
do {
// restore state
try await managedObjectContext.performChanges {
feed.update(isLoadingMore: false)
}
} catch {
assertionFailure(error.localizedDescription)
}
// do {
// // restore state
// try await managedObjectContext.performChanges {
// feed.update(isLoadingMore: false)
// }
// } catch {
// assertionFailure(error.localizedDescription)
// }
}
// reconfigure item again

View File

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

View File

@ -21,16 +21,13 @@ extension NotificationTimelineViewController: DataSourceProvider {
switch item {
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))
let item: DataSourceItem? = {
if let notification = record.notification {
return .notification(record: notification)
} else {
return nil
}
}
}()
return item
default:
return nil

View File

@ -280,14 +280,13 @@ extension NotificationTimelineViewController: TableViewControllerNavigateable {
Task { @MainActor in
switch item {
case .feed(let record):
guard let feed = record.object(in: self.context.managedObjectContext) else { return }
guard let notification = feed.notification else { return }
guard let notification = record.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
$records
.receive(on: DispatchQueue.main)
.sink { [weak self] records in
guard let self = self else { return }
@ -40,7 +40,7 @@ extension NotificationTimelineViewModel {
let oldSnapshot = diffableDataSource.snapshot()
var newSnapshot: NSDiffableDataSourceSnapshot<NotificationSection, NotificationItem> = {
let newItems = records.map { record in
NotificationItem.feed(record: record)
NotificationItem.feed(record: .init(status: nil, notification: record, hasMore: false, isLoadingMore: false))
}
var snapshot = NSDiffableDataSourceSnapshot<NotificationSection, NotificationItem>()
snapshot.appendSections([.main])
@ -48,36 +48,37 @@ 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)
}
}
}
#warning("Code below needs to be re-implemented")
// 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 {

View File

@ -5,7 +5,6 @@
// Created by MainasuK on 2022-1-21.
//
import CoreDataStack
import Foundation
import GameplayKit
import MastodonSDK
@ -32,7 +31,7 @@ extension NotificationTimelineViewModel.LoadOldestState {
class Initial: NotificationTimelineViewModel.LoadOldestState {
override func isValidNextState(_ stateClass: AnyClass) -> Bool {
guard let viewModel = viewModel else { return false }
guard !viewModel.feedFetchedResultsController.records.isEmpty else { return false }
guard !viewModel.records.isEmpty else { return false }
return stateClass == Loading.self
}
}
@ -47,7 +46,7 @@ extension NotificationTimelineViewModel.LoadOldestState {
guard let viewModel = viewModel, let stateMachine = stateMachine else { return }
guard let lastFeedRecord = viewModel.feedFetchedResultsController.records.last else {
guard let lastFeedRecord = viewModel.records.last else {
stateMachine.enter(Fail.self)
return
}
@ -55,12 +54,7 @@ extension NotificationTimelineViewModel.LoadOldestState {
Task {
let managedObjectContext = viewModel.context.managedObjectContext
let _maxID: Mastodon.Entity.Notification.ID? = try await managedObjectContext.perform {
guard let feed = lastFeedRecord.object(in: managedObjectContext),
let notification = feed.notification
else { return nil }
return notification.id
}
let _maxID: Mastodon.Entity.Notification.ID? = viewModel.records.last?.id
guard let maxID = _maxID else {
await self.enter(state: Fail.self)

View File

@ -7,7 +7,6 @@
import UIKit
import Combine
import CoreDataStack
import GameplayKit
import MastodonSDK
import MastodonCore
@ -20,10 +19,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 records = [Mastodon.Entity.Notification]()
// output
var diffableDataSource: UITableViewDiffableDataSource<NotificationSection, NotificationItem>?
@ -51,10 +51,15 @@ 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(
// feedFetchedResultsController.predicate = NotificationTimelineViewModel.feedPredicate(
// authenticationBox: authContext.mastodonAuthenticationBox,
// scope: scope
// )
loadNotifications(
authenticationBox: authContext.mastodonAuthenticationBox,
scope: scope
)
@ -67,40 +72,55 @@ extension NotificationTimelineViewModel {
typealias Scope = APIService.MastodonNotificationScope
static func feedPredicate(
func loadNotifications(
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
) {
Task {
let notifications = try await context.apiService.notifications(
maxID: nil,
scope: scope,
authenticationBox: authenticationBox
).value
records = notifications
}
}
// 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
// }
}
@ -124,26 +144,26 @@ extension NotificationTimelineViewModel {
// load timeline gap
func loadMore(item: NotificationItem) async {
guard case let .feedLoader(record) = item else { return }
guard case let .feedLoader(record) = item, let notification = record.notification else { return }
let managedObjectContext = context.managedObjectContext
let key = "LoadMore@\(record.objectID)"
// let managedObjectContext = context.managedObjectContext
let key = "LoadMore@\(notification.id)"
// return when already loading state
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 }
// keep transient property live
managedObjectContext.cache(feed, key: key)
defer {
managedObjectContext.cache(nil, key: key)
}
// // return when already loading state
// 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 }
// // keep transient property live
// managedObjectContext.cache(feed, key: key)
// defer {
// managedObjectContext.cache(nil, key: key)
// }
// fetch data
do {
_ = try await context.apiService.notifications(
maxID: maxID,
maxID: notification.id,
scope: scope,
authenticationBox: authContext.mastodonAuthenticationBox
)

View File

@ -32,7 +32,7 @@ extension BookmarkViewModel {
stateMachine.enter(State.Reloading.self)
statusFetchedResultsController.$records
$statuses
.receive(on: DispatchQueue.main)
.sink { [weak self] records in
guard let self = self else { return }

View File

@ -57,7 +57,7 @@ extension BookmarkViewModel.State {
guard let viewModel = viewModel, let stateMachine = stateMachine else { return }
// reset
viewModel.statusFetchedResultsController.statusIDs = []
viewModel.statuses = []
stateMachine.enter(Loading.self)
}
@ -128,10 +128,10 @@ extension BookmarkViewModel.State {
)
var hasNewStatusesAppend = false
var statusIDs = viewModel.statusFetchedResultsController.statusIDs
var modifiedStatuses = viewModel.statuses
for status in response.value {
guard !statusIDs.contains(status.id) else { continue }
statusIDs.append(status.id)
guard !modifiedStatuses.map({ $0.id }).contains(status.id) else { continue }
modifiedStatuses.append(status)
hasNewStatusesAppend = true
}
@ -147,7 +147,7 @@ extension BookmarkViewModel.State {
} else {
await enter(state: NoMore.self)
}
viewModel.statusFetchedResultsController.statusIDs = statusIDs
viewModel.statuses = modifiedStatuses
} catch {
await enter(state: Fail.self)
}

View File

@ -11,6 +11,7 @@ import CoreData
import CoreDataStack
import GameplayKit
import MastodonCore
import MastodonSDK
final class BookmarkViewModel {
@ -20,7 +21,8 @@ final class BookmarkViewModel {
let context: AppContext
let authContext: AuthContext
let statusFetchedResultsController: StatusFetchedResultsController
// let statusFetchedResultsController: StatusFetchedResultsController
@Published var statuses = [Mastodon.Entity.Status]()
let listBatchFetchViewModel = ListBatchFetchViewModel()
// output
@ -41,11 +43,11 @@ final class BookmarkViewModel {
init(context: AppContext, authContext: AuthContext) {
self.context = context
self.authContext = authContext
self.statusFetchedResultsController = StatusFetchedResultsController(
managedObjectContext: context.managedObjectContext,
domain: authContext.mastodonAuthenticationBox.domain,
additionalTweetPredicate: nil
)
// self.statusFetchedResultsController = StatusFetchedResultsController(
// managedObjectContext: context.managedObjectContext,
// domain: authContext.mastodonAuthenticationBox.domain,
// additionalTweetPredicate: nil
// )
}
}

View File

@ -1,17 +0,0 @@
//
// CachedProfileViewModel.swift
// Mastodon
//
// Created by MainasuK Cirno on 2021-3-31.
//
import Foundation
import CoreDataStack
import MastodonCore
final class CachedProfileViewModel: ProfileViewModel {
init(context: AppContext, authContext: AuthContext, mastodonUser: MastodonUser) {
super.init(context: context, authContext: authContext, optionalMastodonUser: mastodonUser)
}
}

View File

@ -20,7 +20,7 @@ extension FamiliarFollowersViewModel {
userTableViewCellDelegate: userTableViewCellDelegate
)
userFetchedResultsController.$records
$records
.receive(on: DispatchQueue.main)
.sink { [weak self] records in
guard let self = self else { return }

View File

@ -17,8 +17,8 @@ final class FamiliarFollowersViewModel {
// input
let context: AppContext
let authContext: AuthContext
let userFetchedResultsController: UserFetchedResultsController
// let userFetchedResultsController: UserFetchedResultsController
@Published var records = [Mastodon.Entity.Account]()
@Published var familiarFollowers: Mastodon.Entity.FamiliarFollowers?
// output
@ -27,19 +27,19 @@ final class FamiliarFollowersViewModel {
init(context: AppContext, authContext: AuthContext) {
self.context = context
self.authContext = authContext
self.userFetchedResultsController = UserFetchedResultsController(
managedObjectContext: context.managedObjectContext,
domain: authContext.mastodonAuthenticationBox.domain,
additionalPredicate: nil
)
// self.userFetchedResultsController = UserFetchedResultsController(
// managedObjectContext: context.managedObjectContext,
// domain: authContext.mastodonAuthenticationBox.domain,
// additionalPredicate: nil
// )
// end init
$familiarFollowers
.map { familiarFollowers -> [MastodonUser.ID] in
.map { familiarFollowers in
guard let familiarFollowers = familiarFollowers else { return [] }
return familiarFollowers.accounts.map { $0.id }
return familiarFollowers.accounts
}
.assign(to: \.userIDs, on: userFetchedResultsController)
.assign(to: \.records, on: self)
.store(in: &disposeBag)
}

View File

@ -32,7 +32,7 @@ extension FavoriteViewModel {
stateMachine.enter(State.Reloading.self)
statusFetchedResultsController.$records
$records
.receive(on: DispatchQueue.main)
.sink { [weak self] records in
guard let self = self else { return }

View File

@ -56,7 +56,7 @@ extension FavoriteViewModel.State {
guard let viewModel = viewModel, let stateMachine = stateMachine else { return }
// reset
viewModel.statusFetchedResultsController.statusIDs = []
viewModel.records = []
stateMachine.enter(Loading.self)
}
@ -127,10 +127,10 @@ extension FavoriteViewModel.State {
)
var hasNewStatusesAppend = false
var statusIDs = viewModel.statusFetchedResultsController.statusIDs
var newRecords = viewModel.records
for status in response.value {
guard !statusIDs.contains(status.id) else { continue }
statusIDs.append(status.id)
guard !newRecords.contains(where: { $0.id == status.id }) else { continue }
newRecords.append(status)
hasNewStatusesAppend = true
}
@ -146,7 +146,7 @@ extension FavoriteViewModel.State {
} else {
await enter(state: NoMore.self)
}
viewModel.statusFetchedResultsController.statusIDs = statusIDs
viewModel.records = newRecords
} catch {
await enter(state: Fail.self)
}

View File

@ -7,10 +7,9 @@
import UIKit
import Combine
import CoreData
import CoreDataStack
import GameplayKit
import MastodonCore
import MastodonSDK
final class FavoriteViewModel {
@ -19,9 +18,9 @@ final class FavoriteViewModel {
// input
let context: AppContext
let authContext: AuthContext
let statusFetchedResultsController: StatusFetchedResultsController
let listBatchFetchViewModel = ListBatchFetchViewModel()
@Published var records = [Mastodon.Entity.Status]()
// output
var diffableDataSource: UITableViewDiffableDataSource<StatusSection, StatusItem>?
private(set) lazy var stateMachine: GKStateMachine = {
@ -40,11 +39,6 @@ final class FavoriteViewModel {
init(context: AppContext, authContext: AuthContext) {
self.context = context
self.authContext = authContext
self.statusFetchedResultsController = StatusFetchedResultsController(
managedObjectContext: context.managedObjectContext,
domain: authContext.mastodonAuthenticationBox.domain,
additionalTweetPredicate: nil
)
}
}

View File

@ -28,7 +28,7 @@ extension FollowerListViewModel {
snapshot.appendItems([.bottomLoader], toSection: .main)
diffableDataSource?.applySnapshotUsingReloadData(snapshot, completion: nil)
userFetchedResultsController.$records
$records
.receive(on: DispatchQueue.main)
.sink { [weak self] records in
guard let self = self else { return }

View File

@ -61,7 +61,7 @@ extension FollowerListViewModel.State {
guard let viewModel = viewModel, let stateMachine = stateMachine else { return }
// reset
viewModel.userFetchedResultsController.userIDs = []
viewModel.records = []
stateMachine.enter(Loading.self)
}
@ -139,10 +139,10 @@ extension FollowerListViewModel.State {
)
var hasNewAppend = false
var userIDs = viewModel.userFetchedResultsController.userIDs
var newRecords = viewModel.records
for user in response.value {
guard !userIDs.contains(user.id) else { continue }
userIDs.append(user.id)
guard !newRecords.contains(where: { $0.id == user.id }) else { continue }
newRecords.append(user)
hasNewAppend = true
}
@ -155,7 +155,7 @@ extension FollowerListViewModel.State {
}
self.maxID = maxID
viewModel.userFetchedResultsController.userIDs = userIDs
viewModel.records = newRecords
} catch {
await enter(state: Fail.self)

View File

@ -19,7 +19,8 @@ final class FollowerListViewModel {
// input
let context: AppContext
let authContext: AuthContext
let userFetchedResultsController: UserFetchedResultsController
// let userFetchedResultsController: UserFetchedResultsController
@Published var records = [Mastodon.Entity.Account]()
let listBatchFetchViewModel = ListBatchFetchViewModel()
@Published var domain: String?
@ -48,11 +49,11 @@ final class FollowerListViewModel {
) {
self.context = context
self.authContext = authContext
self.userFetchedResultsController = UserFetchedResultsController(
managedObjectContext: context.managedObjectContext,
domain: domain,
additionalPredicate: nil
)
// self.userFetchedResultsController = UserFetchedResultsController(
// managedObjectContext: context.managedObjectContext,
// domain: domain,
// additionalPredicate: nil
// )
self.domain = domain
self.userID = userID
// end init

View File

@ -15,11 +15,10 @@ import MastodonSDK
final class MeProfileViewModel: ProfileViewModel {
init(context: AppContext, authContext: AuthContext) {
let user = authContext.mastodonAuthenticationBox.authentication.user(in: context.managedObjectContext)
super.init(
context: context,
authContext: authContext,
optionalMastodonUser: user
optionalMastodonUser: authContext.mastodonAuthenticationBox.inMemoryCache.meAccount
)
$me
@ -36,17 +35,7 @@ final class MeProfileViewModel: ProfileViewModel {
Task {
do {
_ = try await context.apiService.authenticatedUserInfo(authenticationBox: authContext.mastodonAuthenticationBox).value
try await context.managedObjectContext.performChanges {
guard let me = self.authContext.mastodonAuthenticationBox.authentication.user(in: self.context.managedObjectContext) else {
assertionFailure()
return
}
self.me = me
}
self.me = try await context.apiService.authenticatedUserInfo(authenticationBox: authContext.mastodonAuthenticationBox).value
} catch {
// do nothing?
}

View File

@ -7,7 +7,6 @@
import UIKit
import Combine
import CoreDataStack
import MastodonSDK
import MastodonMeta
import MastodonAsset
@ -33,8 +32,8 @@ class ProfileViewModel: NSObject {
// input
let context: AppContext
let authContext: AuthContext
@Published var me: MastodonUser?
@Published var user: MastodonUser?
@Published var me: Mastodon.Entity.Account?
@Published var user: Mastodon.Entity.Account?
let viewDidAppear = PassthroughSubject<Void, Never>()
@ -56,7 +55,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: Mastodon.Entity.Account?) {
self.context = context
self.authContext = authContext
self.user = mastodonUser
@ -82,7 +81,9 @@ class ProfileViewModel: NSObject {
super.init()
// bind me
self.me = authContext.mastodonAuthenticationBox.authentication.user(in: context.managedObjectContext)
relationshipViewModel.me = authContext.mastodonAuthenticationBox.inMemoryCache.meAccount
$me
.assign(to: \.me, on: relationshipViewModel)
.store(in: &disposeBag)
@ -91,7 +92,8 @@ class ProfileViewModel: NSObject {
$user
.map { user -> UserIdentifier? in
guard let user = user else { return nil }
return MastodonUserIdentifier(domain: user.domain, userID: user.id)
#warning("fix domain!")
return MastodonUserIdentifier(domain: user.domain!, userID: user.id)
}
.assign(to: &$userIdentifier)
$user
@ -122,14 +124,11 @@ class ProfileViewModel: NSObject {
.store(in: &disposeBag)
// query relationship
let userRecord = $user.map { user -> ManagedObjectRecord<MastodonUser>? in
user.flatMap { ManagedObjectRecord<MastodonUser>(objectID: $0.objectID) }
}
let pendingRetryPublisher = CurrentValueSubject<TimeInterval, Never>(1)
// observe friendship
Publishers.CombineLatest(
userRecord,
$user,
pendingRetryPublisher
)
.sink { [weak self] userRecord, _ in
@ -178,18 +177,17 @@ class ProfileViewModel: NSObject {
// fetch profile info before edit
func fetchEditProfileInfo() -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Account>, Error> {
guard let me = me,
let mastodonAuthentication = me.mastodonAuthentication
guard let me = me
else {
return Fail(error: APIService.APIError.implicit(.authenticationMissing)).eraseToAnyPublisher()
}
let authorization = Mastodon.API.OAuth.Authorization(accessToken: mastodonAuthentication.userAccessToken)
return context.apiService.accountVerifyCredentials(domain: me.domain, authorization: authorization)
let authorization = Mastodon.API.OAuth.Authorization(accessToken: authContext.mastodonAuthenticationBox.userAuthorization.accessToken)
return context.apiService.accountVerifyCredentials(domain: authContext.mastodonAuthenticationBox.domain, authorization: authorization)
}
private func updateRelationship(
record: ManagedObjectRecord<MastodonUser>,
record: Mastodon.Entity.Account,
authenticationBox: MastodonAuthenticationBox
) async throws -> Mastodon.Response.Content<[Mastodon.Entity.Relationship]> {
let response = try await context.apiService.relationship(

View File

@ -38,15 +38,7 @@ final class RemoteProfileViewModel: ProfileViewModel {
}
} receiveValue: { [weak self] response in
guard let self = self else { return }
let managedObjectContext = context.managedObjectContext
let request = MastodonUser.sortedFetchRequest
request.fetchLimit = 1
request.predicate = MastodonUser.predicate(domain: domain, id: response.value.id)
guard let mastodonUser = managedObjectContext.safeFetch(request).first else {
assertionFailure()
return
}
self.user = mastodonUser
self.user = response.value
}
.store(in: &disposeBag)
}
@ -59,33 +51,8 @@ final class RemoteProfileViewModel: ProfileViewModel {
notificationID: notificationID,
authenticationBox: authContext.mastodonAuthenticationBox
)
let userID = response.value.account.id
let _user: MastodonUser? = try await context.managedObjectContext.perform {
let request = MastodonUser.sortedFetchRequest
request.predicate = MastodonUser.predicate(domain: authContext.mastodonAuthenticationBox.domain, id: userID)
request.fetchLimit = 1
return context.managedObjectContext.safeFetch(request).first
}
if let user = _user {
self.user = user
} else {
_ = try await context.apiService.accountInfo(
domain: authContext.mastodonAuthenticationBox.domain,
userID: userID,
authorization: authContext.mastodonAuthenticationBox.userAuthorization
)
let _user: MastodonUser? = try await context.managedObjectContext.perform {
let request = MastodonUser.sortedFetchRequest
request.predicate = MastodonUser.predicate(domain: authContext.mastodonAuthenticationBox.domain, id: userID)
request.fetchLimit = 1
return context.managedObjectContext.safeFetch(request).first
}
self.user = _user
}
self.user = response.value.account
} // end Task
}
@ -114,15 +81,7 @@ final class RemoteProfileViewModel: ProfileViewModel {
}
} receiveValue: { [weak self] response in
guard let self = self, let value = response.value else { return }
let managedObjectContext = context.managedObjectContext
let request = MastodonUser.sortedFetchRequest
request.fetchLimit = 1
request.predicate = MastodonUser.predicate(domain: domain, id: value.id)
guard let mastodonUser = managedObjectContext.safeFetch(request).first else {
assertionFailure()
return
}
self.user = mastodonUser
self.user = value
}
.store(in: &disposeBag)
}

View File

@ -48,7 +48,7 @@ extension UserTimelineViewModel {
).map { $0 || $1 || $2 }
Publishers.CombineLatest(
statusFetchedResultsController.$records,
$records,
needsTimelineHidden.removeDuplicates()
)
.debounce(for: .milliseconds(300), scheduler: DispatchQueue.main)

View File

@ -56,7 +56,7 @@ extension UserTimelineViewModel.State {
guard let viewModel = viewModel, let stateMachine = stateMachine else { return }
// reset
viewModel.statusFetchedResultsController.statusIDs = []
viewModel.records = []
stateMachine.enter(Loading.self)
}
@ -112,7 +112,7 @@ extension UserTimelineViewModel.State {
super.didEnter(from: previousState)
guard let viewModel = viewModel, let stateMachine = stateMachine else { return }
let maxID = viewModel.statusFetchedResultsController.statusIDs.last
let maxID = viewModel.records.last?.id
guard let userID = viewModel.userIdentifier?.userID, !userID.isEmpty else {
stateMachine.enter(Fail.self)
@ -135,10 +135,10 @@ extension UserTimelineViewModel.State {
)
var hasNewStatusesAppend = false
var statusIDs = viewModel.statusFetchedResultsController.statusIDs
var newRecords = viewModel.records
for status in response.value {
guard !statusIDs.contains(status.id) else { continue }
statusIDs.append(status.id)
guard !newRecords.contains(where: { $0.id == status.id }) else { continue }
newRecords.append(status)
hasNewStatusesAppend = true
}
@ -147,7 +147,7 @@ extension UserTimelineViewModel.State {
} else {
await enter(state: NoMore.self)
}
viewModel.statusFetchedResultsController.statusIDs = statusIDs
viewModel.records = newRecords
} catch {
await enter(state: Fail.self)

View File

@ -21,7 +21,7 @@ final class UserTimelineViewModel {
let context: AppContext
let authContext: AuthContext
let title: String
let statusFetchedResultsController: StatusFetchedResultsController
// let statusFetchedResultsController: StatusFetchedResultsController
let listBatchFetchViewModel = ListBatchFetchViewModel()
@Published var userIdentifier: UserIdentifier?
@Published var queryFilter: QueryFilter
@ -32,7 +32,8 @@ final class UserTimelineViewModel {
// let userDisplayName = CurrentValueSubject<String?, Never>(nil) // for suspended prompt label
// var dataSourceDidUpdate = PassthroughSubject<Void, Never>()
@Published var records = [Mastodon.Entity.Status]()
// output
var diffableDataSource: UITableViewDiffableDataSource<StatusSection, StatusItem>?
private(set) lazy var stateMachine: GKStateMachine = {
@ -57,11 +58,11 @@ final class UserTimelineViewModel {
self.context = context
self.authContext = authContext
self.title = title
self.statusFetchedResultsController = StatusFetchedResultsController(
managedObjectContext: context.managedObjectContext,
domain: authContext.mastodonAuthenticationBox.domain,
additionalTweetPredicate: nil
)
// self.statusFetchedResultsController = StatusFetchedResultsController(
// managedObjectContext: context.managedObjectContext,
// domain: authContext.mastodonAuthenticationBox.domain,
// additionalTweetPredicate: nil
// )
self.queryFilter = queryFilter
}
}

View File

@ -33,7 +33,7 @@ extension UserListViewModel {
// trigger initial loading
stateMachine.enter(UserListViewModel.State.Reloading.self)
userFetchedResultsController.$records
$records
.receive(on: DispatchQueue.main)
.sink { [weak self] records in
guard let self = self else { return }

View File

@ -55,7 +55,7 @@ extension UserListViewModel.State {
guard let viewModel = viewModel, let stateMachine = stateMachine else { return }
// reset
viewModel.userFetchedResultsController.userIDs = []
viewModel.records = []
stateMachine.enter(Loading.self)
}
@ -140,10 +140,10 @@ extension UserListViewModel.State {
}
var hasNewAppend = false
var userIDs = viewModel.userFetchedResultsController.userIDs
var newRecords = viewModel.records
for user in response.value {
guard !userIDs.contains(user.id) else { continue }
userIDs.append(user.id)
guard !newRecords.contains(where: { $0.id == user.id }) else { continue }
newRecords.append(user)
hasNewAppend = true
}
@ -155,7 +155,7 @@ extension UserListViewModel.State {
await enter(state: NoMore.self)
}
self.maxID = maxID
viewModel.userFetchedResultsController.userIDs = userIDs
viewModel.records = newRecords
} catch {
await enter(state: Fail.self)
@ -179,7 +179,8 @@ extension UserListViewModel.State {
guard let viewModel = viewModel else { return }
// trigger reload
viewModel.userFetchedResultsController.userIDs = viewModel.userFetchedResultsController.userIDs
// viewModel.userFetchedResultsController.userIDs = viewModel.userFetchedResultsController.userIDs
stateMachine?.enter(Loading.self)
}
}
}

View File

@ -7,9 +7,9 @@
import UIKit
import Combine
import CoreDataStack
import GameplayKit
import MastodonCore
import MastodonSDK
final class UserListViewModel {
var disposeBag = Set<AnyCancellable>()
@ -18,9 +18,10 @@ final class UserListViewModel {
let context: AppContext
let authContext: AuthContext
let kind: Kind
let userFetchedResultsController: UserFetchedResultsController
// let userFetchedResultsController: UserFetchedResultsController
let listBatchFetchViewModel = ListBatchFetchViewModel()
@Published var records = [Mastodon.Entity.Account]()
// output
var diffableDataSource: UITableViewDiffableDataSource<UserSection, UserItem>!
@MainActor private(set) lazy var stateMachine: GKStateMachine = {
@ -43,11 +44,11 @@ final class UserListViewModel {
self.context = context
self.authContext = authContext
self.kind = kind
self.userFetchedResultsController = UserFetchedResultsController(
managedObjectContext: context.managedObjectContext,
domain: authContext.mastodonAuthenticationBox.domain,
additionalPredicate: nil
)
// self.userFetchedResultsController = UserFetchedResultsController(
// managedObjectContext: context.managedObjectContext,
// domain: authContext.mastodonAuthenticationBox.domain,
// additionalPredicate: nil
// )
// end init
}
}
@ -55,7 +56,7 @@ final class UserListViewModel {
extension UserListViewModel {
// TODO: refactor follower and following into user list
enum Kind {
case rebloggedBy(status: ManagedObjectRecord<Status>)
case favoritedBy(status: ManagedObjectRecord<Status>)
case rebloggedBy(status: Mastodon.Entity.Status)
case favoritedBy(status: Mastodon.Entity.Status)
}
}

View File

@ -6,8 +6,6 @@
//
import Combine
import CoreData
import CoreDataStack
import Foundation
import GameplayKit
import MastodonSDK
@ -28,8 +26,8 @@ class ReportViewModel {
// input
let context: AppContext
let authContext: AuthContext
let user: ManagedObjectRecord<MastodonUser>
let status: ManagedObjectRecord<Status>?
let user: Mastodon.Entity.Account
let status: Mastodon.Entity.Status?
// output
@Published var isReporting = false
@ -38,8 +36,8 @@ class ReportViewModel {
init(
context: AppContext,
authContext: AuthContext,
user: ManagedObjectRecord<MastodonUser>,
status: ManagedObjectRecord<Status>?
user: Mastodon.Entity.Account,
status: Mastodon.Entity.Status?
) {
self.context = context
self.authContext = authContext
@ -56,16 +54,7 @@ class ReportViewModel {
reportReasonViewModel.headline = L10n.Scene.Report.StepOne.whatsWrongWithThisPost
} else {
Task { @MainActor in
let managedObjectContext = context.managedObjectContext
let _username: String? = try? await managedObjectContext.perform {
let user = user.object(in: managedObjectContext)
return user?.acctWithDomain
}
if let username = _username {
reportReasonViewModel.headline = L10n.Scene.Report.StepOne.whatsWrongWithThisUsername(username)
} else {
reportReasonViewModel.headline = L10n.Scene.Report.StepOne.whatsWrongWithThisAccount
}
reportReasonViewModel.headline = L10n.Scene.Report.StepOne.whatsWrongWithThisUsername(user.acctWithDomain)
} // end Task
}
@ -95,64 +84,56 @@ extension ReportViewModel {
func report() async throws {
guard !isReporting else { return }
let managedObjectContext = context.managedObjectContext
let _query: Mastodon.API.Reports.FileReportQuery? = try await managedObjectContext.perform {
guard let user = self.user.object(in: managedObjectContext) else { return nil }
// the status picker is essential step in report flow
// only check isSkip or not
let statusIDs: [Status.ID]? = {
if self.reportStatusViewModel.isSkip {
let _id: Status.ID? = self.reportStatusViewModel.status.flatMap { record -> Status.ID? in
guard let status = record.object(in: managedObjectContext) else { return nil }
return status.id
}
return _id.flatMap { [$0] }
} else {
return self.reportStatusViewModel.selectStatuses.compactMap { record -> Status.ID? in
guard let status = record.object(in: managedObjectContext) else { return nil }
return status.id
}
// the status picker is essential step in report flow
// only check isSkip or not
let statusIDs: [Mastodon.Entity.Status.ID]? = {
if self.reportStatusViewModel.isSkip {
let _id: Mastodon.Entity.Status.ID? = self.reportStatusViewModel.status.flatMap { record -> Mastodon.Entity.Status.ID? in
return record.id
}
}()
// the user comment is essential step in report flow
// only check isSkip or not
let comment: String? = {
let _comment = self.reportSupplementaryViewModel.isSkip ? nil : self.reportSupplementaryViewModel.commentContext.comment
if let comment = _comment, !comment.isEmpty {
return comment
} else {
return _id.flatMap { [$0] } ?? []
} else {
return self.reportStatusViewModel.selectStatuses.compactMap { record -> Mastodon.Entity.Status.ID? in
return record.id
}
}
}()
// the user comment is essential step in report flow
// only check isSkip or not
let comment: String? = {
let _comment = self.reportSupplementaryViewModel.isSkip ? nil : self.reportSupplementaryViewModel.commentContext.comment
if let comment = _comment, !comment.isEmpty {
return comment
} else {
return nil
}
}()
let query = Mastodon.API.Reports.FileReportQuery(
accountID: user.id,
statusIDs: statusIDs,
comment: comment,
forward: true,
category: {
switch self.reportReasonViewModel.selectReason {
case .dislike: return nil
case .spam: return .spam
case .violateRule: return .violation
case .other: return .other
case .none: return nil
}
}(),
ruleIDs: {
switch self.reportReasonViewModel.selectReason {
case .violateRule:
let ruleIDs = self.reportServerRulesViewModel.selectRules.map { $0.id }.sorted()
return ruleIDs
default:
return nil
}
}()
return Mastodon.API.Reports.FileReportQuery(
accountID: user.id,
statusIDs: statusIDs,
comment: comment,
forward: true,
category: {
switch self.reportReasonViewModel.selectReason {
case .dislike: return nil
case .spam: return .spam
case .violateRule: return .violation
case .other: return .other
case .none: return nil
}
}(),
ruleIDs: {
switch self.reportReasonViewModel.selectReason {
case .violateRule:
let ruleIDs = self.reportServerRulesViewModel.selectRules.map { $0.id }.sorted()
return ruleIDs
default:
return nil
}
}()
)
}
guard let query = _query else { return }
)
do {
isReporting = true

View File

@ -6,8 +6,6 @@
//
import Combine
import CoreData
import CoreDataStack
import Foundation
import MastodonSDK
import UIKit
@ -23,7 +21,7 @@ class ReportResultViewModel: ObservableObject {
// input
let context: AppContext
let authContext: AuthContext
let user: ManagedObjectRecord<MastodonUser>
let user: Mastodon.Entity.Account
let isReported: Bool
var headline: String {
@ -48,7 +46,7 @@ class ReportResultViewModel: ObservableObject {
init(
context: AppContext,
authContext: AuthContext,
user: ManagedObjectRecord<MastodonUser>,
user: Mastodon.Entity.Account,
isReported: Bool
) {
self.context = context
@ -58,10 +56,8 @@ class ReportResultViewModel: ObservableObject {
// end init
Task { @MainActor in
guard let user = user.object(in: context.managedObjectContext) else { return }
guard let me = authContext.mastodonAuthenticationBox.authentication.user(in: context.managedObjectContext) else { return }
self.relationshipViewModel.user = user
self.relationshipViewModel.me = me
self.relationshipViewModel.me = authContext.mastodonAuthenticationBox.inMemoryCache.meAccount
self.avatarURL = user.avatarImageURL()
self.username = user.acctWithDomain

View File

@ -32,7 +32,7 @@ extension ReportStatusViewModel {
snapshot.appendSections([.main])
diffableDataSource?.apply(snapshot)
statusFetchedResultsController.$records
$records
.receive(on: DispatchQueue.main)
.sink { [weak self] records in
guard let self = self else { return }

View File

@ -7,9 +7,8 @@
import func QuartzCore.CACurrentMediaTime
import Foundation
import CoreData
import CoreDataStack
import GameplayKit
import MastodonSDK
extension ReportStatusViewModel {
class State: GKState {
@ -64,14 +63,11 @@ extension ReportStatusViewModel.State {
super.didEnter(from: previousState)
guard let viewModel else { return }
let maxID = viewModel.statusFetchedResultsController.statusIDs.last
let maxID = viewModel.records.last?.id
Task {
let managedObjectContext = viewModel.context.managedObjectContext
let _userID: MastodonUser.ID? = try await managedObjectContext.perform {
guard let user = viewModel.user.object(in: managedObjectContext) else { return nil }
return user.id
}
let _userID: Mastodon.Entity.Account.ID? = viewModel.user.id
guard let userID = _userID else {
await enter(state: Fail.self)
return
@ -89,10 +85,10 @@ extension ReportStatusViewModel.State {
)
var hasNewStatusesAppend = false
var statusIDs = viewModel.statusFetchedResultsController.statusIDs
var newRecords = viewModel.records
for status in response.value {
guard !statusIDs.contains(status.id) else { continue }
statusIDs.append(status.id)
guard !newRecords.contains(where: { $0.id == status.id }) else { continue }
newRecords.append(status)
hasNewStatusesAppend = true
}
@ -101,7 +97,7 @@ extension ReportStatusViewModel.State {
} else {
await enter(state: NoMore.self)
}
viewModel.statusFetchedResultsController.statusIDs = statusIDs
viewModel.records = newRecords
} catch {
await enter(state: Fail.self)

View File

@ -6,8 +6,6 @@
//
import Combine
import CoreData
import CoreDataStack
import Foundation
import GameplayKit
import MastodonSDK
@ -24,14 +22,15 @@ class ReportStatusViewModel {
// input
let context: AppContext
let authContext: AuthContext
let user: ManagedObjectRecord<MastodonUser>
let status: ManagedObjectRecord<Status>?
let statusFetchedResultsController: StatusFetchedResultsController
let user: Mastodon.Entity.Account
let status: Mastodon.Entity.Status?
// let statusFetchedResultsController: StatusFetchedResultsController
let listBatchFetchViewModel = ListBatchFetchViewModel()
@Published var isSkip = false
@Published var selectStatuses = OrderedSet<ManagedObjectRecord<Status>>()
@Published var selectStatuses = OrderedSet<Mastodon.Entity.Status>()
@Published var records = [Mastodon.Entity.Status]()
// output
var diffableDataSource: UITableViewDiffableDataSource<ReportSection, ReportItem>?
private(set) lazy var stateMachine: GKStateMachine = {
@ -51,18 +50,18 @@ class ReportStatusViewModel {
init(
context: AppContext,
authContext: AuthContext,
user: ManagedObjectRecord<MastodonUser>,
status: ManagedObjectRecord<Status>?
user: Mastodon.Entity.Account,
status: Mastodon.Entity.Status?
) {
self.context = context
self.authContext = authContext
self.user = user
self.status = status
self.statusFetchedResultsController = StatusFetchedResultsController(
managedObjectContext: context.managedObjectContext,
domain: authContext.mastodonAuthenticationBox.domain,
additionalTweetPredicate: nil
)
// self.statusFetchedResultsController = StatusFetchedResultsController(
// managedObjectContext: context.managedObjectContext,
// domain: authContext.mastodonAuthenticationBox.domain,
// additionalTweetPredicate: nil
// )
// end init
if let status = status {

View File

@ -7,7 +7,6 @@
import UIKit
import Combine
import CoreDataStack
import MastodonCore
import MastodonSDK
@ -18,7 +17,7 @@ class ReportSupplementaryViewModel {
// Input
let context: AppContext
let authContext: AuthContext
let user: ManagedObjectRecord<MastodonUser>
let user: Mastodon.Entity.Account
let commentContext = ReportItem.CommentContext()
@Published var isSkip = false
@ -31,7 +30,7 @@ class ReportSupplementaryViewModel {
init(
context: AppContext,
authContext: AuthContext,
user: ManagedObjectRecord<MastodonUser>
user: Mastodon.Entity.Account
) {
self.context = context
self.authContext = authContext

View File

@ -6,13 +6,13 @@
//
import UIKit
import CoreDataStack
import MastodonSDK
extension ReportStatusTableViewCell {
final class ViewModel {
let value: Status
let value: Mastodon.Entity.Status
init(value: Status) {
init(value: Mastodon.Entity.Status) {
self.value = value
}
}

View File

@ -75,22 +75,10 @@ extension SearchResultOverviewCoordinator: SearchResultsOverviewTableViewControl
showProfile(viewController, for: account)
} else if let status = searchResult.statuses.first {
let status = try await managedObjectContext.perform {
return Persistence.Status.fetch(in: managedObjectContext, context: Persistence.Status.PersistContext(
domain: authContext.mastodonAuthenticationBox.domain,
entity: status,
me: authContext.mastodonAuthenticationBox.authentication.user(in: managedObjectContext),
statusCache: nil,
userCache: nil,
networkDate: Date()))
}
guard let status else { return }
await DataSourceFacade.coordinateToStatusThreadScene(
provider: viewController,
target: .status, // remove reblog wrapper
status: status.asRecord
status: status
)
} else if let url = URL(string: urlString) {
let prefixedURL: URL?
@ -111,27 +99,12 @@ extension SearchResultOverviewCoordinator: SearchResultsOverviewTableViewControl
}
func showProfile(_ viewController: SearchResultsOverviewTableViewController, for account: Mastodon.Entity.Account) {
let managedObjectContext = context.managedObjectContext
let domain = authContext.mastodonAuthenticationBox.domain
Task {
let user = try await managedObjectContext.perform {
return Persistence.MastodonUser.fetch(in: managedObjectContext,
context: Persistence.MastodonUser.PersistContext(
domain: domain,
entity: account,
cache: nil,
networkDate: Date()
))
}
await DataSourceFacade.coordinateToProfileScene(provider: viewController,
user: account)
if let user {
await DataSourceFacade.coordinateToProfileScene(provider: viewController,
user: user.asRecord)
await DataSourceFacade.responseToCreateSearchHistory(provider: viewController,
item: .user(record: user.asRecord))
}
await DataSourceFacade.responseToCreateSearchHistory(provider: viewController,
item: .user(record: account))
}
}

View File

@ -6,10 +6,9 @@
//
import Foundation
import CoreData
import CoreDataStack
import MastodonSDK
enum SearchHistoryItem: Hashable {
case hashtag(ManagedObjectRecord<Tag>)
case user(ManagedObjectRecord<MastodonUser>)
case hashtag(Mastodon.Entity.Tag)
case user(Mastodon.Entity.Account)
}

View File

@ -24,7 +24,7 @@ extension SearchHistoryViewController: DataSourceProvider {
case .user(let record):
return .user(record: record)
case .hashtag(let record):
return .hashtag(tag: .record(record))
return .hashtag(tag: .entity(record))
}
}

View File

@ -27,7 +27,7 @@ extension SearchHistoryViewModel {
snapshot.appendSections([.main])
diffableDataSource?.apply(snapshot, animatingDifferences: false)
searchHistoryFetchedResultController.$records
$records
.receive(on: DispatchQueue.main)
.sink { [weak self] records in
guard let self = self else { return }
@ -35,23 +35,24 @@ extension SearchHistoryViewModel {
Task {
do {
let managedObjectContext = self.context.managedObjectContext
let items: [SearchHistoryItem] = try await managedObjectContext.perform {
var items: [SearchHistoryItem] = []
// let managedObjectContext = self.context.managedObjectContext
// let items: [SearchHistoryItem] = try await managedObjectContext.perform {
// var items: [SearchHistoryItem] = []
//
// for record in records {
// guard let searchHistory = record.object(in: managedObjectContext) else { continue }
// if let user = searchHistory.account {
// items.append(.user(.init(objectID: user.objectID)))
// } else if let hashtag = searchHistory.hashtag {
// items.append(.hashtag(.init(objectID: hashtag.objectID)))
// }
// }
//
// return items
// }
for record in records {
guard let searchHistory = record.object(in: managedObjectContext) else { continue }
if let user = searchHistory.account {
items.append(.user(.init(objectID: user.objectID)))
} else if let hashtag = searchHistory.hashtag {
items.append(.hashtag(.init(objectID: hashtag.objectID)))
}
}
return items
}
let mostRecentItems = Array(items.prefix(10))
#warning("Reimplement storing and recovering search history")
let mostRecentItems = [SearchHistoryItem]() //Array(items.prefix(10))
var snapshot = NSDiffableDataSourceSnapshot<SearchHistorySection, SearchHistoryItem>()
snapshot.appendSections([.main])
snapshot.appendItems(mostRecentItems, toSection: .main)

View File

@ -7,8 +7,12 @@
import UIKit
import Combine
import CoreDataStack
import MastodonCore
import MastodonSDK
struct SearchHistoryQueryItem {
}
final class SearchHistoryViewModel {
var disposeBag = Set<AnyCancellable>()
@ -16,18 +20,13 @@ final class SearchHistoryViewModel {
// input
let context: AppContext
let authContext: AuthContext
let searchHistoryFetchedResultController: SearchHistoryFetchedResultController
@Published var records = [SearchHistoryQueryItem]()
// output
var diffableDataSource: UICollectionViewDiffableDataSource<SearchHistorySection, SearchHistoryItem>?
init(context: AppContext, authContext: AuthContext) {
self.context = context
self.authContext = authContext
self.searchHistoryFetchedResultController = SearchHistoryFetchedResultController(managedObjectContext: context.managedObjectContext)
searchHistoryFetchedResultController.domain.value = authContext.mastodonAuthenticationBox.domain
searchHistoryFetchedResultController.userID.value = authContext.mastodonAuthenticationBox.userID
}
}

View File

@ -6,13 +6,11 @@
//
import Foundation
import CoreData
import CoreDataStack
import MastodonSDK
enum SearchResultItem: Hashable {
case user(ManagedObjectRecord<MastodonUser>)
case status(ManagedObjectRecord<Status>)
case user(Mastodon.Entity.Account)
case status(Mastodon.Entity.Status)
case hashtag(tag: Mastodon.Entity.Tag)
case bottomLoader(attribute: BottomLoaderAttribute)
}

View File

@ -32,8 +32,8 @@ extension SearchResultViewModel {
diffableDataSource.apply(snapshot, animatingDifferences: false)
Publishers.CombineLatest3(
statusFetchedResultsController.$records,
userFetchedResultsController.$records,
$statusRecords,
$userRecords,
$hashtags
)
.map { statusRecords, userRecords, hashtags in

View File

@ -7,8 +7,6 @@
import Foundation
import Combine
import CoreData
import CoreDataStack
import GameplayKit
import MastodonSDK
import MastodonCore
@ -22,8 +20,11 @@ final class SearchResultViewModel {
let searchScope: SearchScope
let searchText: String
@Published var hashtags: [Mastodon.Entity.Tag] = []
let userFetchedResultsController: UserFetchedResultsController
let statusFetchedResultsController: StatusFetchedResultsController
@Published var userRecords = [Mastodon.Entity.Account]()
@Published var statusRecords = [Mastodon.Entity.Status]()
// let userFetchedResultsController: UserFetchedResultsController
// let statusFetchedResultsController: StatusFetchedResultsController
let listBatchFetchViewModel = ListBatchFetchViewModel()
var cellFrameCache = NSCache<NSNumber, NSValue>()
@ -51,15 +52,15 @@ final class SearchResultViewModel {
self.searchScope = searchScope
self.searchText = searchText
self.userFetchedResultsController = UserFetchedResultsController(
managedObjectContext: context.managedObjectContext,
domain: authContext.mastodonAuthenticationBox.domain,
additionalPredicate: nil
)
self.statusFetchedResultsController = StatusFetchedResultsController(
managedObjectContext: context.managedObjectContext,
domain: authContext.mastodonAuthenticationBox.domain,
additionalTweetPredicate: nil
)
// self.userFetchedResultsController = UserFetchedResultsController(
// managedObjectContext: context.managedObjectContext,
// domain: authContext.mastodonAuthenticationBox.domain,
// additionalPredicate: nil
// )
// self.statusFetchedResultsController = StatusFetchedResultsController(
// managedObjectContext: context.managedObjectContext,
// domain: authContext.mastodonAuthenticationBox.domain,
// additionalTweetPredicate: nil
// )
}
}

View File

@ -8,17 +8,16 @@
import UIKit
import Combine
import MastodonUI
import CoreDataStack
import MetaTextKit
import MastodonMeta
import Meta
import MastodonAsset
import MastodonCore
import MastodonLocalization
import class CoreDataStack.Notification
import MastodonSDK
extension NotificationView {
public func configure(feed: Feed) {
public func configure(feed: FeedItem) {
guard let notification = feed.notification else {
assertionFailure()
return
@ -29,17 +28,10 @@ extension NotificationView {
}
extension NotificationView {
public func configure(notification: Notification) {
viewModel.objects.insert(notification)
public func configure(notification: Mastodon.Entity.Notification) {
configureAuthor(notification: notification)
guard let type = MastodonNotificationType(rawValue: notification.typeRaw) else {
assertionFailure()
return
}
switch type {
switch notification.type {
case .follow:
setAuthorContainerBottomPaddingViewDisplay()
case .followRequest:
@ -63,167 +55,125 @@ extension NotificationView {
}
extension NotificationView {
private func configureAuthor(notification: Notification) {
private func configureAuthor(notification: Mastodon.Entity.Notification) {
let author = notification.account
// author avatar
Publishers.CombineLatest(
author.publisher(for: \.avatar),
UserDefaults.shared.publisher(for: \.preferredStaticAvatar)
)
.map { _ in author.avatarImageURL() }
.assign(to: \.authorAvatarImageURL, on: viewModel)
.store(in: &disposeBag)
viewModel.authorAvatarImageURL = author.avatarImageURL()
// author name
Publishers.CombineLatest(
author.publisher(for: \.displayName),
author.publisher(for: \.emojis)
)
.map { _, emojis in
viewModel.authorName = {
do {
let content = MastodonContent(content: author.displayNameWithFallback, emojis: emojis.asDictionary)
let content = MastodonContent(content: author.displayNameWithFallback, emojis: author.emojis?.asDictionary ?? [:])
let metaContent = try MastodonMetaContent.convert(document: content)
return metaContent
} catch {
assertionFailure(error.localizedDescription)
return PlaintextMetaContent(string: author.displayNameWithFallback)
}
}
.assign(to: \.authorName, on: viewModel)
.store(in: &disposeBag)
}()
// author username
author.publisher(for: \.acct)
.map { $0 as String? }
.assign(to: \.authorUsername, on: viewModel)
.store(in: &disposeBag)
viewModel.authorUsername = author.acct
// timestamp
viewModel.timestamp = notification.createAt
viewModel.timestamp = notification.createdAt
viewModel.visibility = notification.status?.visibility ?? ._other("")
viewModel.visibility = notification.status?.mastodonVisibility ?? ._other("")
// notification type indicator
Publishers.CombineLatest3(
notification.publisher(for: \.typeRaw),
author.publisher(for: \.displayName),
author.publisher(for: \.emojis)
)
.sink { [weak self] typeRaw, _, emojis in
guard let self = self else { return }
guard let type = MastodonNotificationType(rawValue: typeRaw) else {
self.viewModel.notificationIndicatorText = nil
return
}
self.viewModel.type = type
self.viewModel.type = notification.type
func createMetaContent(text: String, emojis: MastodonContent.Emojis) -> MetaContent {
let content = MastodonContent(content: text, emojis: emojis)
guard let metaContent = try? MastodonMetaContent.convert(document: content) else {
return PlaintextMetaContent(string: text)
}
return metaContent
}
// TODO: fix the i18n. The subject should assert place at the string beginning
switch type {
case .follow:
self.viewModel.notificationIndicatorText = createMetaContent(
text: L10n.Scene.Notification.NotificationDescription.followedYou,
emojis: emojis.asDictionary
)
case .followRequest:
self.viewModel.notificationIndicatorText = createMetaContent(
text: L10n.Scene.Notification.NotificationDescription.requestToFollowYou,
emojis: emojis.asDictionary
)
case .mention:
self.viewModel.notificationIndicatorText = createMetaContent(
text: L10n.Scene.Notification.NotificationDescription.mentionedYou,
emojis: emojis.asDictionary
)
case .reblog:
self.viewModel.notificationIndicatorText = createMetaContent(
text: L10n.Scene.Notification.NotificationDescription.rebloggedYourPost,
emojis: emojis.asDictionary
)
case .favourite:
self.viewModel.notificationIndicatorText = createMetaContent(
text: L10n.Scene.Notification.NotificationDescription.favoritedYourPost,
emojis: emojis.asDictionary
)
case .poll:
self.viewModel.notificationIndicatorText = createMetaContent(
text: L10n.Scene.Notification.NotificationDescription.pollHasEnded,
emojis: emojis.asDictionary
)
case .status:
self.viewModel.notificationIndicatorText = createMetaContent(
text: .empty,
emojis: emojis.asDictionary
)
case ._other:
self.viewModel.notificationIndicatorText = nil
func createMetaContent(text: String, emojis: MastodonContent.Emojis) -> MetaContent {
let content = MastodonContent(content: text, emojis: emojis)
guard let metaContent = try? MastodonMetaContent.convert(document: content) else {
return PlaintextMetaContent(string: text)
}
return metaContent
}
.store(in: &disposeBag)
// TODO: fix the i18n. The subject should assert place at the string beginning
guard let type = viewModel.type else { return }
let authContext = viewModel.authContext
// isMuting
author.publisher(for: \.mutingBy)
.map { mutingBy in
guard let authContext = authContext else { return false }
return mutingBy.contains(where: {
$0.id == authContext.mastodonAuthenticationBox.userID
&& $0.domain == authContext.mastodonAuthenticationBox.domain
})
switch type {
case .follow:
self.viewModel.notificationIndicatorText = createMetaContent(
text: L10n.Scene.Notification.NotificationDescription.followedYou,
emojis: author.emojis?.asDictionary ?? [:]
)
case .followRequest:
self.viewModel.notificationIndicatorText = createMetaContent(
text: L10n.Scene.Notification.NotificationDescription.requestToFollowYou,
emojis: author.emojis?.asDictionary ?? [:]
)
case .mention:
self.viewModel.notificationIndicatorText = createMetaContent(
text: L10n.Scene.Notification.NotificationDescription.mentionedYou,
emojis: author.emojis?.asDictionary ?? [:]
)
case .reblog:
self.viewModel.notificationIndicatorText = createMetaContent(
text: L10n.Scene.Notification.NotificationDescription.rebloggedYourPost,
emojis: author.emojis?.asDictionary ?? [:]
)
case .favourite:
self.viewModel.notificationIndicatorText = createMetaContent(
text: L10n.Scene.Notification.NotificationDescription.favoritedYourPost,
emojis: author.emojis?.asDictionary ?? [:]
)
case .poll:
self.viewModel.notificationIndicatorText = createMetaContent(
text: L10n.Scene.Notification.NotificationDescription.pollHasEnded,
emojis: author.emojis?.asDictionary ?? [:]
)
case .status:
self.viewModel.notificationIndicatorText = createMetaContent(
text: .empty,
emojis: author.emojis?.asDictionary ?? [:]
)
case ._other:
self.viewModel.notificationIndicatorText = nil
}
guard let authContext = viewModel.authContext else { return }
Task {
guard let context = viewModel.context else { return }
if let relationship = try await context.apiService.relationship(records: [author], authenticationBox: authContext.mastodonAuthenticationBox).value.first {
viewModel.isMuting = relationship.muting == true
viewModel.isBlocking = relationship.blockedBy == true // OR: blocking ???
viewModel.isFollowed = relationship.followedBy
}
.assign(to: \.isMuting, on: viewModel)
.store(in: &disposeBag)
// isBlocking
author.publisher(for: \.blockingBy)
.map { blockingBy in
guard let authContext = authContext else { return false }
return blockingBy.contains(where: {
$0.id == authContext.mastodonAuthenticationBox.userID
&& $0.domain == authContext.mastodonAuthenticationBox.domain
})
}
.assign(to: \.isBlocking, on: viewModel)
.store(in: &disposeBag)
// let pendingFollowRequests = try await context.apiService.pendingFollowRequest(userID: notification.account.id, authenticationBox: authContext.mastodonAuthenticationBox).value
// pendingFollowRequests
}
// isMyself
Publishers.CombineLatest(
author.publisher(for: \.domain),
author.publisher(for: \.id)
)
.map { domain, id in
guard let authContext = authContext else { return false }
return authContext.mastodonAuthenticationBox.domain == domain
&& authContext.mastodonAuthenticationBox.userID == id
}
.assign(to: \.isMyself, on: viewModel)
.store(in: &disposeBag)
viewModel.isMyself = (author.domain == authContext.mastodonAuthenticationBox.domain) && (author.id == authContext.mastodonAuthenticationBox.userID)
#warning("re-implemented the two below")
// follow request state
notification.publisher(for: \.followRequestState)
.assign(to: \.followRequestState, on: viewModel)
.store(in: &disposeBag)
notification.publisher(for: \.transientFollowRequestState)
.assign(to: \.transientFollowRequestState, on: viewModel)
.store(in: &disposeBag)
// notification.publisher(for: \.followRequestState)
// .assign(to: \.followRequestState, on: viewModel)
// .store(in: &disposeBag)
// notification.publisher(for: \.transientFollowRequestState)
// .assign(to: \.transientFollowRequestState, on: viewModel)
// .store(in: &disposeBag)
// Following
author.publisher(for: \.followingBy)
.map { [weak viewModel] followingBy in
guard let viewModel = viewModel else { return false }
guard let authContext = viewModel.authContext else { return false }
return followingBy.contains(where: {
$0.id == authContext.mastodonAuthenticationBox.userID && $0.domain == authContext.mastodonAuthenticationBox.domain
})
}
.assign(to: \.isFollowed, on: viewModel)
.store(in: &disposeBag)
// author.publisher(for: \.followingBy)
// .map { [weak viewModel] followingBy in
// guard let viewModel = viewModel else { return false }
// guard let authContext = viewModel.authContext else { return false }
// return followingBy.contains(where: {
// $0.id == authContext.mastodonAuthenticationBox.userID && $0.domain == authContext.mastodonAuthenticationBox.domain
// })
// }
// .assign(to: \.isFollowed, on: viewModel)
// .store(in: &disposeBag)
}
}

View File

@ -7,94 +7,82 @@
import UIKit
import Combine
import CoreDataStack
import MetaTextKit
import MastodonCore
import MastodonUI
import MastodonSDK
extension PollOptionView {
public func configure(pollOption option: PollOption) {
guard let poll = option.poll, let status = poll.status else {
assertionFailure("PollOption to be configured is expected to be part of Poll with Status")
return
}
public func configure(status: Mastodon.Entity.Status, pollOption option: Mastodon.Entity.Poll.Option, poll: Mastodon.Entity.Poll) {
viewModel.objects.insert(option)
// metaContent
option.publisher(for: \.title)
.map { title -> MetaContent? in
return PlaintextMetaContent(string: title)
}
.assign(to: \.metaContent, on: viewModel)
.store(in: &disposeBag)
viewModel.metaContent = PlaintextMetaContent(string: option.title)
// percentage
Publishers.CombineLatest(
poll.publisher(for: \.votersCount),
option.publisher(for: \.votesCount)
)
.map { pollVotersCount, optionVotesCount -> Double? in
viewModel.percentage = {
let pollVotersCount = poll.votersCount ?? 0
let optionVotesCount = option.votesCount ?? 0
guard pollVotersCount > 0, optionVotesCount >= 0 else { return 0 }
return Double(optionVotesCount) / Double(pollVotersCount)
}
.assign(to: \.percentage, on: viewModel)
.store(in: &disposeBag)
}()
// $isExpire
poll.publisher(for: \.expired)
.assign(to: \.isExpire, on: viewModel)
.store(in: &disposeBag)
viewModel.isExpire = poll.expired
// isMultiple
viewModel.isMultiple = poll.multiple
let optionIndex = option.index
let authorDomain = status.author.domain
let authorID = status.author.id
// let optionIndex = option.index
let authorDomain = status.account.domain
let authorID = status.account.id
#warning("re-implemented the code below")
// isSelect, isPollVoted, isMyPoll
Publishers.CombineLatest4(
option.publisher(for: \.poll),
option.publisher(for: \.votedBy),
option.publisher(for: \.isSelected),
viewModel.$authContext
)
.sink { [weak self] poll, optionVotedBy, isSelected, authContext in
guard let self = self, let poll = poll else { return }
let domain = authContext?.mastodonAuthenticationBox.domain ?? ""
let userID = authContext?.mastodonAuthenticationBox.userID ?? ""
let options = poll.options
let pollVoteBy = poll.votedBy ?? Set()
let isMyPoll = authorDomain == domain
&& authorID == userID
let votedOptions = options.filter { option in
let votedBy = option.votedBy ?? Set()
return votedBy.contains(where: { $0.id == userID && $0.domain == domain })
}
let isRemoteVotedOption = votedOptions.contains(where: { $0.index == optionIndex })
let isRemoteVotedPoll = pollVoteBy.contains(where: { $0.id == userID && $0.domain == domain })
let isLocalVotedOption = isSelected
let isSelect: Bool? = {
if isLocalVotedOption {
return true
} else if !votedOptions.isEmpty {
return isRemoteVotedOption ? true : false
} else if isRemoteVotedPoll, votedOptions.isEmpty {
// the poll voted. But server not mark voted options
return nil
} else {
return false
}
}()
self.viewModel.isSelect = isSelect
self.viewModel.isPollVoted = isRemoteVotedPoll
self.viewModel.isMyPoll = isMyPoll
}
.store(in: &disposeBag)
// Publishers.CombineLatest4(
// option.publisher(for: \.poll),
// option.publisher(for: \.votedBy),
// option.publisher(for: \.isSelected),
// viewModel.$authContext
// )
// .sink { [weak self] poll, optionVotedBy, isSelected, authContext in
// guard let self = self, let poll = poll else { return }
//
// let domain = authContext?.mastodonAuthenticationBox.domain ?? ""
// let userID = authContext?.mastodonAuthenticationBox.userID ?? ""
//
// let options = poll.options
// let pollVoteBy = poll.votedBy ?? Set()
//
// let isMyPoll = authorDomain == domain
// && authorID == userID
//
// let votedOptions = options.filter { option in
// let votedBy = option.votedBy ?? Set()
// return votedBy.contains(where: { $0.id == userID && $0.domain == domain })
// }
// let isRemoteVotedOption = votedOptions.contains(where: { $0.index == optionIndex })
// let isRemoteVotedPoll = pollVoteBy.contains(where: { $0.id == userID && $0.domain == domain })
//
// let isLocalVotedOption = isSelected
//
// let isSelect: Bool? = {
// if isLocalVotedOption {
// return true
// } else if !votedOptions.isEmpty {
// return isRemoteVotedOption ? true : false
// } else if isRemoteVotedPoll, votedOptions.isEmpty {
// // the poll voted. But server not mark voted options
// return nil
// } else {
// return false
// }
// }()
// self.viewModel.isSelect = isSelect
// self.viewModel.isPollVoted = isRemoteVotedPoll
// self.viewModel.isMyPoll = isMyPoll
// }
// .store(in: &disposeBag)
// appearance
checkmarkBackgroundView.backgroundColor = UIColor(dynamicProvider: { trailtCollection in
return trailtCollection.userInterfaceStyle == .light ? .white : SystemTheme.tableViewCellSelectionBackgroundColor

View File

@ -8,7 +8,6 @@
import UIKit
import Combine
import MastodonUI
import CoreDataStack
import MastodonLocalization
import MastodonMeta
import MastodonCore
@ -17,55 +16,32 @@ import MastodonSDK
import MastodonAsset
extension UserView {
public func configure(user: MastodonUser, delegate: UserViewDelegate?) {
public func configure(user: Mastodon.Entity.Account, delegate: UserViewDelegate?) {
self.delegate = delegate
viewModel.user = user
viewModel.account = nil
viewModel.relationship = nil
Publishers.CombineLatest(
user.publisher(for: \.avatar),
UserDefaults.shared.publisher(for: \.preferredStaticAvatar)
)
.map { _ in user.avatarImageURL() }
.assign(to: \.authorAvatarImageURL, on: viewModel)
.store(in: &disposeBag)
viewModel.authorAvatarImageURL = user.avatarImageURL()
// author name
Publishers.CombineLatest(
user.publisher(for: \.displayName),
user.publisher(for: \.emojis)
)
.map { _, emojis in
viewModel.authorName = {
do {
let content = MastodonContent(content: user.displayNameWithFallback, emojis: emojis.asDictionary)
let content = MastodonContent(content: user.displayNameWithFallback, emojis: user.emojis?.asDictionary ?? [:])
let metaContent = try MastodonMetaContent.convert(document: content)
return metaContent
} catch {
assertionFailure(error.localizedDescription)
return PlaintextMetaContent(string: user.displayNameWithFallback)
}
}
.assign(to: \.authorName, on: viewModel)
.store(in: &disposeBag)
}()
// author username
user.publisher(for: \.acct)
.map { $0 as String? }
.assign(to: \.authorUsername, on: viewModel)
.store(in: &disposeBag)
viewModel.authorUsername = user.acct
user.publisher(for: \.followersCount)
.map { Int($0) }
.assign(to: \.authorFollowers, on: viewModel)
.store(in: &disposeBag)
viewModel.authorFollowers = user.followersCount
user.publisher(for: \.fields)
.map { fields in
let firstVerified = fields.first(where: { $0.verifiedAt != nil })
return firstVerified?.value
}
.assign(to: \.authorVerifiedLink, on: viewModel)
.store(in: &disposeBag)
viewModel.authorVerifiedLink = user.fields?.first(where: { $0.verifiedAt != nil })?.value
}
func configure(with account: Mastodon.Entity.Account, relationship: Mastodon.Entity.Relationship?, delegate: UserViewDelegate?) {

View File

@ -6,7 +6,7 @@
//
import UIKit
import CoreDataStack
import MastodonSDK
extension StatusTableViewCell {
final class ViewModel {
@ -17,8 +17,8 @@ extension StatusTableViewCell {
}
enum Value {
case feed(Feed)
case status(Status)
case feed(FeedItem)
case status(Mastodon.Entity.Status)
}
}
}
@ -38,13 +38,8 @@ extension StatusTableViewCell {
switch viewModel.value {
case .feed(let feed):
statusView.configure(feed: feed)
feed.publisher(for: \.hasMore)
.sink { [weak self] hasMore in
guard let self = self else { return }
self.separatorLine.isHidden = hasMore
}
.store(in: &disposeBag)
self.separatorLine.isHidden = feed.hasMore
case .status(let status):
statusView.configure(status: status)

View File

@ -6,7 +6,7 @@
//
import UIKit
import CoreDataStack
import MastodonSDK
extension StatusThreadRootTableViewCell {
final class ViewModel {
@ -17,7 +17,7 @@ extension StatusThreadRootTableViewCell {
}
enum Value {
case status(Status)
case status(Mastodon.Entity.Status)
}
}
}

View File

@ -6,7 +6,6 @@
//
import UIKit
import CoreDataStack
import MastodonUI
import Combine
import MastodonCore
@ -14,13 +13,13 @@ import MastodonSDK
extension UserTableViewCell {
final class ViewModel {
let user: MastodonUser
let user: Mastodon.Entity.Account
let followedUsers: AnyPublisher<[String], Never>
let blockedUsers: AnyPublisher<[String], Never>
let followRequestedUsers: AnyPublisher<[String], Never>
init(user: MastodonUser, followedUsers: AnyPublisher<[String], Never>, blockedUsers: AnyPublisher<[String], Never>, followRequestedUsers: AnyPublisher<[String], Never>) {
init(user: Mastodon.Entity.Account, followedUsers: AnyPublisher<[String], Never>, blockedUsers: AnyPublisher<[String], Never>, followRequestedUsers: AnyPublisher<[String], Never>) {
self.user = user
self.followedUsers = followedUsers
self.followRequestedUsers = followRequestedUsers
@ -32,7 +31,7 @@ extension UserTableViewCell {
extension UserTableViewCell {
func configure(
me: MastodonUser,
me: Mastodon.Entity.Account,
tableView: UITableView,
account: Mastodon.Entity.Account,
relationship: Mastodon.Entity.Relationship?,
@ -47,61 +46,62 @@ extension UserTableViewCell {
}
func configure(
me: MastodonUser? = nil,
me: Mastodon.Entity.Account? = nil,
tableView: UITableView,
viewModel: ViewModel,
delegate: UserTableViewCellDelegate?
) {
userView.configure(user: viewModel.user, delegate: delegate)
guard let me = me else {
guard let me else {
return userView.setButtonState(.none)
}
if viewModel.user == me {
userView.setButtonState(.none)
} else {
userView.setButtonState(.loading)
}
Publishers.CombineLatest3(
viewModel.followedUsers,
viewModel.followRequestedUsers,
viewModel.blockedUsers
)
.receive(on: DispatchQueue.main)
.sink { [weak self] followed, requested, blocked in
if viewModel.user == me {
self?.userView.setButtonState(.none)
} else if blocked.contains(viewModel.user.id) {
self?.userView.setButtonState(.blocked)
} else if followed.contains(viewModel.user.id) {
self?.userView.setButtonState(.unfollow)
} else if requested.contains(viewModel.user.id) {
self?.userView.setButtonState(.pending)
} else if viewModel.user.locked {
self?.userView.setButtonState(.request)
} else if viewModel.user != me {
self?.userView.setButtonState(.follow)
}
}
.store(in: &disposeBag)
#warning("re-implement the code below")
// if viewModel.user == me {
// userView.setButtonState(.none)
// } else {
// userView.setButtonState(.loading)
// }
//
// Publishers.CombineLatest3(
// viewModel.followedUsers,
// viewModel.followRequestedUsers,
// viewModel.blockedUsers
// )
// .receive(on: DispatchQueue.main)
// .sink { [weak self] followed, requested, blocked in
// if viewModel.user == me {
// self?.userView.setButtonState(.none)
// } else if blocked.contains(viewModel.user.id) {
// self?.userView.setButtonState(.blocked)
// } else if followed.contains(viewModel.user.id) {
// self?.userView.setButtonState(.unfollow)
// } else if requested.contains(viewModel.user.id) {
// self?.userView.setButtonState(.pending)
// } else if viewModel.user.locked {
// self?.userView.setButtonState(.request)
// } else if viewModel.user != me {
// self?.userView.setButtonState(.follow)
// }
// }
// .store(in: &disposeBag)
self.delegate = delegate
}
}
extension UserTableViewCellDelegate where Self: NeedsDependency & AuthContextProvider {
func userView(_ view: UserView, didTapButtonWith state: UserView.ButtonState, for user: MastodonUser) {
func userView(_ view: UserView, didTapButtonWith state: UserView.ButtonState, for user: Mastodon.Entity.Account) {
Task {
try await DataSourceFacade.responseToUserViewButtonAction(
dependency: self,
user: user.asRecord,
user: user,
buttonState: state
)
}
}
func userView(_ view: UserView, didTapButtonWith state: UserView.ButtonState, for account: Mastodon.Entity.Account, me: MastodonUser?) {
func userView(_ view: UserView, didTapButtonWith state: UserView.ButtonState, for account: Mastodon.Entity.Account, me: Mastodon.Entity.Account?) {
Task {
await MainActor.run { view.setButtonState(.loading) }

View File

@ -7,7 +7,6 @@
import UIKit
import Combine
import CoreDataStack
import MastodonAsset
import MastodonLocalization
import MastodonUI

View File

@ -6,8 +6,8 @@
//
import Foundation
import CoreDataStack
import MastodonSDK
enum RecommendAccountItem: Hashable {
case account(ManagedObjectRecord<MastodonUser>)
case account(Mastodon.Entity.Account)
}

Some files were not shown because too many files have changed in this diff Show More