Remove usage of Status (IOS-176)
This commit is contained in:
parent
ace671af15
commit
b010b6112e
|
@ -7,10 +7,10 @@
|
|||
|
||||
import CoreData
|
||||
import Foundation
|
||||
import CoreDataStack
|
||||
import MastodonSDK
|
||||
|
||||
enum NotificationItem: Hashable {
|
||||
case feed(record: ManagedObjectRecord<Feed>)
|
||||
case feedLoader(record: ManagedObjectRecord<Feed>)
|
||||
case feed(record: MastodonFeed)
|
||||
case feedLoader(record: MastodonFeed)
|
||||
case bottomLoader
|
||||
}
|
||||
|
|
|
@ -41,18 +41,15 @@ extension NotificationSection {
|
|||
|
||||
return UITableViewDiffableDataSource(tableView: tableView) { tableView, indexPath, item -> UITableViewCell? in
|
||||
switch item {
|
||||
case .feed(let record):
|
||||
case .feed(let feed):
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: NotificationTableViewCell.self), for: indexPath) as! NotificationTableViewCell
|
||||
context.managedObjectContext.performAndWait {
|
||||
guard let feed = record.object(in: context.managedObjectContext) else { return }
|
||||
configure(
|
||||
context: context,
|
||||
tableView: tableView,
|
||||
cell: cell,
|
||||
viewModel: NotificationTableViewCell.ViewModel(value: .feed(feed)),
|
||||
configuration: configuration
|
||||
)
|
||||
}
|
||||
configure(
|
||||
context: context,
|
||||
tableView: tableView,
|
||||
cell: cell,
|
||||
viewModel: NotificationTableViewCell.ViewModel(value: .feed(feed)),
|
||||
configuration: configuration
|
||||
)
|
||||
return cell
|
||||
case .feedLoader:
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: TimelineBottomLoaderTableViewCell.self), for: indexPath) as! TimelineBottomLoaderTableViewCell
|
||||
|
|
|
@ -7,10 +7,11 @@
|
|||
|
||||
import Foundation
|
||||
import CoreDataStack
|
||||
import MastodonSDK
|
||||
|
||||
enum ReportItem: Hashable {
|
||||
case header(context: HeaderContext)
|
||||
case status(record: ManagedObjectRecord<Status>)
|
||||
case status(record: MastodonStatus)
|
||||
case comment(context: CommentContext)
|
||||
case result(record: ManagedObjectRecord<MastodonUser>)
|
||||
case bottomLoader
|
||||
|
|
|
@ -45,18 +45,15 @@ extension ReportSection {
|
|||
cell.primaryLabel.text = headerContext.primaryLabelText
|
||||
cell.secondaryLabel.text = headerContext.secondaryLabelText
|
||||
return cell
|
||||
case .status(let record):
|
||||
case .status(let status):
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: ReportStatusTableViewCell.self), for: indexPath) as! ReportStatusTableViewCell
|
||||
context.managedObjectContext.performAndWait {
|
||||
guard let status = record.object(in: context.managedObjectContext) else { return }
|
||||
configure(
|
||||
context: context,
|
||||
tableView: tableView,
|
||||
cell: cell,
|
||||
viewModel: .init(value: status),
|
||||
configuration: configuration
|
||||
)
|
||||
}
|
||||
configure(
|
||||
context: context,
|
||||
tableView: tableView,
|
||||
cell: cell,
|
||||
viewModel: .init(value: status),
|
||||
configuration: configuration
|
||||
)
|
||||
return cell
|
||||
case .comment(let commentContext):
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: ReportCommentTableViewCell.self), for: indexPath) as! ReportCommentTableViewCell
|
||||
|
|
|
@ -8,11 +8,12 @@
|
|||
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: MastodonFeed)
|
||||
case feedLoader(record: MastodonFeed)
|
||||
case status(record: MastodonStatus)
|
||||
case thread(Thread)
|
||||
case topLoader
|
||||
case bottomLoader
|
||||
|
@ -24,7 +25,7 @@ extension StatusItem {
|
|||
case reply(context: Context)
|
||||
case leaf(context: Context)
|
||||
|
||||
public var record: ManagedObjectRecord<Status> {
|
||||
public var record: MastodonStatus {
|
||||
switch self {
|
||||
case .root(let threadContext),
|
||||
.reply(let threadContext),
|
||||
|
@ -37,12 +38,12 @@ extension StatusItem {
|
|||
|
||||
extension StatusItem.Thread {
|
||||
class Context: Hashable {
|
||||
let status: ManagedObjectRecord<Status>
|
||||
let status: MastodonStatus
|
||||
var displayUpperConversationLink: Bool
|
||||
var displayBottomConversationLink: Bool
|
||||
|
||||
init(
|
||||
status: ManagedObjectRecord<Status>,
|
||||
status: MastodonStatus,
|
||||
displayUpperConversationLink: Bool = false,
|
||||
displayBottomConversationLink: Bool = false
|
||||
) {
|
||||
|
|
|
@ -44,42 +44,33 @@ extension StatusSection {
|
|||
|
||||
return UITableViewDiffableDataSource(tableView: tableView) { tableView, indexPath, item -> UITableViewCell? in
|
||||
switch item {
|
||||
case .feed(let record):
|
||||
case .feed(let feed):
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: 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(feed)),
|
||||
configuration: configuration
|
||||
)
|
||||
return cell
|
||||
case .feedLoader(let record):
|
||||
case .feedLoader(let feed):
|
||||
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: feed,
|
||||
configuration: configuration
|
||||
)
|
||||
return cell
|
||||
case .status(let record):
|
||||
case .status(let status):
|
||||
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(status)),
|
||||
configuration: configuration
|
||||
)
|
||||
return cell
|
||||
case .thread(let thread):
|
||||
let cell = dequeueConfiguredReusableCell(
|
||||
|
@ -124,30 +115,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
|
||||
}
|
||||
}
|
||||
|
@ -319,7 +304,7 @@ extension StatusSection {
|
|||
|
||||
static func configure(
|
||||
cell: TimelineMiddleLoaderTableViewCell,
|
||||
feed: Feed,
|
||||
feed: MastodonFeed,
|
||||
configuration: Configuration
|
||||
) {
|
||||
cell.configure(
|
||||
|
|
|
@ -9,11 +9,12 @@ import UIKit
|
|||
import CoreData
|
||||
import CoreDataStack
|
||||
import MastodonCore
|
||||
import MastodonSDK
|
||||
|
||||
extension DataSourceFacade {
|
||||
public static func responseToStatusBookmarkAction(
|
||||
provider: UIViewController & NeedsDependency & AuthContextProvider,
|
||||
status: ManagedObjectRecord<Status>
|
||||
status: MastodonStatus
|
||||
) async throws {
|
||||
let selectionFeedbackGenerator = await UISelectionFeedbackGenerator()
|
||||
await selectionFeedbackGenerator.selectionChanged()
|
||||
|
|
|
@ -7,19 +7,19 @@
|
|||
|
||||
import UIKit
|
||||
import CoreData
|
||||
import CoreDataStack
|
||||
import MastodonSDK
|
||||
import MastodonCore
|
||||
|
||||
extension DataSourceFacade {
|
||||
public static func responseToStatusFavoriteAction(
|
||||
provider: DataSourceProvider & AuthContextProvider,
|
||||
status: ManagedObjectRecord<Status>
|
||||
status: MastodonStatus
|
||||
) async throws {
|
||||
let selectionFeedbackGenerator = await UISelectionFeedbackGenerator()
|
||||
await selectionFeedbackGenerator.selectionChanged()
|
||||
|
||||
_ = try await provider.context.apiService.favorite(
|
||||
record: status,
|
||||
status: status,
|
||||
authenticationBox: provider.authContext.mastodonAuthenticationBox
|
||||
)
|
||||
}
|
||||
|
|
|
@ -46,15 +46,14 @@ extension DataSourceFacade {
|
|||
extension DataSourceFacade {
|
||||
static func responseToUserFollowRequestAction(
|
||||
dependency: NeedsDependency & AuthContextProvider,
|
||||
notification: ManagedObjectRecord<Notification>,
|
||||
notification: MastodonNotification,
|
||||
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
|
||||
}
|
||||
|
||||
|
@ -63,23 +62,17 @@ extension DataSourceFacade {
|
|||
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
|
||||
}
|
||||
let state: MastodonFollowRequestState = 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)
|
||||
}
|
||||
switch query {
|
||||
case .accept:
|
||||
notification.transientFollowRequestState = .init(state: .isAccepting)
|
||||
case .reject:
|
||||
notification.transientFollowRequestState = .init(state: .isRejecting)
|
||||
}
|
||||
|
||||
do {
|
||||
|
@ -90,22 +83,12 @@ extension DataSourceFacade {
|
|||
)
|
||||
} catch {
|
||||
// reset state when failure
|
||||
try? await managedObjectContext.performChanges {
|
||||
guard let notification = notification.object(in: managedObjectContext) else { return }
|
||||
notification.transientFollowRequestState = .init(state: .none)
|
||||
}
|
||||
|
||||
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
|
||||
default:
|
||||
let alertController = await UIAlertController(for: error, title: nil, preferredStyle: .alert)
|
||||
let okAction = await UIAlertAction(title: L10n.Common.Controls.Actions.ok, style: .default)
|
||||
|
@ -121,32 +104,14 @@ 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
|
||||
}
|
||||
switch query {
|
||||
case .accept:
|
||||
notification.transientFollowRequestState = .init(state: .isAccept)
|
||||
notification.followRequestState = .init(state: .isAccept)
|
||||
case .reject:
|
||||
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)
|
||||
}
|
||||
}
|
||||
} // end func
|
||||
}
|
||||
}
|
||||
|
||||
extension DataSourceFacade {
|
||||
|
|
|
@ -9,6 +9,7 @@ import UIKit
|
|||
import CoreDataStack
|
||||
import MastodonUI
|
||||
import MastodonLocalization
|
||||
import MastodonSDK
|
||||
|
||||
extension DataSourceFacade {
|
||||
|
||||
|
@ -61,15 +62,12 @@ extension DataSourceFacade {
|
|||
@MainActor
|
||||
static func coordinateToMediaPreviewScene(
|
||||
dependency: NeedsDependency & MediaPreviewableViewController,
|
||||
status: ManagedObjectRecord<Status>,
|
||||
status: MastodonStatus,
|
||||
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
|
||||
let attachments = status.entity.mastodonAttachments
|
||||
|
||||
let thumbnails = await previewContext.thumbnails()
|
||||
|
||||
|
|
|
@ -9,16 +9,17 @@ import Foundation
|
|||
import CoreDataStack
|
||||
import MetaTextKit
|
||||
import MastodonCore
|
||||
import MastodonSDK
|
||||
|
||||
extension DataSourceFacade {
|
||||
|
||||
static func responseToMetaTextAction(
|
||||
provider: DataSourceProvider & AuthContextProvider,
|
||||
target: StatusTarget,
|
||||
status: ManagedObjectRecord<Status>,
|
||||
status: MastodonStatus,
|
||||
meta: Meta
|
||||
) async throws {
|
||||
let _redirectRecord = await DataSourceFacade.status(
|
||||
let _redirectRecord = DataSourceFacade.status(
|
||||
managedObjectContext: provider.context.managedObjectContext,
|
||||
status: status,
|
||||
target: target
|
||||
|
@ -35,7 +36,7 @@ extension DataSourceFacade {
|
|||
|
||||
static func responseToMetaTextAction(
|
||||
provider: DataSourceProvider & AuthContextProvider,
|
||||
status: ManagedObjectRecord<Status>,
|
||||
status: MastodonStatus,
|
||||
meta: Meta
|
||||
) async {
|
||||
switch meta {
|
||||
|
@ -55,7 +56,7 @@ extension DataSourceFacade {
|
|||
url: url
|
||||
)
|
||||
case .hashtag(_, let hashtag, _):
|
||||
let hashtagTimelineViewModel = HashtagTimelineViewModel(context: provider.context, authContext: provider.authContext, hashtag: hashtag)
|
||||
let hashtagTimelineViewModel = await HashtagTimelineViewModel(context: provider.context, authContext: provider.authContext, hashtag: hashtag)
|
||||
_ = await provider.coordinator.present(scene: .hashtagTimeline(viewModel: hashtagTimelineViewModel), from: provider, transition: .show)
|
||||
case .mention(_, let mention, let userInfo):
|
||||
await coordinateToProfileScene(
|
||||
|
|
|
@ -9,41 +9,14 @@ import Foundation
|
|||
import CoreData
|
||||
import CoreDataStack
|
||||
import MastodonUI
|
||||
import MastodonSDK
|
||||
|
||||
extension DataSourceFacade {
|
||||
static func status(
|
||||
managedObjectContext: NSManagedObjectContext,
|
||||
status: ManagedObjectRecord<Status>,
|
||||
status: MastodonStatus,
|
||||
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? {
|
||||
) -> MastodonStatus? {
|
||||
switch target {
|
||||
case .status:
|
||||
return status.reblog ?? status
|
||||
|
@ -52,3 +25,19 @@ extension DataSourceFacade {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension DataSourceFacade {
|
||||
static func author(
|
||||
managedObjectContext: NSManagedObjectContext,
|
||||
status: MastodonStatus,
|
||||
target: StatusTarget
|
||||
) async -> ManagedObjectRecord<MastodonUser>? {
|
||||
return try? await managedObjectContext.perform {
|
||||
return DataSourceFacade.status(managedObjectContext: managedObjectContext, status: status, target: target)
|
||||
.flatMap { $0.entity.account }
|
||||
.flatMap {
|
||||
MastodonUser.findOrFetch(in: managedObjectContext, matching: MastodonUser.predicate(domain: $0.domain ?? "", id: $0.id))?.asRecord
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,7 +15,7 @@ extension DataSourceFacade {
|
|||
static func coordinateToProfileScene(
|
||||
provider: DataSourceProvider & AuthContextProvider,
|
||||
target: StatusTarget,
|
||||
status: ManagedObjectRecord<Status>
|
||||
status: MastodonStatus
|
||||
) async {
|
||||
let _redirectRecord = await DataSourceFacade.author(
|
||||
managedObjectContext: provider.context.managedObjectContext,
|
||||
|
@ -83,9 +83,10 @@ extension DataSourceFacade {
|
|||
|
||||
extension DataSourceFacade {
|
||||
|
||||
@MainActor
|
||||
static func coordinateToProfileScene(
|
||||
provider: DataSourceProvider & AuthContextProvider,
|
||||
status: ManagedObjectRecord<Status>,
|
||||
status: MastodonStatus,
|
||||
mention: String, // username,
|
||||
userInfo: [AnyHashable: Any]?
|
||||
) async {
|
||||
|
@ -100,11 +101,11 @@ extension DataSourceFacade {
|
|||
|
||||
let managedObjectContext = provider.context.managedObjectContext
|
||||
let mentions = try? await managedObjectContext.perform {
|
||||
return status.object(in: managedObjectContext)?.mentions ?? []
|
||||
return status.entity.mentions ?? []
|
||||
}
|
||||
|
||||
guard let mention = mentions?.first(where: { $0.url == href }) else {
|
||||
_ = await provider.coordinator.present(
|
||||
_ = provider.coordinator.present(
|
||||
scene: .safari(url: url),
|
||||
from: provider,
|
||||
transition: .safariPresent(animated: true, completion: nil)
|
||||
|
@ -131,7 +132,7 @@ extension DataSourceFacade {
|
|||
}
|
||||
}()
|
||||
|
||||
_ = await provider.coordinator.present(
|
||||
_ = provider.coordinator.present(
|
||||
scene: .profile(viewModel: profileViewModel),
|
||||
from: provider,
|
||||
transition: .show
|
||||
|
|
|
@ -6,20 +6,20 @@
|
|||
//
|
||||
|
||||
import UIKit
|
||||
import CoreDataStack
|
||||
import MastodonCore
|
||||
import MastodonUI
|
||||
import MastodonSDK
|
||||
|
||||
extension DataSourceFacade {
|
||||
static func responseToStatusReblogAction(
|
||||
provider: DataSourceProvider & AuthContextProvider,
|
||||
status: ManagedObjectRecord<Status>
|
||||
status: MastodonStatus
|
||||
) async throws {
|
||||
let selectionFeedbackGenerator = await UISelectionFeedbackGenerator()
|
||||
await selectionFeedbackGenerator.selectionChanged()
|
||||
|
||||
_ = try await provider.context.apiService.reblog(
|
||||
record: status,
|
||||
status: status,
|
||||
authenticationBox: provider.authContext.mastodonAuthenticationBox
|
||||
)
|
||||
} // end func
|
||||
|
|
|
@ -14,13 +14,14 @@ import MastodonUI
|
|||
import MastodonLocalization
|
||||
import LinkPresentation
|
||||
import UniformTypeIdentifiers
|
||||
import MastodonSDK
|
||||
|
||||
// Delete
|
||||
extension DataSourceFacade {
|
||||
|
||||
static func responseToDeleteStatus(
|
||||
dependency: NeedsDependency & AuthContextProvider,
|
||||
status: ManagedObjectRecord<Status>
|
||||
status: MastodonStatus
|
||||
) async throws {
|
||||
_ = try await dependency.context.apiService.deleteStatus(
|
||||
status: status,
|
||||
|
@ -36,7 +37,7 @@ extension DataSourceFacade {
|
|||
@MainActor
|
||||
public static func responseToStatusShareAction(
|
||||
provider: DataSourceProvider,
|
||||
status: ManagedObjectRecord<Status>,
|
||||
status: MastodonStatus,
|
||||
button: UIButton
|
||||
) async throws {
|
||||
let activityViewController = try await createActivityViewController(
|
||||
|
@ -56,22 +57,22 @@ extension DataSourceFacade {
|
|||
|
||||
private static func createActivityViewController(
|
||||
dependency: NeedsDependency,
|
||||
status: ManagedObjectRecord<Status>
|
||||
status: MastodonStatus
|
||||
) 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.entity.url ?? status.entity.uri)
|
||||
else { return [] }
|
||||
return [
|
||||
URLActivityItemWithMetadata(url: url) { metadata in
|
||||
metadata.title = "\(status.author.displayName) (@\(status.author.acctWithDomain))"
|
||||
metadata.title = "\(status.entity.account.displayName) (@\(status.entity.account.acctWithDomain))"
|
||||
metadata.iconProvider = ImageProvider(
|
||||
url: status.author.avatarImageURLWithFallback(domain: status.author.domain),
|
||||
url: status.entity.account.avatarImageURLWithFallback(domain: status.entity.account.domain ?? ""),
|
||||
filter: ScaledToSizeFilter(size: CGSize.authorAvatarButtonSize)
|
||||
).itemProvider
|
||||
}
|
||||
] as [Any]
|
||||
}
|
||||
}()
|
||||
|
||||
var applicationActivities: [UIActivity] = [
|
||||
SafariActivity(sceneCoordinator: dependency.coordinator), // open URL
|
||||
]
|
||||
|
@ -94,20 +95,12 @@ extension DataSourceFacade {
|
|||
@MainActor
|
||||
static func responseToActionToolbar(
|
||||
provider: DataSourceProvider & AuthContextProvider,
|
||||
status: ManagedObjectRecord<Status>,
|
||||
status: MastodonStatus,
|
||||
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 _status = status.reblog ?? status
|
||||
|
||||
switch action {
|
||||
case .reply:
|
||||
|
@ -266,7 +259,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 +290,7 @@ extension DataSourceFacade {
|
|||
)
|
||||
case .bookmarkStatus:
|
||||
Task {
|
||||
guard let status = menuContext.statusViewModel?.originalStatus?.asRecord else {
|
||||
guard let status = menuContext.statusViewModel?.originalStatus else {
|
||||
assertionFailure()
|
||||
return
|
||||
}
|
||||
|
@ -309,11 +302,7 @@ extension DataSourceFacade {
|
|||
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 {
|
||||
guard let status: MastodonStatus = menuContext.statusViewModel?.originalStatus?.reblog ?? menuContext.statusViewModel?.originalStatus else {
|
||||
assertionFailure()
|
||||
return
|
||||
}
|
||||
|
@ -344,7 +333,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 +347,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 +360,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,12 +391,11 @@ extension DataSourceFacade {
|
|||
|
||||
static func responseToToggleSensitiveAction(
|
||||
dependency: NeedsDependency,
|
||||
status: ManagedObjectRecord<Status>
|
||||
status: MastodonStatus
|
||||
) 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)
|
||||
let _status = status.reblog ?? status
|
||||
_status.isSensitiveToggled = !_status.isSensitiveToggled
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -9,15 +9,16 @@ import UIKit
|
|||
import CoreData
|
||||
import CoreDataStack
|
||||
import MastodonCore
|
||||
import MastodonSDK
|
||||
|
||||
extension DataSourceFacade {
|
||||
static func coordinateToStatusThreadScene(
|
||||
provider: ViewControllerWithDependencies & AuthContextProvider,
|
||||
target: StatusTarget,
|
||||
status: ManagedObjectRecord<Status>
|
||||
status: MastodonStatus
|
||||
) async {
|
||||
let _root: StatusItem.Thread? = await {
|
||||
let _redirectRecord = await DataSourceFacade.status(
|
||||
let _root: StatusItem.Thread? = {
|
||||
let _redirectRecord = DataSourceFacade.status(
|
||||
managedObjectContext: provider.context.managedObjectContext,
|
||||
status: status,
|
||||
target: target
|
||||
|
|
|
@ -20,27 +20,11 @@ extension DataSourceFacade {
|
|||
|
||||
public static func translateStatus(
|
||||
provider: Provider,
|
||||
status: ManagedObjectRecord<Status>
|
||||
status: MastodonStatus
|
||||
) 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)
|
||||
} else {
|
||||
return try await translateStatus(provider: provider, status: status)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension DataSourceFacade {
|
||||
static func translateStatus(provider: Provider, status: Status) async throws -> Mastodon.Entity.Translation? {
|
||||
do {
|
||||
let value = try await provider.context
|
||||
.apiService
|
||||
|
|
|
@ -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: MastodonStatus,
|
||||
url: URL
|
||||
) async {
|
||||
let domain = provider.authContext.mastodonAuthenticationBox.domain
|
||||
|
|
|
@ -10,6 +10,7 @@ import MetaTextKit
|
|||
import CoreDataStack
|
||||
import MastodonCore
|
||||
import MastodonUI
|
||||
import MastodonSDK
|
||||
|
||||
// MARK: - Notification AuthorMenuAction
|
||||
extension NotificationTableViewCellDelegate where Self: DataSourceProvider & AuthContextProvider {
|
||||
|
@ -31,7 +32,6 @@ extension NotificationTableViewCellDelegate where Self: DataSourceProvider & Aut
|
|||
}
|
||||
|
||||
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 {
|
||||
|
@ -71,7 +71,6 @@ 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 {
|
||||
|
@ -155,7 +154,7 @@ extension NotificationTableViewCellDelegate where Self: DataSourceProvider & Aut
|
|||
}
|
||||
|
||||
private struct NotificationMediaTransitionContext {
|
||||
let status: ManagedObjectRecord<Status>
|
||||
let status: MastodonStatus
|
||||
let needsToggleMediaSensitive: Bool
|
||||
}
|
||||
|
||||
|
@ -180,16 +179,13 @@ extension NotificationTableViewCellDelegate where Self: DataSourceProvider & Med
|
|||
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
|
||||
let _mediaTransitionContext: NotificationMediaTransitionContext? = {
|
||||
guard let status = record.status?.reblog ?? record.status else { return nil }
|
||||
return NotificationMediaTransitionContext(
|
||||
status: .init(objectID: status.objectID),
|
||||
needsToggleMediaSensitive: status.isSensitiveToggled ? !status.sensitive : status.sensitive
|
||||
status: status,
|
||||
needsToggleMediaSensitive: status.isSensitiveToggled ? !(status.entity.sensitive == true) : (status.entity.sensitive == true)
|
||||
)
|
||||
}
|
||||
}()
|
||||
|
||||
guard let mediaTransitionContext = _mediaTransitionContext else { return }
|
||||
|
||||
|
@ -233,15 +229,13 @@ extension NotificationTableViewCellDelegate where Self: DataSourceProvider & Med
|
|||
}
|
||||
|
||||
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
|
||||
let _mediaTransitionContext: NotificationMediaTransitionContext? = {
|
||||
guard let status = record.status?.reblog ?? record.status else { return nil }
|
||||
return NotificationMediaTransitionContext(
|
||||
status: .init(objectID: status.objectID),
|
||||
needsToggleMediaSensitive: status.isMediaSensitive ? !status.isSensitiveToggled : false
|
||||
status: status,
|
||||
needsToggleMediaSensitive: status.entity.sensitive == true ? !status.isSensitiveToggled : false
|
||||
)
|
||||
}
|
||||
}()
|
||||
|
||||
guard let mediaTransitionContext = _mediaTransitionContext else { return }
|
||||
|
||||
|
@ -286,12 +280,7 @@ 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)
|
||||
}
|
||||
guard let status = _status else {
|
||||
guard let status = notification.status?.reblog ?? notification.status else {
|
||||
assertionFailure()
|
||||
return
|
||||
}
|
||||
|
@ -323,18 +312,10 @@ 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 {
|
||||
assertionFailure()
|
||||
return
|
||||
}
|
||||
|
||||
await DataSourceFacade.coordinateToProfileScene(
|
||||
provider: self,
|
||||
user: author
|
||||
user: notification.account.asRecord
|
||||
)
|
||||
} // end Task
|
||||
}
|
||||
|
@ -367,12 +348,7 @@ 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?.reblog ?? notification.status else {
|
||||
assertionFailure()
|
||||
return
|
||||
}
|
||||
|
@ -400,12 +376,7 @@ 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?.reblog ?? notification.status else {
|
||||
assertionFailure()
|
||||
return
|
||||
}
|
||||
|
@ -465,12 +436,7 @@ 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?.reblog ?? notification.status else {
|
||||
assertionFailure()
|
||||
return
|
||||
}
|
||||
|
@ -497,12 +463,7 @@ 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?.reblog ?? notification.status else {
|
||||
assertionFailure()
|
||||
return
|
||||
}
|
||||
|
|
|
@ -38,10 +38,9 @@ extension StatusTableViewCellDelegate where Self: DataSourceProvider & AuthConte
|
|||
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 }
|
||||
guard let inReplyToAccountID = status.entity.inReplyToAccountID else { return nil }
|
||||
let request = MastodonUser.sortedFetchRequest
|
||||
request.predicate = MastodonUser.predicate(domain: status.author.domain, id: inReplyToAccountID)
|
||||
request.predicate = MastodonUser.predicate(domain: status.entity.account.domain ?? "", id: inReplyToAccountID)
|
||||
request.fetchLimit = 1
|
||||
guard let author = self.context.managedObjectContext.safeFetch(request).first else { return nil }
|
||||
return .init(objectID: author.objectID)
|
||||
|
@ -184,7 +183,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 +205,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
|
||||
}
|
||||
}
|
||||
],
|
||||
|
@ -471,8 +470,10 @@ extension StatusTableViewCellDelegate where Self: DataSourceProvider & AuthConte
|
|||
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
|
||||
let request = MastodonUser.sortedFetchRequest
|
||||
request.predicate = MastodonUser.predicate(domain: status.entity.account.domain ?? "", id: status.entity.account.id)
|
||||
request.fetchLimit = 1
|
||||
guard let author = self.context.managedObjectContext.safeFetch(request).first else { return nil }
|
||||
return .init(objectID: author.objectID)
|
||||
}
|
||||
guard let author = _author else {
|
||||
|
@ -679,11 +680,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
|
||||
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
import UIKit
|
||||
import CoreDataStack
|
||||
import MastodonCore
|
||||
import MastodonSDK
|
||||
|
||||
extension StatusTableViewControllerNavigateableCore where Self: DataSourceProvider & StatusTableViewControllerNavigateableRelay {
|
||||
|
||||
|
@ -55,7 +56,7 @@ extension StatusTableViewControllerNavigateableCore where Self: DataSourceProvid
|
|||
extension StatusTableViewControllerNavigateableCore where Self: DataSourceProvider & AuthContextProvider {
|
||||
|
||||
@MainActor
|
||||
private func statusRecord() async -> ManagedObjectRecord<Status>? {
|
||||
private func statusRecord() async -> MastodonStatus? {
|
||||
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,12 +65,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 {
|
||||
guard let statusRecord = record.status else {
|
||||
return nil
|
||||
}
|
||||
return statusRecord
|
||||
|
|
|
@ -10,6 +10,7 @@ import CoreDataStack
|
|||
import MastodonCore
|
||||
import MastodonUI
|
||||
import MastodonLocalization
|
||||
import MastodonSDK
|
||||
|
||||
extension UITableViewDelegate where Self: DataSourceProvider & AuthContextProvider {
|
||||
|
||||
|
@ -42,11 +43,7 @@ extension UITableViewDelegate where Self: DataSourceProvider & AuthContextProvid
|
|||
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)
|
||||
}
|
||||
let _status: MastodonStatus? = notification.status
|
||||
if let status = _status {
|
||||
await DataSourceFacade.coordinateToStatusThreadScene(
|
||||
provider: self,
|
||||
|
@ -54,10 +51,7 @@ extension UITableViewDelegate where Self: DataSourceProvider & AuthContextProvid
|
|||
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)
|
||||
}
|
||||
let _author: ManagedObjectRecord<MastodonUser>? = notification.account.asRecord
|
||||
if let author = _author {
|
||||
await DataSourceFacade.coordinateToProfileScene(
|
||||
provider: self,
|
||||
|
|
|
@ -12,10 +12,10 @@ import MastodonSDK
|
|||
import class CoreDataStack.Notification
|
||||
|
||||
enum DataSourceItem: Hashable {
|
||||
case status(record: ManagedObjectRecord<Status>)
|
||||
case status(record: MastodonStatus)
|
||||
case user(record: ManagedObjectRecord<MastodonUser>)
|
||||
case hashtag(tag: TagKind)
|
||||
case notification(record: ManagedObjectRecord<Notification>)
|
||||
case notification(record: MastodonNotification)
|
||||
case account(account: Mastodon.Entity.Account, relationship: Mastodon.Entity.Relationship?)
|
||||
}
|
||||
|
||||
|
|
|
@ -21,7 +21,7 @@ final class ComposeViewModel {
|
|||
|
||||
enum Context {
|
||||
case composeStatus
|
||||
case editStatus(status: Status, statusSource: Mastodon.Entity.StatusSource)
|
||||
case editStatus(status: MastodonStatus, statusSource: Mastodon.Entity.StatusSource)
|
||||
}
|
||||
|
||||
var disposeBag = Set<AnyCancellable>()
|
||||
|
|
|
@ -145,10 +145,10 @@ extension DiscoveryCommunityViewModel.State {
|
|||
self.maxID = newMaxID
|
||||
|
||||
var hasNewStatusesAppend = false
|
||||
var statusIDs = isReloading ? [] : viewModel.statusFetchedResultsController.statusIDs
|
||||
var statusIDs = isReloading ? [] : await viewModel.statusFetchedResultsController.records
|
||||
for status in response.value {
|
||||
guard !statusIDs.contains(status.id) else { continue }
|
||||
statusIDs.append(status.id)
|
||||
guard !statusIDs.contains(where: { $0.id == status.id }) else { continue }
|
||||
statusIDs.append(.fromEntity(status))
|
||||
hasNewStatusesAppend = true
|
||||
}
|
||||
|
||||
|
@ -158,7 +158,7 @@ extension DiscoveryCommunityViewModel.State {
|
|||
} else {
|
||||
await enter(state: NoMore.self)
|
||||
}
|
||||
viewModel.statusFetchedResultsController.statusIDs = statusIDs
|
||||
await viewModel.statusFetchedResultsController.setRecords(statusIDs)
|
||||
viewModel.didLoadLatest.send()
|
||||
|
||||
} catch {
|
||||
|
|
|
@ -41,14 +41,11 @@ final class DiscoveryCommunityViewModel {
|
|||
|
||||
let didLoadLatest = PassthroughSubject<Void, Never>()
|
||||
|
||||
@MainActor
|
||||
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()
|
||||
// end init
|
||||
}
|
||||
}
|
||||
|
|
|
@ -27,6 +27,7 @@ final class DiscoveryViewModel {
|
|||
|
||||
@Published var viewControllers: [ScrollViewContainer & PageViewController]
|
||||
|
||||
@MainActor
|
||||
init(context: AppContext, coordinator: SceneCoordinator, authContext: AuthContext) {
|
||||
self.context = context
|
||||
self.authContext = authContext
|
||||
|
|
|
@ -143,10 +143,10 @@ extension DiscoveryPostsViewModel.State {
|
|||
self.offset = newOffset
|
||||
|
||||
var hasNewStatusesAppend = false
|
||||
var statusIDs = isReloading ? [] : viewModel.statusFetchedResultsController.statusIDs
|
||||
var statusIDs = isReloading ? [] : await viewModel.statusFetchedResultsController.records
|
||||
for status in response.value {
|
||||
guard !statusIDs.contains(status.id) else { continue }
|
||||
statusIDs.append(status.id)
|
||||
guard !statusIDs.contains(where: { $0.id == status.id }) else { continue }
|
||||
statusIDs.append(.fromEntity(status))
|
||||
hasNewStatusesAppend = true
|
||||
}
|
||||
|
||||
|
@ -155,7 +155,7 @@ extension DiscoveryPostsViewModel.State {
|
|||
} else {
|
||||
await enter(state: NoMore.self)
|
||||
}
|
||||
viewModel.statusFetchedResultsController.statusIDs = statusIDs
|
||||
await viewModel.statusFetchedResultsController.setRecords(statusIDs)
|
||||
viewModel.didLoadLatest.send()
|
||||
|
||||
} catch {
|
||||
|
|
|
@ -41,14 +41,11 @@ final class DiscoveryPostsViewModel {
|
|||
let didLoadLatest = PassthroughSubject<Void, Never>()
|
||||
@Published var isServerSupportEndpoint = true
|
||||
|
||||
@MainActor
|
||||
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()
|
||||
// end init
|
||||
|
||||
Task {
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
import Foundation
|
||||
import GameplayKit
|
||||
import CoreDataStack
|
||||
import MastodonSDK
|
||||
|
||||
extension HashtagTimelineViewModel {
|
||||
class State: GKState {
|
||||
|
@ -145,10 +146,10 @@ extension HashtagTimelineViewModel.State {
|
|||
self.maxID = newMaxID
|
||||
|
||||
var hasNewStatusesAppend = false
|
||||
var statusIDs = isReloading ? [] : viewModel.fetchedResultsController.statusIDs
|
||||
var statusIDs = isReloading ? [] : await viewModel.fetchedResultsController.records.map { $0.entity }
|
||||
for status in response.value {
|
||||
guard !statusIDs.contains(status.id) else { continue }
|
||||
statusIDs.append(status.id)
|
||||
guard !statusIDs.contains(status) else { continue }
|
||||
statusIDs.append(status)
|
||||
hasNewStatusesAppend = true
|
||||
}
|
||||
|
||||
|
@ -158,7 +159,7 @@ extension HashtagTimelineViewModel.State {
|
|||
await enter(state: NoMore.self)
|
||||
}
|
||||
|
||||
viewModel.fetchedResultsController.append(statusIDs: statusIDs)
|
||||
await viewModel.fetchedResultsController.setRecords(statusIDs.map { MastodonStatus.fromEntity($0) })
|
||||
viewModel.didLoadLatest.send()
|
||||
} catch {
|
||||
await enter(state: Fail.self)
|
||||
|
|
|
@ -50,15 +50,12 @@ final class HashtagTimelineViewModel {
|
|||
return stateMachine
|
||||
}()
|
||||
|
||||
@MainActor
|
||||
init(context: AppContext, authContext: AuthContext, hashtag: String) {
|
||||
self.context = context
|
||||
self.authContext = authContext
|
||||
self.hashtag = hashtag
|
||||
self.fetchedResultsController = StatusFetchedResultsController(
|
||||
managedObjectContext: context.managedObjectContext,
|
||||
domain: authContext.mastodonAuthenticationBox.domain,
|
||||
additionalTweetPredicate: nil
|
||||
)
|
||||
self.fetchedResultsController = StatusFetchedResultsController()
|
||||
updateTagInformation()
|
||||
// end init
|
||||
}
|
||||
|
|
|
@ -20,17 +20,16 @@ extension HomeTimelineViewController: DataSourceProvider {
|
|||
}
|
||||
|
||||
switch item {
|
||||
case .feed(let record):
|
||||
case .feed(let feed):
|
||||
let managedObjectContext = context.managedObjectContext
|
||||
let item: DataSourceItem? = try? await managedObjectContext.perform {
|
||||
guard let feed = record.object(in: managedObjectContext) else { return nil }
|
||||
let item: DataSourceItem? = {
|
||||
guard feed.kind == .home else { return nil }
|
||||
if let status = feed.status {
|
||||
return .status(record: .init(objectID: status.objectID))
|
||||
return .status(record: status)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}()
|
||||
return item
|
||||
default:
|
||||
return nil
|
||||
|
|
|
@ -52,37 +52,37 @@ extension HomeTimelineViewModel {
|
|||
snapshot.appendItems(newItems, toSection: .main)
|
||||
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("We probably need to replace the code below")
|
||||
// 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 {
|
||||
|
|
|
@ -84,14 +84,10 @@ 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
|
||||
|
||||
Task {
|
||||
let latestStatusIDs: [Status.ID] = latestFeedRecords.compactMap { record in
|
||||
guard let feed = record.object(in: managedObjectContext) else { return nil }
|
||||
return feed.status?.id
|
||||
return record.status?.id
|
||||
}
|
||||
|
||||
do {
|
||||
|
|
|
@ -52,13 +52,10 @@ extension HomeTimelineViewModel.LoadOldestState {
|
|||
}
|
||||
|
||||
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 }
|
||||
let _maxID: Mastodon.Entity.Status.ID? = {
|
||||
guard let status = lastFeedRecord.status else { return nil }
|
||||
return status.id
|
||||
}
|
||||
}()
|
||||
|
||||
guard let maxID = _maxID else {
|
||||
await self.enter(state: Fail.self)
|
||||
|
|
|
@ -80,15 +80,9 @@ final class HomeTimelineViewModel: NSObject {
|
|||
init(context: AppContext, authContext: AuthContext) {
|
||||
self.context = context
|
||||
self.authContext = authContext
|
||||
self.fetchedResultsController = FeedFetchedResultsController(managedObjectContext: context.managedObjectContext)
|
||||
self.fetchedResultsController = FeedFetchedResultsController()
|
||||
self.homeTimelineNavigationBarTitleViewModel = HomeTimelineNavigationBarTitleViewModel(context: context)
|
||||
super.init()
|
||||
|
||||
fetchedResultsController.predicate = Feed.predicate(
|
||||
kind: .home,
|
||||
acct: .mastodon(domain: authContext.mastodonAuthenticationBox.domain, userID: authContext.mastodonAuthenticationBox.userID)
|
||||
)
|
||||
|
||||
homeTimelineNeedRefresh
|
||||
.sink { [weak self] _ in
|
||||
self?.loadLatestStateMachine.enter(LoadLatestState.Loading.self)
|
||||
|
@ -116,7 +110,7 @@ extension HomeTimelineViewModel {
|
|||
|
||||
extension HomeTimelineViewModel {
|
||||
func timelineDidReachEnd() {
|
||||
fetchedResultsController.fetchNextBatch()
|
||||
#warning("Check if required, e.g. when locally caching MastodonStatus")
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -128,47 +122,41 @@ extension HomeTimelineViewModel {
|
|||
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@\(record.objectID)"
|
||||
//
|
||||
// guard let feed = record.object(in: managedObjectContext) 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)
|
||||
}
|
||||
guard let status = record.status else { return }
|
||||
record.isLoadingMore = true
|
||||
|
||||
// // 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])
|
||||
await updateSnapshotUsingReloadData(snapshot: snapshot)
|
||||
|
||||
// fetch data
|
||||
do {
|
||||
let maxID = status.id
|
||||
_ = try await context.apiService.homeTimeline(
|
||||
maxID: maxID,
|
||||
authenticationBox: authContext.mastodonAuthenticationBox
|
||||
)
|
||||
} catch {
|
||||
do {
|
||||
// restore state
|
||||
try await managedObjectContext.performChanges {
|
||||
feed.update(isLoadingMore: false)
|
||||
}
|
||||
} catch {
|
||||
assertionFailure(error.localizedDescription)
|
||||
}
|
||||
}
|
||||
let maxID = status.id
|
||||
_ = try? await context.apiService.homeTimeline(
|
||||
maxID: maxID,
|
||||
authenticationBox: authContext.mastodonAuthenticationBox
|
||||
)
|
||||
|
||||
record.isLoadingMore = false
|
||||
|
||||
// reconfigure item again
|
||||
snapshot.reconfigureItems([item])
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
import UIKit
|
||||
import Combine
|
||||
import CoreDataStack
|
||||
import MastodonSDK
|
||||
|
||||
extension NotificationTableViewCell {
|
||||
final class ViewModel {
|
||||
|
@ -18,7 +19,7 @@ extension NotificationTableViewCell {
|
|||
}
|
||||
|
||||
enum Value {
|
||||
case feed(Feed)
|
||||
case feed(MastodonFeed)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
//
|
||||
|
||||
import UIKit
|
||||
import MastodonSDK
|
||||
|
||||
extension NotificationTimelineViewController: DataSourceProvider {
|
||||
func item(from source: DataSourceItem.Source) async -> DataSourceItem? {
|
||||
|
@ -20,17 +21,16 @@ extension NotificationTimelineViewController: DataSourceProvider {
|
|||
}
|
||||
|
||||
switch item {
|
||||
case .feed(let record):
|
||||
case .feed(let feed):
|
||||
let managedObjectContext = context.managedObjectContext
|
||||
let item: DataSourceItem? = try? await managedObjectContext.perform {
|
||||
guard let feed = record.object(in: managedObjectContext) else { return nil }
|
||||
let item: DataSourceItem? = {
|
||||
guard feed.kind == .notificationAll || feed.kind == .notificationMentions else { return nil }
|
||||
if let notification = feed.notification {
|
||||
return .notification(record: .init(objectID: notification.objectID))
|
||||
if let notification = feed.notification, let mastodonNotification = MastodonNotification.fromEntity(notification, using: managedObjectContext) {
|
||||
return .notification(record: mastodonNotification)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}()
|
||||
return item
|
||||
default:
|
||||
return nil
|
||||
|
|
|
@ -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: .fromEntity(status)))
|
||||
)
|
||||
_ = self.coordinator.present(
|
||||
scene: .thread(viewModel: threadViewModel),
|
||||
|
@ -295,16 +294,25 @@ extension NotificationTimelineViewController: TableViewControllerNavigateable {
|
|||
transition: .show
|
||||
)
|
||||
} else {
|
||||
let profileViewModel = ProfileViewModel(
|
||||
context: self.context,
|
||||
authContext: self.viewModel.authContext,
|
||||
optionalMastodonUser: notification.account
|
||||
)
|
||||
_ = self.coordinator.present(
|
||||
scene: .profile(viewModel: profileViewModel),
|
||||
from: self,
|
||||
transition: .show
|
||||
)
|
||||
context.managedObjectContext.perform {
|
||||
let mastodonUserRequest = MastodonUser.sortedFetchRequest
|
||||
mastodonUserRequest.predicate = MastodonUser.predicate(domain: notification.account.domain ?? "", id: notification.account.id)
|
||||
mastodonUserRequest.fetchLimit = 1
|
||||
guard let mastodonUser = try? self.context.managedObjectContext.fetch(mastodonUserRequest).first else {
|
||||
return
|
||||
}
|
||||
|
||||
let profileViewModel = ProfileViewModel(
|
||||
context: self.context,
|
||||
authContext: self.viewModel.authContext,
|
||||
optionalMastodonUser: mastodonUser
|
||||
)
|
||||
_ = self.coordinator.present(
|
||||
scene: .profile(viewModel: profileViewModel),
|
||||
from: self,
|
||||
transition: .show
|
||||
)
|
||||
}
|
||||
}
|
||||
default:
|
||||
break
|
||||
|
|
|
@ -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("Do we still need the code below?")
|
||||
// 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 {
|
||||
|
|
|
@ -55,12 +55,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? = lastFeedRecord.notification?.id
|
||||
|
||||
guard let maxID = _maxID else {
|
||||
await self.enter(state: Fail.self)
|
||||
|
|
|
@ -43,6 +43,7 @@ final class NotificationTimelineViewModel {
|
|||
return stateMachine
|
||||
}()
|
||||
|
||||
@MainActor
|
||||
init(
|
||||
context: AppContext,
|
||||
authContext: AuthContext,
|
||||
|
@ -51,13 +52,7 @@ final class NotificationTimelineViewModel {
|
|||
self.context = context
|
||||
self.authContext = authContext
|
||||
self.scope = scope
|
||||
self.feedFetchedResultsController = FeedFetchedResultsController(managedObjectContext: context.managedObjectContext)
|
||||
// end init
|
||||
|
||||
feedFetchedResultsController.predicate = NotificationTimelineViewModel.feedPredicate(
|
||||
authenticationBox: authContext.mastodonAuthenticationBox,
|
||||
scope: scope
|
||||
)
|
||||
self.feedFetchedResultsController = FeedFetchedResultsController()
|
||||
}
|
||||
|
||||
|
||||
|
@ -125,29 +120,16 @@ extension NotificationTimelineViewModel {
|
|||
// load timeline gap
|
||||
func loadMore(item: NotificationItem) async {
|
||||
guard case let .feedLoader(record) = item else { return }
|
||||
|
||||
let managedObjectContext = context.managedObjectContext
|
||||
let key = "LoadMore@\(record.objectID)"
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
guard let maxID = record.notification?.id else { return }
|
||||
|
||||
// fetch data
|
||||
do {
|
||||
_ = try await context.apiService.notifications(
|
||||
maxID: maxID,
|
||||
scope: scope,
|
||||
authenticationBox: authContext.mastodonAuthenticationBox
|
||||
)
|
||||
} catch {
|
||||
if let notifications = try? await context.apiService.notifications(
|
||||
maxID: maxID,
|
||||
scope: scope,
|
||||
authenticationBox: authContext.mastodonAuthenticationBox
|
||||
) {
|
||||
self.feedFetchedResultsController.records += notifications.value.map { MastodonFeed.fromNotification($0, kind: record.kind) }
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -57,7 +57,9 @@ extension BookmarkViewModel.State {
|
|||
guard let viewModel = viewModel, let stateMachine = stateMachine else { return }
|
||||
|
||||
// reset
|
||||
viewModel.statusFetchedResultsController.statusIDs = []
|
||||
DispatchQueue.main.async {
|
||||
viewModel.statusFetchedResultsController.reset()
|
||||
}
|
||||
|
||||
stateMachine.enter(Loading.self)
|
||||
}
|
||||
|
@ -128,10 +130,10 @@ extension BookmarkViewModel.State {
|
|||
)
|
||||
|
||||
var hasNewStatusesAppend = false
|
||||
var statusIDs = viewModel.statusFetchedResultsController.statusIDs
|
||||
var statusIDs = await viewModel.statusFetchedResultsController.records.map { $0.entity }
|
||||
for status in response.value {
|
||||
guard !statusIDs.contains(status.id) else { continue }
|
||||
statusIDs.append(status.id)
|
||||
guard !statusIDs.contains(status) else { continue }
|
||||
statusIDs.append(status)
|
||||
hasNewStatusesAppend = true
|
||||
}
|
||||
|
||||
|
@ -147,7 +149,9 @@ extension BookmarkViewModel.State {
|
|||
} else {
|
||||
await enter(state: NoMore.self)
|
||||
}
|
||||
viewModel.statusFetchedResultsController.statusIDs = statusIDs
|
||||
|
||||
await viewModel.statusFetchedResultsController.setRecords(statusIDs.map { MastodonStatus.fromEntity($0) })
|
||||
|
||||
} catch {
|
||||
await enter(state: Fail.self)
|
||||
}
|
||||
|
|
|
@ -38,14 +38,11 @@ final class BookmarkViewModel {
|
|||
return stateMachine
|
||||
}()
|
||||
|
||||
@MainActor
|
||||
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()
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -11,6 +11,7 @@ import MastodonCore
|
|||
|
||||
final class CachedProfileViewModel: ProfileViewModel {
|
||||
|
||||
@MainActor
|
||||
init(context: AppContext, authContext: AuthContext, mastodonUser: MastodonUser) {
|
||||
super.init(context: context, authContext: authContext, optionalMastodonUser: mastodonUser)
|
||||
}
|
||||
|
|
|
@ -55,10 +55,12 @@ extension FavoriteViewModel.State {
|
|||
super.didEnter(from: previousState)
|
||||
guard let viewModel = viewModel, let stateMachine = stateMachine else { return }
|
||||
|
||||
// reset
|
||||
viewModel.statusFetchedResultsController.statusIDs = []
|
||||
|
||||
stateMachine.enter(Loading.self)
|
||||
Task {
|
||||
// reset
|
||||
await viewModel.statusFetchedResultsController.reset()
|
||||
|
||||
stateMachine.enter(Loading.self)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -127,10 +129,10 @@ extension FavoriteViewModel.State {
|
|||
)
|
||||
|
||||
var hasNewStatusesAppend = false
|
||||
var statusIDs = viewModel.statusFetchedResultsController.statusIDs
|
||||
var statusIDs = await viewModel.statusFetchedResultsController.records
|
||||
for status in response.value {
|
||||
guard !statusIDs.contains(status.id) else { continue }
|
||||
statusIDs.append(status.id)
|
||||
guard !statusIDs.contains(where: { $0.id == status.id }) else { continue }
|
||||
statusIDs.append(.fromEntity(status))
|
||||
hasNewStatusesAppend = true
|
||||
}
|
||||
|
||||
|
@ -146,7 +148,7 @@ extension FavoriteViewModel.State {
|
|||
} else {
|
||||
await enter(state: NoMore.self)
|
||||
}
|
||||
viewModel.statusFetchedResultsController.statusIDs = statusIDs
|
||||
await viewModel.statusFetchedResultsController.setRecords(statusIDs)
|
||||
} catch {
|
||||
await enter(state: Fail.self)
|
||||
}
|
||||
|
|
|
@ -37,14 +37,11 @@ final class FavoriteViewModel {
|
|||
return stateMachine
|
||||
}()
|
||||
|
||||
@MainActor
|
||||
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()
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -14,6 +14,7 @@ import MastodonSDK
|
|||
|
||||
final class MeProfileViewModel: ProfileViewModel {
|
||||
|
||||
@MainActor
|
||||
init(context: AppContext, authContext: AuthContext) {
|
||||
let user = authContext.mastodonAuthenticationBox.authentication.user(in: context.managedObjectContext)
|
||||
super.init(
|
||||
|
|
|
@ -56,6 +56,7 @@ class ProfileViewModel: NSObject {
|
|||
// @Published var protected: Bool? = nil
|
||||
// let needsPagePinToTop = CurrentValueSubject<Bool, Never>(false)
|
||||
|
||||
@MainActor
|
||||
init(context: AppContext, authContext: AuthContext, optionalMastodonUser mastodonUser: MastodonUser?) {
|
||||
self.context = context
|
||||
self.authContext = authContext
|
||||
|
|
|
@ -13,6 +13,7 @@ import MastodonCore
|
|||
|
||||
final class RemoteProfileViewModel: ProfileViewModel {
|
||||
|
||||
@MainActor
|
||||
init(context: AppContext, authContext: AuthContext, userID: Mastodon.Entity.Account.ID) {
|
||||
super.init(context: context, authContext: authContext, optionalMastodonUser: nil)
|
||||
|
||||
|
@ -51,6 +52,7 @@ final class RemoteProfileViewModel: ProfileViewModel {
|
|||
.store(in: &disposeBag)
|
||||
}
|
||||
|
||||
@MainActor
|
||||
init(context: AppContext, authContext: AuthContext, notificationID: Mastodon.Entity.Notification.ID) {
|
||||
super.init(context: context, authContext: authContext, optionalMastodonUser: nil)
|
||||
|
||||
|
@ -89,6 +91,7 @@ final class RemoteProfileViewModel: ProfileViewModel {
|
|||
} // end Task
|
||||
}
|
||||
|
||||
@MainActor
|
||||
init(context: AppContext, authContext: AuthContext, acct: String){
|
||||
super.init(context: context, authContext: authContext, optionalMastodonUser: nil)
|
||||
|
||||
|
|
|
@ -54,11 +54,13 @@ extension UserTimelineViewModel.State {
|
|||
override func didEnter(from previousState: GKState?) {
|
||||
super.didEnter(from: previousState)
|
||||
guard let viewModel = viewModel, let stateMachine = stateMachine else { return }
|
||||
|
||||
// reset
|
||||
viewModel.statusFetchedResultsController.statusIDs = []
|
||||
|
||||
stateMachine.enter(Loading.self)
|
||||
Task {
|
||||
// reset
|
||||
await viewModel.statusFetchedResultsController.reset()
|
||||
|
||||
stateMachine.enter(Loading.self)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -112,17 +114,17 @@ extension UserTimelineViewModel.State {
|
|||
super.didEnter(from: previousState)
|
||||
guard let viewModel = viewModel, let stateMachine = stateMachine else { return }
|
||||
|
||||
let maxID = viewModel.statusFetchedResultsController.statusIDs.last
|
||||
|
||||
guard let userID = viewModel.userIdentifier?.userID, !userID.isEmpty else {
|
||||
stateMachine.enter(Fail.self)
|
||||
return
|
||||
}
|
||||
|
||||
let queryFilter = viewModel.queryFilter
|
||||
|
||||
Task {
|
||||
|
||||
let maxID = await viewModel.statusFetchedResultsController.records.last?.id
|
||||
|
||||
guard let userID = viewModel.userIdentifier?.userID, !userID.isEmpty else {
|
||||
stateMachine.enter(Fail.self)
|
||||
return
|
||||
}
|
||||
|
||||
let queryFilter = viewModel.queryFilter
|
||||
|
||||
do {
|
||||
let response = try await viewModel.context.apiService.userTimeline(
|
||||
accountID: userID,
|
||||
|
@ -135,10 +137,10 @@ extension UserTimelineViewModel.State {
|
|||
)
|
||||
|
||||
var hasNewStatusesAppend = false
|
||||
var statusIDs = viewModel.statusFetchedResultsController.statusIDs
|
||||
var statusIDs = await viewModel.statusFetchedResultsController.records
|
||||
for status in response.value {
|
||||
guard !statusIDs.contains(status.id) else { continue }
|
||||
statusIDs.append(status.id)
|
||||
guard !statusIDs.contains(where: { $0.id == status.id }) else { continue }
|
||||
statusIDs.append(.fromEntity(status))
|
||||
hasNewStatusesAppend = true
|
||||
}
|
||||
|
||||
|
@ -147,7 +149,7 @@ extension UserTimelineViewModel.State {
|
|||
} else {
|
||||
await enter(state: NoMore.self)
|
||||
}
|
||||
viewModel.statusFetchedResultsController.statusIDs = statusIDs
|
||||
await viewModel.statusFetchedResultsController.setRecords(statusIDs)
|
||||
|
||||
} catch {
|
||||
await enter(state: Fail.self)
|
||||
|
|
|
@ -48,6 +48,7 @@ final class UserTimelineViewModel {
|
|||
return stateMachine
|
||||
}()
|
||||
|
||||
@MainActor
|
||||
init(
|
||||
context: AppContext,
|
||||
authContext: AuthContext,
|
||||
|
@ -57,11 +58,7 @@ 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()
|
||||
self.queryFilter = queryFilter
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,6 +10,7 @@ import Combine
|
|||
import CoreDataStack
|
||||
import GameplayKit
|
||||
import MastodonCore
|
||||
import MastodonSDK
|
||||
|
||||
final class UserListViewModel {
|
||||
var disposeBag = Set<AnyCancellable>()
|
||||
|
@ -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: MastodonStatus)
|
||||
case favoritedBy(status: MastodonStatus)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -29,17 +29,18 @@ class ReportViewModel {
|
|||
let context: AppContext
|
||||
let authContext: AuthContext
|
||||
let user: ManagedObjectRecord<MastodonUser>
|
||||
let status: ManagedObjectRecord<Status>?
|
||||
let status: MastodonStatus?
|
||||
|
||||
// output
|
||||
@Published var isReporting = false
|
||||
@Published var isReportSuccess = false
|
||||
|
||||
@MainActor
|
||||
init(
|
||||
context: AppContext,
|
||||
authContext: AuthContext,
|
||||
user: ManagedObjectRecord<MastodonUser>,
|
||||
status: ManagedObjectRecord<Status>?
|
||||
status: MastodonStatus?
|
||||
) {
|
||||
self.context = context
|
||||
self.authContext = authContext
|
||||
|
@ -101,17 +102,15 @@ extension ReportViewModel {
|
|||
|
||||
// the status picker is essential step in report flow
|
||||
// only check isSkip or not
|
||||
let statusIDs: [Status.ID]? = {
|
||||
let statusIDs: [MastodonStatus.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
|
||||
let _id: MastodonStatus.ID? = self.reportStatusViewModel.status.flatMap { record -> MastodonStatus.ID? in
|
||||
return record.id
|
||||
}
|
||||
return _id.flatMap { [$0] }
|
||||
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
|
||||
return self.reportStatusViewModel.selectStatuses.compactMap { record -> MastodonStatus.ID? in
|
||||
return record.id
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
|
|
@ -64,9 +64,10 @@ extension ReportStatusViewModel.State {
|
|||
super.didEnter(from: previousState)
|
||||
guard let viewModel else { return }
|
||||
|
||||
let maxID = viewModel.statusFetchedResultsController.statusIDs.last
|
||||
|
||||
Task {
|
||||
let maxID = await viewModel.statusFetchedResultsController.records.last?.id
|
||||
|
||||
let managedObjectContext = viewModel.context.managedObjectContext
|
||||
let _userID: MastodonUser.ID? = try await managedObjectContext.perform {
|
||||
guard let user = viewModel.user.object(in: managedObjectContext) else { return nil }
|
||||
|
@ -89,10 +90,10 @@ extension ReportStatusViewModel.State {
|
|||
)
|
||||
|
||||
var hasNewStatusesAppend = false
|
||||
var statusIDs = viewModel.statusFetchedResultsController.statusIDs
|
||||
var statusIDs = await viewModel.statusFetchedResultsController.records
|
||||
for status in response.value {
|
||||
guard !statusIDs.contains(status.id) else { continue }
|
||||
statusIDs.append(status.id)
|
||||
guard !statusIDs.contains(where: { $0.id == status.id }) else { continue }
|
||||
statusIDs.append(.fromEntity(status))
|
||||
hasNewStatusesAppend = true
|
||||
}
|
||||
|
||||
|
@ -101,7 +102,7 @@ extension ReportStatusViewModel.State {
|
|||
} else {
|
||||
await enter(state: NoMore.self)
|
||||
}
|
||||
viewModel.statusFetchedResultsController.statusIDs = statusIDs
|
||||
await viewModel.statusFetchedResultsController.setRecords(statusIDs)
|
||||
|
||||
} catch {
|
||||
await enter(state: Fail.self)
|
||||
|
|
|
@ -25,12 +25,12 @@ class ReportStatusViewModel {
|
|||
let context: AppContext
|
||||
let authContext: AuthContext
|
||||
let user: ManagedObjectRecord<MastodonUser>
|
||||
let status: ManagedObjectRecord<Status>?
|
||||
let status: MastodonStatus?
|
||||
let statusFetchedResultsController: StatusFetchedResultsController
|
||||
let listBatchFetchViewModel = ListBatchFetchViewModel()
|
||||
|
||||
@Published var isSkip = false
|
||||
@Published var selectStatuses = OrderedSet<ManagedObjectRecord<Status>>()
|
||||
@Published var selectStatuses = OrderedSet<MastodonStatus>()
|
||||
|
||||
// output
|
||||
var diffableDataSource: UITableViewDiffableDataSource<ReportSection, ReportItem>?
|
||||
|
@ -48,21 +48,18 @@ class ReportStatusViewModel {
|
|||
|
||||
@Published var isNextButtonEnabled = false
|
||||
|
||||
@MainActor
|
||||
init(
|
||||
context: AppContext,
|
||||
authContext: AuthContext,
|
||||
user: ManagedObjectRecord<MastodonUser>,
|
||||
status: ManagedObjectRecord<Status>?
|
||||
status: MastodonStatus?
|
||||
) {
|
||||
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()
|
||||
// end init
|
||||
|
||||
if let status = status {
|
||||
|
|
|
@ -6,13 +6,13 @@
|
|||
//
|
||||
|
||||
import UIKit
|
||||
import CoreDataStack
|
||||
import MastodonSDK
|
||||
|
||||
extension ReportStatusTableViewCell {
|
||||
final class ViewModel {
|
||||
let value: Status
|
||||
let value: MastodonStatus
|
||||
|
||||
init(value: Status) {
|
||||
init(value: MastodonStatus) {
|
||||
self.value = value
|
||||
}
|
||||
}
|
||||
|
|
|
@ -87,7 +87,8 @@ class MainTabBarController: UITabBarController {
|
|||
return image.withRenderingMode(.alwaysTemplate).resized(size: CGSize(width: 80, height: 80))
|
||||
}
|
||||
|
||||
func viewController(context: AppContext, authContext: AuthContext?, coordinator: SceneCoordinator) -> UIViewController {
|
||||
@MainActor
|
||||
func viewController(context: AppContext, authContext: AuthContext?, coordinator: SceneCoordinator) async -> UIViewController {
|
||||
guard let authContext = authContext else {
|
||||
return UITableViewController()
|
||||
}
|
||||
|
@ -170,21 +171,26 @@ extension MainTabBarController {
|
|||
view.backgroundColor = .systemBackground
|
||||
|
||||
// seealso: `ThemeService.apply(theme:)`
|
||||
let tabs = Tab.allCases
|
||||
let viewControllers: [UIViewController] = tabs.map { tab in
|
||||
let viewController = tab.viewController(context: context, authContext: authContext, coordinator: coordinator)
|
||||
viewController.tabBarItem.tag = tab.tag
|
||||
viewController.tabBarItem.title = tab.title // needs for acessiblity large content label
|
||||
viewController.tabBarItem.image = tab.image.imageWithoutBaseline()
|
||||
viewController.tabBarItem.largeContentSizeImage = tab.largeImage.imageWithoutBaseline()
|
||||
viewController.tabBarItem.accessibilityLabel = tab.title
|
||||
viewController.tabBarItem.accessibilityUserInputLabels = tab.inputLabels
|
||||
viewController.tabBarItem.imageInsets = UIEdgeInsets(top: 6, left: 0, bottom: -6, right: 0)
|
||||
return viewController
|
||||
Task { @MainActor in
|
||||
let tabs = Tab.allCases
|
||||
var viewControllers = [UIViewController]()
|
||||
|
||||
for tab in tabs {
|
||||
let viewController = await tab.viewController(context: context, authContext: authContext, coordinator: coordinator)
|
||||
viewController.tabBarItem.tag = tab.tag
|
||||
viewController.tabBarItem.title = tab.title // needs for acessiblity large content label
|
||||
viewController.tabBarItem.image = tab.image.imageWithoutBaseline()
|
||||
viewController.tabBarItem.largeContentSizeImage = tab.largeImage.imageWithoutBaseline()
|
||||
viewController.tabBarItem.accessibilityLabel = tab.title
|
||||
viewController.tabBarItem.accessibilityUserInputLabels = tab.inputLabels
|
||||
viewController.tabBarItem.imageInsets = UIEdgeInsets(top: 6, left: 0, bottom: -6, right: 0)
|
||||
viewControllers.append(viewController)
|
||||
}
|
||||
|
||||
_viewControllers = viewControllers
|
||||
setViewControllers(viewControllers, animated: false)
|
||||
selectedIndex = 0
|
||||
}
|
||||
_viewControllers = viewControllers
|
||||
setViewControllers(viewControllers, animated: false)
|
||||
selectedIndex = 0
|
||||
|
||||
// hacky workaround for FB11986255 (Setting accessibilityUserInputLabels on a UITabBarItem has no effect)
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(50)) {
|
||||
|
|
|
@ -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: MastodonStatus.fromEntity(status)
|
||||
)
|
||||
} else if let url = URL(string: urlString) {
|
||||
let prefixedURL: URL?
|
||||
|
|
|
@ -12,7 +12,7 @@ import MastodonSDK
|
|||
|
||||
enum SearchResultItem: Hashable {
|
||||
case user(ManagedObjectRecord<MastodonUser>)
|
||||
case status(ManagedObjectRecord<Status>)
|
||||
case status(MastodonStatus)
|
||||
case hashtag(tag: Mastodon.Entity.Tag)
|
||||
case bottomLoader(attribute: BottomLoaderAttribute)
|
||||
}
|
||||
|
|
|
@ -58,18 +58,15 @@ extension SearchResultSection {
|
|||
)
|
||||
}
|
||||
return cell
|
||||
case .status(let record):
|
||||
case .status(let status):
|
||||
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(status)),
|
||||
configuration: configuration
|
||||
)
|
||||
return cell
|
||||
case .hashtag(let tag):
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: HashtagTableViewCell.self)) as! HashtagTableViewCell
|
||||
|
|
|
@ -124,7 +124,7 @@ extension SearchResultViewModel.State {
|
|||
guard stateMachine.currentState is Loading else { return }
|
||||
|
||||
let userIDs = response.value.accounts.map { $0.id }
|
||||
let statusIDs = response.value.statuses.map { $0.id }
|
||||
let statusIDs = response.value.statuses.map { MastodonStatus.fromEntity($0) }
|
||||
|
||||
let isNoMore = userIDs.isEmpty && statusIDs.isEmpty
|
||||
|
||||
|
@ -137,12 +137,12 @@ extension SearchResultViewModel.State {
|
|||
// reset data source when the search is refresh
|
||||
if offset == nil {
|
||||
viewModel.userFetchedResultsController.userIDs = []
|
||||
viewModel.statusFetchedResultsController.statusIDs = []
|
||||
await viewModel.statusFetchedResultsController.reset()
|
||||
viewModel.hashtags = []
|
||||
}
|
||||
|
||||
viewModel.userFetchedResultsController.append(userIDs: userIDs)
|
||||
viewModel.statusFetchedResultsController.append(statusIDs: statusIDs)
|
||||
await viewModel.statusFetchedResultsController.appendRecords(statusIDs)
|
||||
|
||||
var hashtags = viewModel.hashtags
|
||||
for hashtag in response.value.hashtags where !hashtags.contains(hashtag) {
|
||||
|
|
|
@ -45,6 +45,7 @@ final class SearchResultViewModel {
|
|||
}()
|
||||
let didDataSourceUpdate = PassthroughSubject<Void, Never>()
|
||||
|
||||
@MainActor
|
||||
init(context: AppContext, authContext: AuthContext, searchScope: SearchScope = .all, searchText: String) {
|
||||
self.context = context
|
||||
self.authContext = authContext
|
||||
|
@ -56,10 +57,6 @@ final class SearchResultViewModel {
|
|||
domain: authContext.mastodonAuthenticationBox.domain,
|
||||
additionalPredicate: nil
|
||||
)
|
||||
self.statusFetchedResultsController = StatusFetchedResultsController(
|
||||
managedObjectContext: context.managedObjectContext,
|
||||
domain: authContext.mastodonAuthenticationBox.domain,
|
||||
additionalTweetPredicate: nil
|
||||
)
|
||||
self.statusFetchedResultsController = StatusFetchedResultsController()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,30 +16,29 @@ import MastodonAsset
|
|||
import MastodonCore
|
||||
import MastodonLocalization
|
||||
import class CoreDataStack.Notification
|
||||
import MastodonSDK
|
||||
|
||||
extension NotificationView {
|
||||
public func configure(feed: Feed) {
|
||||
guard let notification = feed.notification else {
|
||||
public func configure(feed: MastodonFeed) {
|
||||
guard
|
||||
let notification = feed.notification,
|
||||
let managedObjectContext = viewModel.context?.managedObjectContext
|
||||
else {
|
||||
assertionFailure()
|
||||
return
|
||||
}
|
||||
|
||||
configure(notification: notification)
|
||||
|
||||
MastodonNotification.fromEntity(notification, using: managedObjectContext).map(configure(notification:))
|
||||
}
|
||||
}
|
||||
|
||||
extension NotificationView {
|
||||
public func configure(notification: Notification) {
|
||||
public func configure(notification: MastodonNotification) {
|
||||
viewModel.objects.insert(notification)
|
||||
|
||||
configureAuthor(notification: notification)
|
||||
|
||||
guard let type = MastodonNotificationType(rawValue: notification.typeRaw) else {
|
||||
assertionFailure()
|
||||
return
|
||||
}
|
||||
|
||||
switch type {
|
||||
|
||||
switch notification.entity.type {
|
||||
case .follow:
|
||||
setAuthorContainerBottomPaddingViewDisplay()
|
||||
case .followRequest:
|
||||
|
@ -63,7 +62,7 @@ extension NotificationView {
|
|||
}
|
||||
|
||||
extension NotificationView {
|
||||
private func configureAuthor(notification: Notification) {
|
||||
private func configureAuthor(notification: MastodonNotification) {
|
||||
let author = notification.account
|
||||
// author avatar
|
||||
|
||||
|
@ -98,19 +97,18 @@ extension NotificationView {
|
|||
.assign(to: \.authorUsername, on: viewModel)
|
||||
.store(in: &disposeBag)
|
||||
// timestamp
|
||||
viewModel.timestamp = notification.createAt
|
||||
viewModel.timestamp = notification.entity.createdAt
|
||||
|
||||
viewModel.visibility = notification.status?.visibility ?? ._other("")
|
||||
viewModel.visibility = notification.entity.status?.mastodonVisibility ?? ._other("")
|
||||
|
||||
// notification type indicator
|
||||
Publishers.CombineLatest3(
|
||||
notification.publisher(for: \.typeRaw),
|
||||
Publishers.CombineLatest(
|
||||
author.publisher(for: \.displayName),
|
||||
author.publisher(for: \.emojis)
|
||||
)
|
||||
.sink { [weak self] typeRaw, _, emojis in
|
||||
.sink { [weak self] _, emojis in
|
||||
guard let self = self else { return }
|
||||
guard let type = MastodonNotificationType(rawValue: typeRaw) else {
|
||||
guard let type = MastodonNotificationType(rawValue: notification.entity.type.rawValue) else {
|
||||
self.viewModel.notificationIndicatorText = nil
|
||||
return
|
||||
}
|
||||
|
@ -205,13 +203,8 @@ extension NotificationView {
|
|||
.store(in: &disposeBag)
|
||||
|
||||
// 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)
|
||||
viewModel.followRequestState = notification.followRequestState
|
||||
viewModel.transientFollowRequestState = notification.transientFollowRequestState
|
||||
|
||||
// Following
|
||||
author.publisher(for: \.followingBy)
|
||||
|
|
|
@ -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(MastodonFeed)
|
||||
case status(MastodonStatus)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -38,13 +38,7 @@ 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)
|
||||
|
|
|
@ -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(MastodonStatus)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,12 +6,12 @@
|
|||
//
|
||||
|
||||
import Foundation
|
||||
import CoreDataStack
|
||||
import MastodonSDK
|
||||
import MastodonCore
|
||||
|
||||
final class CachedThreadViewModel: ThreadViewModel {
|
||||
init(context: AppContext, authContext: AuthContext, status: Status) {
|
||||
let threadContext = StatusItem.Thread.Context(status: .init(objectID: status.objectID))
|
||||
init(context: AppContext, authContext: AuthContext, status: MastodonStatus) {
|
||||
let threadContext = StatusItem.Thread.Context(status: status)
|
||||
super.init(
|
||||
context: context,
|
||||
authContext: authContext,
|
||||
|
|
|
@ -72,7 +72,7 @@ class StatusEditHistoryTableViewCell: UITableViewCell {
|
|||
NSLayoutConstraint.activate(constraints)
|
||||
}
|
||||
|
||||
func configure(status: Status, statusEdit: Mastodon.Entity.StatusEdit, dateText: String) {
|
||||
func configure(status: MastodonStatus, statusEdit: Mastodon.Entity.StatusEdit, dateText: String) {
|
||||
dateLabel.text = dateText
|
||||
statusHistoryView.statusView.configure(status: status, statusEdit: statusEdit)
|
||||
}
|
||||
|
|
|
@ -8,7 +8,7 @@ import UIKit
|
|||
import MastodonSDK
|
||||
|
||||
struct StatusEditHistoryViewModel {
|
||||
let status: Status
|
||||
let status: MastodonStatus
|
||||
let edits: [Mastodon.Entity.StatusEdit]
|
||||
|
||||
let appContext: AppContext
|
||||
|
|
|
@ -20,7 +20,7 @@ final class MastodonStatusThreadViewModel {
|
|||
|
||||
// input
|
||||
let context: AppContext
|
||||
@Published private(set) var deletedObjectIDs: Set<NSManagedObjectID> = Set()
|
||||
@Published private(set) var deletedObjectIDs: Set<MastodonStatus.ID> = Set()
|
||||
|
||||
// output
|
||||
@Published var __ancestors: [StatusItem] = []
|
||||
|
@ -41,7 +41,7 @@ final class MastodonStatusThreadViewModel {
|
|||
let newItems = items.filter { item in
|
||||
switch item {
|
||||
case .thread(let thread):
|
||||
return !deletedObjectIDs.contains(thread.record.objectID)
|
||||
return !deletedObjectIDs.contains(thread.record.id)
|
||||
default:
|
||||
assertionFailure()
|
||||
return false
|
||||
|
@ -60,7 +60,7 @@ final class MastodonStatusThreadViewModel {
|
|||
let newItems = items.filter { item in
|
||||
switch item {
|
||||
case .thread(let thread):
|
||||
return !deletedObjectIDs.contains(thread.record.objectID)
|
||||
return !deletedObjectIDs.contains(thread.record.id)
|
||||
default:
|
||||
assertionFailure()
|
||||
return false
|
||||
|
@ -94,19 +94,20 @@ extension MastodonStatusThreadViewModel {
|
|||
}
|
||||
|
||||
var newItems: [StatusItem] = []
|
||||
for (i, node) in nodes.enumerated() {
|
||||
guard let status = dictionary[node.statusID] else { continue }
|
||||
let isLast = i == nodes.count - 1
|
||||
|
||||
let record = ManagedObjectRecord<Status>(objectID: status.objectID)
|
||||
let context = StatusItem.Thread.Context(
|
||||
status: record,
|
||||
displayUpperConversationLink: !isLast,
|
||||
displayBottomConversationLink: true
|
||||
)
|
||||
let item = StatusItem.thread(.leaf(context: context))
|
||||
newItems.append(item)
|
||||
}
|
||||
#warning("Potentially this can be removed and replaced by native threading logic")
|
||||
// for (i, node) in nodes.enumerated() {
|
||||
// guard let status = dictionary[node.statusID] else { continue }
|
||||
// let isLast = i == nodes.count - 1
|
||||
//
|
||||
// let record = ManagedObjectRecord<Status>(objectID: status.objectID)
|
||||
// let context = StatusItem.Thread.Context(
|
||||
// status: record,
|
||||
// displayUpperConversationLink: !isLast,
|
||||
// displayBottomConversationLink: true
|
||||
// )
|
||||
// let item = StatusItem.thread(.leaf(context: context))
|
||||
// newItems.append(item)
|
||||
// }
|
||||
|
||||
let items = self.__ancestors + newItems
|
||||
self.__ancestors = items
|
||||
|
@ -132,31 +133,32 @@ extension MastodonStatusThreadViewModel {
|
|||
}
|
||||
|
||||
var newItems: [StatusItem] = []
|
||||
for node in nodes {
|
||||
guard let status = dictionary[node.statusID] else { continue }
|
||||
// first tier
|
||||
let record = ManagedObjectRecord<Status>(objectID: status.objectID)
|
||||
let context = StatusItem.Thread.Context(
|
||||
status: record
|
||||
)
|
||||
let item = StatusItem.thread(.leaf(context: context))
|
||||
newItems.append(item)
|
||||
|
||||
// second tier
|
||||
if let child = node.children.first {
|
||||
guard let secondaryStatus = dictionary[child.statusID] else { continue }
|
||||
let secondaryRecord = ManagedObjectRecord<Status>(objectID: secondaryStatus.objectID)
|
||||
let secondaryContext = StatusItem.Thread.Context(
|
||||
status: secondaryRecord,
|
||||
displayUpperConversationLink: true
|
||||
)
|
||||
let secondaryItem = StatusItem.thread(.leaf(context: secondaryContext))
|
||||
newItems.append(secondaryItem)
|
||||
|
||||
// update first tier context
|
||||
context.displayBottomConversationLink = true
|
||||
}
|
||||
}
|
||||
#warning("Potentially this can be removed and replaced by native threading logic")
|
||||
// for node in nodes {
|
||||
// guard let status = dictionary[node.statusID] else { continue }
|
||||
// // first tier
|
||||
// let record = ManagedObjectRecord<Status>(objectID: status.objectID)
|
||||
// let context = StatusItem.Thread.Context(
|
||||
// status: record
|
||||
// )
|
||||
// let item = StatusItem.thread(.leaf(context: context))
|
||||
// newItems.append(item)
|
||||
//
|
||||
// // second tier
|
||||
// if let child = node.children.first {
|
||||
// guard let secondaryStatus = dictionary[child.statusID] else { continue }
|
||||
// let secondaryRecord = ManagedObjectRecord<Status>(objectID: secondaryStatus.objectID)
|
||||
// let secondaryContext = StatusItem.Thread.Context(
|
||||
// status: secondaryRecord,
|
||||
// displayUpperConversationLink: true
|
||||
// )
|
||||
// let secondaryItem = StatusItem.thread(.leaf(context: secondaryContext))
|
||||
// newItems.append(secondaryItem)
|
||||
//
|
||||
// // update first tier context
|
||||
// context.displayBottomConversationLink = true
|
||||
// }
|
||||
// }
|
||||
|
||||
var items = self.__descendants
|
||||
for item in newItems {
|
||||
|
@ -262,12 +264,3 @@ extension MastodonStatusThreadViewModel.Node {
|
|||
|
||||
}
|
||||
|
||||
extension MastodonStatusThreadViewModel {
|
||||
func delete(objectIDs: [NSManagedObjectID]) {
|
||||
var set = deletedObjectIDs
|
||||
for objectID in objectIDs {
|
||||
set.insert(objectID)
|
||||
}
|
||||
self.deletedObjectIDs = set
|
||||
}
|
||||
}
|
||||
|
|
|
@ -30,15 +30,7 @@ final class RemoteThreadViewModel: ThreadViewModel {
|
|||
authenticationBox: authContext.mastodonAuthenticationBox
|
||||
)
|
||||
|
||||
let managedObjectContext = context.managedObjectContext
|
||||
let request = Status.sortedFetchRequest
|
||||
request.fetchLimit = 1
|
||||
request.predicate = Status.predicate(domain: domain, id: response.value.id)
|
||||
guard let status = managedObjectContext.safeFetch(request).first else {
|
||||
assertionFailure()
|
||||
return
|
||||
}
|
||||
let threadContext = StatusItem.Thread.Context(status: .init(objectID: status.objectID))
|
||||
let threadContext = StatusItem.Thread.Context(status: .fromEntity(response.value))
|
||||
self.root = .root(context: threadContext)
|
||||
|
||||
} // end Task
|
||||
|
@ -62,17 +54,9 @@ final class RemoteThreadViewModel: ThreadViewModel {
|
|||
authenticationBox: authContext.mastodonAuthenticationBox
|
||||
)
|
||||
|
||||
guard let statusID = response.value.status?.id else { return }
|
||||
guard let status = response.value.status else { return }
|
||||
|
||||
let managedObjectContext = context.managedObjectContext
|
||||
let request = Status.sortedFetchRequest
|
||||
request.fetchLimit = 1
|
||||
request.predicate = Status.predicate(domain: domain, id: statusID)
|
||||
guard let status = managedObjectContext.safeFetch(request).first else {
|
||||
assertionFailure()
|
||||
return
|
||||
}
|
||||
let threadContext = StatusItem.Thread.Context(status: .init(objectID: status.objectID))
|
||||
let threadContext = StatusItem.Thread.Context(status: .fromEntity(status))
|
||||
self.root = .root(context: threadContext)
|
||||
} // end Task
|
||||
}
|
||||
|
|
|
@ -38,8 +38,7 @@ extension ThreadViewModel {
|
|||
snapshot.appendSections([.main])
|
||||
if let root = self.root {
|
||||
if case let .root(threadContext) = root,
|
||||
let status = threadContext.status.object(in: context.managedObjectContext),
|
||||
status.inReplyToID != nil
|
||||
threadContext.status.entity.inReplyToID != nil
|
||||
{
|
||||
snapshot.appendItems([.topLoader], toSection: .main)
|
||||
}
|
||||
|
@ -81,8 +80,7 @@ extension ThreadViewModel {
|
|||
// top loader
|
||||
let _hasReplyTo: Bool? = try? await self.context.managedObjectContext.perform {
|
||||
guard case let .root(threadContext) = root else { return nil }
|
||||
guard let status = threadContext.status.object(in: self.context.managedObjectContext) else { return nil }
|
||||
return status.inReplyToID != nil
|
||||
return threadContext.status.entity.inReplyToID != nil
|
||||
}
|
||||
if let hasReplyTo = _hasReplyTo, hasReplyTo {
|
||||
let state = self.loadThreadStateMachine.currentState
|
||||
|
|
|
@ -55,40 +55,25 @@ class ThreadViewModel {
|
|||
self.root = optionalRoot
|
||||
self.mastodonStatusThreadViewModel = MastodonStatusThreadViewModel(context: context)
|
||||
// end init
|
||||
|
||||
ManagedObjectObserver.observe(context: context.managedObjectContext)
|
||||
.sink(receiveCompletion: { completion in
|
||||
// do nohting
|
||||
}, receiveValue: { [weak self] changes in
|
||||
guard let self = self else { return }
|
||||
|
||||
let objectIDs: [NSManagedObjectID] = changes.changeTypes.compactMap { changeType in
|
||||
guard case let .delete(object) = changeType else { return nil }
|
||||
return object.objectID
|
||||
}
|
||||
|
||||
self.delete(objectIDs: objectIDs)
|
||||
})
|
||||
.store(in: &disposeBag)
|
||||
|
||||
|
||||
$root
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] root in
|
||||
guard let self = self else { return }
|
||||
guard case let .root(threadContext) = root else { return }
|
||||
guard let status = threadContext.status.object(in: self.context.managedObjectContext) else { return }
|
||||
let status = threadContext.status
|
||||
|
||||
// bind threadContext
|
||||
self.threadContext = .init(
|
||||
domain: status.domain,
|
||||
domain: authContext.mastodonAuthenticationBox.domain, //status.domain,
|
||||
statusID: status.id,
|
||||
replyToID: status.inReplyToID
|
||||
replyToID: status.entity.inReplyToID
|
||||
)
|
||||
|
||||
// bind titleView
|
||||
self.navigationBarTitle = {
|
||||
let title = L10n.Scene.Thread.title(status.author.displayNameWithFallback)
|
||||
let content = MastodonContent(content: title, emojis: status.author.emojis.asDictionary)
|
||||
let title = L10n.Scene.Thread.title(status.entity.account.displayNameWithFallback)
|
||||
let content = MastodonContent(content: title, emojis: status.entity.account.emojis?.asDictionary ?? [:])
|
||||
return try? MastodonMetaContent.convert(document: content)
|
||||
}()
|
||||
}
|
||||
|
@ -116,16 +101,3 @@ extension ThreadViewModel {
|
|||
}
|
||||
|
||||
}
|
||||
|
||||
extension ThreadViewModel {
|
||||
func delete(objectIDs: [NSManagedObjectID]) {
|
||||
if let root = self.root,
|
||||
case let .root(threadContext) = root,
|
||||
objectIDs.contains(threadContext.status.objectID)
|
||||
{
|
||||
self.root = nil
|
||||
}
|
||||
|
||||
self.mastodonStatusThreadViewModel.delete(objectIDs: objectIDs)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,87 +9,11 @@
|
|||
import Foundation
|
||||
import UIKit
|
||||
import Combine
|
||||
import CoreData
|
||||
import CoreDataStack
|
||||
import MastodonSDK
|
||||
|
||||
final public class FeedFetchedResultsController: NSObject {
|
||||
|
||||
private enum Constants {
|
||||
static let defaultFetchLimit = 100
|
||||
}
|
||||
|
||||
var disposeBag = Set<AnyCancellable>()
|
||||
|
||||
private let fetchedResultsController: NSFetchedResultsController<Feed>
|
||||
|
||||
public var managedObjectContext: NSManagedObjectContext {
|
||||
fetchedResultsController.managedObjectContext
|
||||
}
|
||||
|
||||
// input
|
||||
@Published public var predicate = Feed.predicate(kind: .none, acct: .none)
|
||||
|
||||
// output
|
||||
private let _objectIDs = PassthroughSubject<[NSManagedObjectID], Never>()
|
||||
@Published public var records: [ManagedObjectRecord<Feed>] = []
|
||||
final public class FeedFetchedResultsController {
|
||||
|
||||
public func fetchNextBatch() {
|
||||
fetchedResultsController.fetchRequest.fetchLimit += Constants.defaultFetchLimit
|
||||
try? fetchedResultsController.performFetch()
|
||||
}
|
||||
|
||||
public init(managedObjectContext: NSManagedObjectContext) {
|
||||
self.fetchedResultsController = {
|
||||
let fetchRequest = Feed.sortedFetchRequest
|
||||
// make sure initial query return empty results
|
||||
fetchRequest.returnsObjectsAsFaults = false
|
||||
fetchRequest.shouldRefreshRefetchedObjects = true
|
||||
fetchRequest.fetchBatchSize = 15
|
||||
fetchRequest.fetchLimit = Constants.defaultFetchLimit
|
||||
let controller = NSFetchedResultsController(
|
||||
fetchRequest: fetchRequest,
|
||||
managedObjectContext: managedObjectContext,
|
||||
sectionNameKeyPath: nil,
|
||||
cacheName: nil
|
||||
)
|
||||
|
||||
return controller
|
||||
}()
|
||||
super.init()
|
||||
|
||||
// debounce output to prevent UI update issues
|
||||
_objectIDs
|
||||
.throttle(for: 0.1, scheduler: DispatchQueue.main, latest: true)
|
||||
.map { objectIDs in objectIDs.map { ManagedObjectRecord(objectID: $0) } }
|
||||
.assign(to: &$records)
|
||||
|
||||
fetchedResultsController.delegate = self
|
||||
|
||||
$predicate
|
||||
.removeDuplicates()
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] predicate in
|
||||
guard let self = self else { return }
|
||||
self.fetchedResultsController.fetchRequest.predicate = predicate
|
||||
do {
|
||||
try self.fetchedResultsController.performFetch()
|
||||
} catch {
|
||||
assertionFailure(error.localizedDescription)
|
||||
}
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
}
|
||||
@Published public var records: [MastodonFeed] = []
|
||||
|
||||
public init() {}
|
||||
}
|
||||
|
||||
// MARK: - NSFetchedResultsControllerDelegate
|
||||
extension FeedFetchedResultsController: NSFetchedResultsControllerDelegate {
|
||||
public func controller(
|
||||
_ controller: NSFetchedResultsController<NSFetchRequestResult>,
|
||||
didChangeContentWith snapshot: NSDiffableDataSourceSnapshotReference
|
||||
) {
|
||||
let snapshot = snapshot as NSDiffableDataSourceSnapshot<String, NSManagedObjectID>
|
||||
self._objectIDs.send(snapshot.itemIdentifiers)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -11,93 +11,27 @@ import CoreData
|
|||
import CoreDataStack
|
||||
import MastodonSDK
|
||||
|
||||
public final class StatusFetchedResultsController: NSObject {
|
||||
|
||||
var disposeBag = Set<AnyCancellable>()
|
||||
|
||||
let fetchedResultsController: NSFetchedResultsController<Status>
|
||||
|
||||
// input
|
||||
@Published public var domain: String? = nil
|
||||
@Published public var statusIDs: [Mastodon.Entity.Status.ID] = []
|
||||
public final class StatusFetchedResultsController {
|
||||
@MainActor
|
||||
@Published public private(set) var records: [MastodonStatus] = []
|
||||
|
||||
// output
|
||||
let _objectIDs = CurrentValueSubject<[NSManagedObjectID], Never>([])
|
||||
@Published public private(set) var records: [ManagedObjectRecord<Status>] = []
|
||||
|
||||
public init(managedObjectContext: NSManagedObjectContext, domain: String?, additionalTweetPredicate: NSPredicate?) {
|
||||
self.domain = domain ?? ""
|
||||
self.fetchedResultsController = {
|
||||
let fetchRequest = Status.sortedFetchRequest
|
||||
fetchRequest.predicate = Status.predicate(domain: domain ?? "", ids: [])
|
||||
fetchRequest.returnsObjectsAsFaults = false
|
||||
fetchRequest.fetchBatchSize = 20
|
||||
let controller = NSFetchedResultsController(
|
||||
fetchRequest: fetchRequest,
|
||||
managedObjectContext: managedObjectContext,
|
||||
sectionNameKeyPath: nil,
|
||||
cacheName: nil
|
||||
)
|
||||
|
||||
return controller
|
||||
}()
|
||||
super.init()
|
||||
|
||||
// debounce output to prevent UI update issues
|
||||
_objectIDs
|
||||
.throttle(for: 0.1, scheduler: DispatchQueue.main, latest: true)
|
||||
.map { objectIDs in objectIDs.map { ManagedObjectRecord(objectID: $0) } }
|
||||
.assign(to: &$records)
|
||||
|
||||
fetchedResultsController.delegate = self
|
||||
|
||||
Publishers.CombineLatest(
|
||||
self.$domain.removeDuplicates(),
|
||||
self.$statusIDs.removeDuplicates()
|
||||
)
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] domain, ids in
|
||||
guard let self = self else { return }
|
||||
var predicates = [Status.predicate(domain: domain ?? "", ids: ids)]
|
||||
if let additionalPredicate = additionalTweetPredicate {
|
||||
predicates.append(additionalPredicate)
|
||||
}
|
||||
self.fetchedResultsController.fetchRequest.predicate = NSCompoundPredicate(andPredicateWithSubpredicates: predicates)
|
||||
do {
|
||||
try self.fetchedResultsController.performFetch()
|
||||
} catch {
|
||||
assertionFailure(error.localizedDescription)
|
||||
}
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
@MainActor
|
||||
public init(records: [MastodonStatus] = []) {
|
||||
self.records = records
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension StatusFetchedResultsController {
|
||||
|
||||
public func append(statusIDs: [Mastodon.Entity.Status.ID]) {
|
||||
var result = self.statusIDs
|
||||
for statusID in statusIDs where !result.contains(statusID) {
|
||||
result.append(statusID)
|
||||
}
|
||||
self.statusIDs = result
|
||||
@MainActor
|
||||
public func reset() {
|
||||
records = []
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - NSFetchedResultsControllerDelegate
|
||||
extension StatusFetchedResultsController: NSFetchedResultsControllerDelegate {
|
||||
public func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChangeContentWith snapshot: NSDiffableDataSourceSnapshotReference) {
|
||||
let indexes = statusIDs
|
||||
let objects = fetchedResultsController.fetchedObjects ?? []
|
||||
|
||||
let items: [NSManagedObjectID] = objects
|
||||
.compactMap { object in
|
||||
indexes.firstIndex(of: object.id).map { index in (index, object) }
|
||||
}
|
||||
.sorted { $0.0 < $1.0 }
|
||||
.map { $0.1.objectID }
|
||||
self._objectIDs.value = items
|
||||
@MainActor
|
||||
public func setRecords(_ records: [MastodonStatus]) {
|
||||
self.records = records
|
||||
}
|
||||
|
||||
@MainActor
|
||||
public func appendRecords(_ records: [MastodonStatus]) {
|
||||
self.records += records
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,26 +19,26 @@ extension APIService {
|
|||
}
|
||||
|
||||
public func bookmark(
|
||||
record: ManagedObjectRecord<Status>,
|
||||
record: MastodonStatus,
|
||||
authenticationBox: MastodonAuthenticationBox
|
||||
) async throws -> Mastodon.Response.Content<Mastodon.Entity.Status> {
|
||||
|
||||
let managedObjectContext = backgroundManagedObjectContext
|
||||
|
||||
|
||||
// update bookmark state and retrieve bookmark context
|
||||
let bookmarkContext: MastodonBookmarkContext = try await managedObjectContext.performChanges {
|
||||
let authentication = authenticationBox.authentication
|
||||
|
||||
guard
|
||||
let _status = record.object(in: managedObjectContext),
|
||||
let me = authentication.user(in: managedObjectContext)
|
||||
else {
|
||||
throw APIError.implicit(.badRequest)
|
||||
}
|
||||
|
||||
|
||||
let _status = record.entity
|
||||
let status = _status.reblog ?? _status
|
||||
let isBookmarked = status.bookmarkedBy.contains(me)
|
||||
status.update(bookmarked: !isBookmarked, by: me)
|
||||
let isBookmarked = status.bookmarked == true
|
||||
|
||||
let context = MastodonBookmarkContext(
|
||||
statusID: status.id,
|
||||
isBookmarked: isBookmarked
|
||||
|
@ -60,38 +60,12 @@ extension APIService {
|
|||
} catch {
|
||||
result = .failure(error)
|
||||
}
|
||||
|
||||
let response = try result.get()
|
||||
|
||||
// update bookmark state
|
||||
try await managedObjectContext.performChanges {
|
||||
let authentication = authenticationBox.authentication
|
||||
|
||||
guard
|
||||
let _status = record.object(in: managedObjectContext),
|
||||
let me = authentication.user(in: managedObjectContext)
|
||||
else { return }
|
||||
|
||||
let status = _status.reblog ?? _status
|
||||
|
||||
switch result {
|
||||
case .success(let response):
|
||||
_ = Persistence.Status.createOrMerge(
|
||||
in: managedObjectContext,
|
||||
context: Persistence.Status.PersistContext(
|
||||
domain: authenticationBox.domain,
|
||||
entity: response.value,
|
||||
me: me,
|
||||
statusCache: nil,
|
||||
userCache: nil,
|
||||
networkDate: response.networkDate
|
||||
)
|
||||
)
|
||||
case .failure:
|
||||
// rollback
|
||||
status.update(bookmarked: bookmarkContext.isBookmarked, by: me)
|
||||
}
|
||||
}
|
||||
record.entity = response.value
|
||||
|
||||
let response = try result.get()
|
||||
return response
|
||||
}
|
||||
|
||||
|
|
|
@ -14,13 +14,13 @@ import CoreDataStack
|
|||
extension APIService {
|
||||
|
||||
private struct MastodonFavoriteContext {
|
||||
let statusID: Status.ID
|
||||
let statusID: MastodonStatus.ID
|
||||
let isFavorited: Bool
|
||||
let favoritedCount: Int64
|
||||
}
|
||||
|
||||
public func favorite(
|
||||
record: ManagedObjectRecord<Status>,
|
||||
status: MastodonStatus,
|
||||
authenticationBox: MastodonAuthenticationBox
|
||||
) async throws -> Mastodon.Response.Content<Mastodon.Entity.Status> {
|
||||
|
||||
|
@ -31,18 +31,15 @@ extension APIService {
|
|||
let authentication = authenticationBox.authentication
|
||||
|
||||
guard
|
||||
let _status = record.object(in: managedObjectContext),
|
||||
let me = authentication.user(in: managedObjectContext)
|
||||
else {
|
||||
throw APIError.implicit(.badRequest)
|
||||
}
|
||||
|
||||
let status = _status.reblog ?? _status
|
||||
let isFavorited = status.favouritedBy.contains(me)
|
||||
let favoritedCount = status.favouritesCount
|
||||
let favoriteCount = isFavorited ? favoritedCount - 1 : favoritedCount + 1
|
||||
status.update(liked: !isFavorited, by: me)
|
||||
status.update(favouritesCount: favoriteCount)
|
||||
let _status = status.reblog ?? status
|
||||
let isFavorited = status.entity.favourited == true
|
||||
let favoritedCount = Int64(status.entity.favouritesCount)
|
||||
|
||||
let context = MastodonFavoriteContext(
|
||||
statusID: status.id,
|
||||
isFavorited: isFavorited,
|
||||
|
@ -66,40 +63,6 @@ extension APIService {
|
|||
result = .failure(error)
|
||||
}
|
||||
|
||||
// update like state
|
||||
try await managedObjectContext.performChanges {
|
||||
let authentication = authenticationBox.authentication
|
||||
|
||||
guard
|
||||
let _status = record.object(in: managedObjectContext),
|
||||
let me = authentication.user(in: managedObjectContext)
|
||||
else { return }
|
||||
|
||||
let status = _status.reblog ?? _status
|
||||
|
||||
switch result {
|
||||
case .success(let response):
|
||||
_ = Persistence.Status.createOrMerge(
|
||||
in: managedObjectContext,
|
||||
context: Persistence.Status.PersistContext(
|
||||
domain: authenticationBox.domain,
|
||||
entity: response.value,
|
||||
me: me,
|
||||
statusCache: nil,
|
||||
userCache: nil,
|
||||
networkDate: response.networkDate
|
||||
)
|
||||
)
|
||||
if favoriteContext.isFavorited {
|
||||
status.update(favouritesCount: max(0, status.favouritesCount - 1)) // undo API return count has delay. Needs -1 local
|
||||
}
|
||||
case .failure:
|
||||
// rollback
|
||||
status.update(liked: favoriteContext.isFavorited, by: me)
|
||||
status.update(favouritesCount: favoriteContext.favoritedCount)
|
||||
}
|
||||
}
|
||||
|
||||
let response = try result.get()
|
||||
return response
|
||||
}
|
||||
|
@ -152,19 +115,11 @@ extension APIService {
|
|||
|
||||
extension APIService {
|
||||
public func favoritedBy(
|
||||
status: ManagedObjectRecord<Status>,
|
||||
status: MastodonStatus,
|
||||
query: Mastodon.API.Statuses.FavoriteByQuery,
|
||||
authenticationBox: MastodonAuthenticationBox
|
||||
) async throws -> Mastodon.Response.Content<[Mastodon.Entity.Account]> {
|
||||
let managedObjectContext = backgroundManagedObjectContext
|
||||
let _statusID: Status.ID? = try? await managedObjectContext.perform {
|
||||
guard let _status = status.object(in: managedObjectContext) else { return nil }
|
||||
let status = _status.reblog ?? _status
|
||||
return status.id
|
||||
}
|
||||
guard let statusID = _statusID else {
|
||||
throw APIError.implicit(.badRequest)
|
||||
}
|
||||
let statusID: String = status.reblog?.id ?? status.id
|
||||
|
||||
let response = try await Mastodon.API.Statuses.favoriteBy(
|
||||
session: session,
|
||||
|
@ -173,21 +128,7 @@ extension APIService {
|
|||
query: query,
|
||||
authorization: authenticationBox.userAuthorization
|
||||
).singleOutput()
|
||||
|
||||
try await managedObjectContext.performChanges {
|
||||
for entity in response.value {
|
||||
_ = Persistence.MastodonUser.createOrMerge(
|
||||
in: managedObjectContext,
|
||||
context: .init(
|
||||
domain: authenticationBox.domain,
|
||||
entity: entity,
|
||||
cache: nil,
|
||||
networkDate: response.networkDate
|
||||
)
|
||||
)
|
||||
} // end for … in
|
||||
}
|
||||
|
||||
|
||||
return response
|
||||
} // end func
|
||||
}
|
||||
|
|
|
@ -20,7 +20,7 @@ extension APIService {
|
|||
}
|
||||
|
||||
public func reblog(
|
||||
record: ManagedObjectRecord<Status>,
|
||||
status: MastodonStatus,
|
||||
authenticationBox: MastodonAuthenticationBox
|
||||
) async throws -> Mastodon.Response.Content<Mastodon.Entity.Status> {
|
||||
let managedObjectContext = backgroundManagedObjectContext
|
||||
|
@ -30,19 +30,16 @@ extension APIService {
|
|||
let authentication = authenticationBox.authentication
|
||||
|
||||
guard
|
||||
let me = authentication.user(in: managedObjectContext),
|
||||
let _status = record.object(in: managedObjectContext)
|
||||
let me = authentication.user(in: managedObjectContext)
|
||||
else { return nil }
|
||||
|
||||
let status = _status.reblog ?? _status
|
||||
let isReblogged = status.rebloggedBy.contains(me)
|
||||
let rebloggedCount = status.reblogsCount
|
||||
let reblogCount = isReblogged ? rebloggedCount - 1 : rebloggedCount + 1
|
||||
status.update(reblogged: !isReblogged, by: me)
|
||||
status.update(reblogsCount: Int64(max(0, reblogCount)))
|
||||
let _status = status.reblog ?? status
|
||||
let isReblogged = _status.entity.reblogged == true
|
||||
let rebloggedCount = Int64(_status.entity.reblogsCount)
|
||||
|
||||
let reblogContext = MastodonReblogContext(
|
||||
statusID: status.id,
|
||||
isReblogged: isReblogged,
|
||||
statusID: _status.id,
|
||||
isReblogged: !isReblogged,
|
||||
rebloggedCount: rebloggedCount
|
||||
)
|
||||
return reblogContext
|
||||
|
@ -65,41 +62,7 @@ extension APIService {
|
|||
} catch {
|
||||
result = .failure(error)
|
||||
}
|
||||
|
||||
// update repost state
|
||||
try await managedObjectContext.performChanges {
|
||||
let authentication = authenticationBox.authentication
|
||||
|
||||
guard
|
||||
let me = authentication.user(in: managedObjectContext),
|
||||
let _status = record.object(in: managedObjectContext)
|
||||
else { return }
|
||||
|
||||
let status = _status.reblog ?? _status
|
||||
|
||||
switch result {
|
||||
case .success(let response):
|
||||
_ = Persistence.Status.createOrMerge(
|
||||
in: managedObjectContext,
|
||||
context: Persistence.Status.PersistContext(
|
||||
domain: authentication.domain,
|
||||
entity: response.value,
|
||||
me: me,
|
||||
statusCache: nil,
|
||||
userCache: nil,
|
||||
networkDate: response.networkDate
|
||||
)
|
||||
)
|
||||
if reblogContext.isReblogged {
|
||||
status.update(reblogsCount: max(0, status.reblogsCount - 1)) // undo API return count has delay. Needs -1 local
|
||||
}
|
||||
case .failure:
|
||||
// rollback
|
||||
status.update(reblogged: reblogContext.isReblogged, by: me)
|
||||
status.update(reblogsCount: reblogContext.rebloggedCount)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
let response = try result.get()
|
||||
return response
|
||||
}
|
||||
|
@ -108,19 +71,12 @@ extension APIService {
|
|||
|
||||
extension APIService {
|
||||
public func rebloggedBy(
|
||||
status: ManagedObjectRecord<Status>,
|
||||
status: MastodonStatus,
|
||||
query: Mastodon.API.Statuses.RebloggedByQuery,
|
||||
authenticationBox: MastodonAuthenticationBox
|
||||
) async throws -> Mastodon.Response.Content<[Mastodon.Entity.Account]> {
|
||||
let managedObjectContext = backgroundManagedObjectContext
|
||||
let _statusID: Status.ID? = try? await managedObjectContext.perform {
|
||||
guard let _status = status.object(in: managedObjectContext) else { return nil }
|
||||
let status = _status.reblog ?? _status
|
||||
return status.id
|
||||
}
|
||||
guard let statusID = _statusID else {
|
||||
throw APIError.implicit(.badRequest)
|
||||
}
|
||||
let statusID: Status.ID = status.reblog?.id ?? status.id
|
||||
|
||||
let response = try await Mastodon.API.Statuses.rebloggedBy(
|
||||
session: session,
|
||||
|
@ -130,6 +86,7 @@ extension APIService {
|
|||
authorization: authenticationBox.userAuthorization
|
||||
).singleOutput()
|
||||
|
||||
#warning("Is this still required?")
|
||||
try await managedObjectContext.performChanges {
|
||||
for entity in response.value {
|
||||
_ = Persistence.MastodonUser.createOrMerge(
|
||||
|
|
|
@ -47,14 +47,14 @@ extension APIService {
|
|||
}
|
||||
|
||||
public func deleteStatus(
|
||||
status: ManagedObjectRecord<Status>,
|
||||
status: MastodonStatus,
|
||||
authenticationBox: MastodonAuthenticationBox
|
||||
) async throws -> Mastodon.Response.Content<Mastodon.Entity.Status> {
|
||||
let authorization = authenticationBox.userAuthorization
|
||||
|
||||
let managedObjectContext = backgroundManagedObjectContext
|
||||
let _query: Mastodon.API.Statuses.DeleteStatusQuery? = try? await managedObjectContext.perform {
|
||||
guard let _status = status.object(in: managedObjectContext) else { return nil }
|
||||
let _status = status.entity
|
||||
let status = _status.reblog ?? _status
|
||||
return Mastodon.API.Statuses.DeleteStatusQuery(id: status.id)
|
||||
}
|
||||
|
@ -68,12 +68,7 @@ extension APIService {
|
|||
query: query,
|
||||
authorization: authorization
|
||||
).singleOutput()
|
||||
|
||||
try await managedObjectContext.performChanges {
|
||||
guard let status = status.object(in: managedObjectContext) else { return }
|
||||
managedObjectContext.delete(status)
|
||||
}
|
||||
|
||||
|
||||
return response
|
||||
}
|
||||
|
||||
|
|
|
@ -104,6 +104,15 @@ extension Mastodon.Entity.Account: Equatable {
|
|||
|
||||
//MARK: - Convenience
|
||||
extension Mastodon.Entity.Account {
|
||||
public var acctWithDomain: String {
|
||||
if !acct.contains("@") {
|
||||
// Safe concat due to username cannot contains "@"
|
||||
return username + "@" + (domain ?? "")
|
||||
} else {
|
||||
return acct
|
||||
}
|
||||
}
|
||||
|
||||
public func acctWithDomainIfMissing(_ localDomain: String) -> String {
|
||||
guard acct.contains("@") else {
|
||||
return "\(acct)@\(localDomain)"
|
||||
|
|
|
@ -83,3 +83,13 @@ extension Mastodon.Entity.Card {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Mastodon.Entity.Card: Hashable {
|
||||
public static func == (lhs: Mastodon.Entity.Card, rhs: Mastodon.Entity.Card) -> Bool {
|
||||
lhs.url == rhs.url
|
||||
}
|
||||
|
||||
public func hash(into hasher: inout Hasher) {
|
||||
hasher.combine(url)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -88,3 +88,13 @@ extension Mastodon.Entity.Notification {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Mastodon.Entity.Notification: Hashable {
|
||||
public static func == (lhs: Mastodon.Entity.Notification, rhs: Mastodon.Entity.Notification) -> Bool {
|
||||
lhs.id == rhs.id
|
||||
}
|
||||
|
||||
public func hash(into hasher: inout Hasher) {
|
||||
hasher.combine(id)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -133,3 +133,14 @@ extension Mastodon.Entity.Status {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Mastodon.Entity.Status: Hashable {
|
||||
public static func == (lhs: Mastodon.Entity.Status, rhs: Mastodon.Entity.Status) -> Bool {
|
||||
lhs.uri == rhs.uri && lhs.id == rhs.id
|
||||
}
|
||||
|
||||
public func hash(into hasher: inout Hasher) {
|
||||
hasher.combine(uri)
|
||||
hasher.combine(id)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,56 @@
|
|||
// Copyright © 2023 Mastodon gGmbH. All rights reserved.
|
||||
|
||||
import Foundation
|
||||
import CoreDataStack
|
||||
|
||||
public final class MastodonFeed {
|
||||
public var hasMore: Bool = false
|
||||
public var isLoadingMore: Bool = false
|
||||
|
||||
public let status: MastodonStatus?
|
||||
public let notification: Mastodon.Entity.Notification?
|
||||
|
||||
public let kind: Feed.Kind
|
||||
|
||||
init(hasMore: Bool, isLoadingMore: Bool, status: MastodonStatus?, notification: Mastodon.Entity.Notification?, kind: Feed.Kind) {
|
||||
self.hasMore = hasMore
|
||||
self.isLoadingMore = isLoadingMore
|
||||
self.status = status
|
||||
self.notification = notification
|
||||
self.kind = kind
|
||||
}
|
||||
}
|
||||
|
||||
public extension MastodonFeed {
|
||||
static func fromStatus(_ status: MastodonStatus, kind: Feed.Kind) -> MastodonFeed {
|
||||
MastodonFeed(
|
||||
hasMore: false,
|
||||
isLoadingMore: false,
|
||||
status: status,
|
||||
notification: nil,
|
||||
kind: kind
|
||||
)
|
||||
}
|
||||
|
||||
static func fromNotification(_ notification: Mastodon.Entity.Notification, kind: Feed.Kind) -> MastodonFeed {
|
||||
MastodonFeed(
|
||||
hasMore: false,
|
||||
isLoadingMore: false,
|
||||
status: nil,
|
||||
notification: notification,
|
||||
kind: kind
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
extension MastodonFeed: Hashable {
|
||||
public static func == (lhs: MastodonFeed, rhs: MastodonFeed) -> Bool {
|
||||
lhs.status?.id == rhs.status?.id || lhs.notification?.id == rhs.notification?.id
|
||||
}
|
||||
|
||||
public func hash(into hasher: inout Hasher) {
|
||||
hasher.combine(status)
|
||||
hasher.combine(notification)
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,45 @@
|
|||
// Copyright © 2023 Mastodon gGmbH. All rights reserved.
|
||||
|
||||
import Foundation
|
||||
import CoreDataStack
|
||||
|
||||
public final class MastodonNotification {
|
||||
public let entity: Mastodon.Entity.Notification
|
||||
|
||||
public var id: Mastodon.Entity.Notification.ID {
|
||||
entity.id
|
||||
}
|
||||
|
||||
public let account: MastodonUser
|
||||
public let status: MastodonStatus?
|
||||
public let feeds: [MastodonFeed]
|
||||
|
||||
public var followRequestState: MastodonFollowRequestState = .init(state: .none)
|
||||
public var transientFollowRequestState: MastodonFollowRequestState = .init(state: .none)
|
||||
|
||||
public init(entity: Mastodon.Entity.Notification, account: MastodonUser, status: MastodonStatus?, feeds: [MastodonFeed]) {
|
||||
self.entity = entity
|
||||
self.account = account
|
||||
self.status = status
|
||||
self.feeds = feeds
|
||||
}
|
||||
}
|
||||
|
||||
public extension MastodonNotification {
|
||||
static func fromEntity(_ entity: Mastodon.Entity.Notification, using managedObjectContext: NSManagedObjectContext) -> MastodonNotification? {
|
||||
guard let user = MastodonUser.fetch(in: managedObjectContext, configurationBlock: { request in
|
||||
request.predicate = MastodonUser.predicate(domain: entity.account.domain ?? "", id: entity.account.id)
|
||||
}).first else { return nil }
|
||||
return MastodonNotification(entity: entity, account: user, status: entity.status.map(MastodonStatus.fromEntity), feeds: [])
|
||||
}
|
||||
}
|
||||
|
||||
extension MastodonNotification: Hashable {
|
||||
public static func == (lhs: MastodonNotification, rhs: MastodonNotification) -> Bool {
|
||||
lhs.id == rhs.id
|
||||
}
|
||||
|
||||
public func hash(into hasher: inout Hasher) {
|
||||
hasher.combine(id)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,54 @@
|
|||
// Copyright © 2023 Mastodon gGmbH. All rights reserved.
|
||||
|
||||
import Foundation
|
||||
import Combine
|
||||
import CoreDataStack
|
||||
|
||||
public final class MastodonStatus: ObservableObject {
|
||||
public typealias ID = Mastodon.Entity.Status.ID
|
||||
|
||||
@Published public var entity: Mastodon.Entity.Status
|
||||
@Published public private(set) var reblog: MastodonStatus?
|
||||
|
||||
@Published public var isSensitiveToggled: Bool = false
|
||||
|
||||
init(entity: Mastodon.Entity.Status, isSensitiveToggled: Bool) {
|
||||
self.entity = entity
|
||||
self.isSensitiveToggled = isSensitiveToggled
|
||||
|
||||
if let reblog = entity.reblog {
|
||||
self.reblog = MastodonStatus.fromEntity(reblog)
|
||||
} else {
|
||||
self.reblog = nil
|
||||
}
|
||||
}
|
||||
|
||||
public var id: ID {
|
||||
entity.id
|
||||
}
|
||||
}
|
||||
|
||||
extension MastodonStatus {
|
||||
public static func fromEntity(_ entity: Mastodon.Entity.Status) -> MastodonStatus {
|
||||
return MastodonStatus(entity: entity, isSensitiveToggled: false)
|
||||
}
|
||||
}
|
||||
|
||||
extension MastodonStatus: Hashable {
|
||||
public static func == (lhs: MastodonStatus, rhs: MastodonStatus) -> Bool {
|
||||
lhs.entity.id == rhs.entity.id
|
||||
}
|
||||
|
||||
public func hash(into hasher: inout Hasher) {
|
||||
hasher.combine(entity)
|
||||
hasher.combine(isSensitiveToggled)
|
||||
}
|
||||
}
|
||||
|
||||
public extension Mastodon.Entity.Status {
|
||||
var mastodonVisibility: MastodonVisibility? {
|
||||
guard let visibility = visibility?.rawValue else { return nil }
|
||||
return MastodonVisibility(rawValue: visibility)
|
||||
}
|
||||
}
|
||||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
import Foundation
|
||||
import CoreDataStack
|
||||
import MastodonSDK
|
||||
|
||||
public protocol StatusCompatible {
|
||||
var reblog: Status? { get }
|
||||
|
|
|
@ -60,10 +60,8 @@ extension ComposeContentViewModel {
|
|||
cell.statusView.frame.size.width = tableView.frame.width
|
||||
|
||||
// configure status
|
||||
context.managedObjectContext.performAndWait {
|
||||
guard let replyTo = status.object(in: context.managedObjectContext) else { return }
|
||||
cell.statusView.configure(status: replyTo)
|
||||
}
|
||||
cell.statusView.configure(status: status)
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -23,7 +23,7 @@ public final class ComposeContentViewModel: NSObject, ObservableObject {
|
|||
|
||||
public enum ComposeContext {
|
||||
case composeStatus
|
||||
case editStatus(status: Status, statusSource: Mastodon.Entity.StatusSource)
|
||||
case editStatus(status: MastodonStatus, statusSource: Mastodon.Entity.StatusSource)
|
||||
}
|
||||
|
||||
var disposeBag = Set<AnyCancellable>()
|
||||
|
@ -163,24 +163,18 @@ public final class ComposeContentViewModel: NSObject, ObservableObject {
|
|||
}()
|
||||
// set visibility for reply post
|
||||
if case .reply(let record) = destination {
|
||||
context.managedObjectContext.performAndWait {
|
||||
guard let status = record.object(in: context.managedObjectContext) else {
|
||||
assertionFailure()
|
||||
return
|
||||
}
|
||||
let repliedStatusVisibility = status.visibility
|
||||
switch repliedStatusVisibility {
|
||||
case .public, .unlisted:
|
||||
// keep default
|
||||
break
|
||||
case .private:
|
||||
visibility = .private
|
||||
case .direct:
|
||||
visibility = .direct
|
||||
case ._other:
|
||||
assertionFailure()
|
||||
break
|
||||
}
|
||||
let repliedStatusVisibility = record.entity.visibility
|
||||
switch repliedStatusVisibility {
|
||||
case .public, .unlisted:
|
||||
// keep default
|
||||
break
|
||||
case .private:
|
||||
visibility = .private
|
||||
case .direct:
|
||||
visibility = .direct
|
||||
case ._other, .none:
|
||||
assertionFailure()
|
||||
break
|
||||
}
|
||||
}
|
||||
return visibility
|
||||
|
@ -189,26 +183,7 @@ public final class ComposeContentViewModel: NSObject, ObservableObject {
|
|||
self.customEmojiViewModel = context.emojiService.dequeueCustomEmojiViewModel(
|
||||
for: authContext.mastodonAuthenticationBox.domain
|
||||
)
|
||||
|
||||
if case let ComposeContext.editStatus(status, _) = composeContext {
|
||||
if status.isContentSensitive {
|
||||
isContentWarningActive = true
|
||||
contentWarning = status.spoilerText ?? ""
|
||||
}
|
||||
if let poll = status.poll {
|
||||
isPollActive = !poll.expired
|
||||
pollMultipleConfigurationOption = poll.multiple
|
||||
if let pollExpiresAt = poll.expiresAt {
|
||||
pollExpireConfigurationOption = .init(closestDateToExpiry: pollExpiresAt)
|
||||
}
|
||||
pollOptions = poll.options.sortedByIndex().map {
|
||||
let option = PollComposeItem.Option()
|
||||
option.text = $0.title
|
||||
return option
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
let recentLanguages = context.settingService.currentSetting.value?.recentLanguages ?? []
|
||||
self.recentLanguages = recentLanguages
|
||||
self.language = recentLanguages.first ?? Locale.current.language.languageCode?.identifier ?? "en"
|
||||
|
@ -220,17 +195,14 @@ public final class ComposeContentViewModel: NSObject, ObservableObject {
|
|||
switch destination {
|
||||
case .reply(let record):
|
||||
context.managedObjectContext.performAndWait {
|
||||
guard let status = record.object(in: context.managedObjectContext) else {
|
||||
assertionFailure()
|
||||
return
|
||||
}
|
||||
let status = record.entity
|
||||
let author = authContext.mastodonAuthenticationBox.authentication.user(in: context.managedObjectContext)
|
||||
|
||||
var mentionAccts: [String] = []
|
||||
if author?.id != status.author.id {
|
||||
mentionAccts.append("@" + status.author.acct)
|
||||
if author?.id != status.account.id {
|
||||
mentionAccts.append("@" + status.account.acct)
|
||||
}
|
||||
let mentions = status.mentions
|
||||
let mentions = status.mentions ?? []
|
||||
.filter { author?.id != $0.id }
|
||||
for mention in mentions {
|
||||
let acct = "@" + mention.acct
|
||||
|
@ -288,11 +260,11 @@ public final class ComposeContentViewModel: NSObject, ObservableObject {
|
|||
case .composeStatus:
|
||||
self.isVisibilityButtonEnabled = true
|
||||
case let .editStatus(status, _):
|
||||
if let visibility = Mastodon.Entity.Status.Visibility(rawValue: status.visibility.rawValue) {
|
||||
if let visibility = status.entity.visibility {
|
||||
self.visibility = visibility
|
||||
}
|
||||
self.isVisibilityButtonEnabled = false
|
||||
self.attachmentViewModels = status.attachments.compactMap {
|
||||
self.attachmentViewModels = status.entity.mastodonAttachments.compactMap {
|
||||
guard let assetURL = $0.assetURL, let url = URL(string: assetURL) else { return nil }
|
||||
let attachmentViewModel = AttachmentViewModel(
|
||||
api: context.apiService,
|
||||
|
@ -306,6 +278,27 @@ public final class ComposeContentViewModel: NSObject, ObservableObject {
|
|||
}
|
||||
}
|
||||
|
||||
if case let ComposeContext.editStatus(status, _) = composeContext {
|
||||
if status.entity.sensitive == true {
|
||||
isContentWarningActive = true
|
||||
contentWarning = status.entity.spoilerText ?? ""
|
||||
}
|
||||
Task {
|
||||
if let poll = await status.getPoll(in: context.managedObjectContext) {
|
||||
isPollActive = !poll.expired
|
||||
pollMultipleConfigurationOption = poll.multiple
|
||||
if let pollExpiresAt = poll.expiresAt {
|
||||
pollExpireConfigurationOption = .init(closestDateToExpiry: pollExpiresAt)
|
||||
}
|
||||
pollOptions = poll.options.sortedByIndex().map {
|
||||
let option = PollComposeItem.Option()
|
||||
option.text = $0.title
|
||||
return option
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bind()
|
||||
}
|
||||
|
||||
|
@ -503,7 +496,7 @@ extension ComposeContentViewModel {
|
|||
extension ComposeContentViewModel {
|
||||
public enum Destination {
|
||||
case topLevel
|
||||
case reply(parent: ManagedObjectRecord<Status>)
|
||||
case reply(parent: MastodonStatus)
|
||||
}
|
||||
|
||||
public enum ScrollViewState {
|
||||
|
|
|
@ -19,7 +19,7 @@ public final class MastodonStatusPublisher: NSObject, ProgressReporting {
|
|||
// author
|
||||
public let author: ManagedObjectRecord<MastodonUser>
|
||||
// refer
|
||||
public let replyTo: ManagedObjectRecord<Status>?
|
||||
public let replyTo: MastodonStatus?
|
||||
// content warning
|
||||
public let isContentWarningComposing: Bool
|
||||
public let contentWarning: String
|
||||
|
@ -48,7 +48,7 @@ public final class MastodonStatusPublisher: NSObject, ProgressReporting {
|
|||
|
||||
public init(
|
||||
author: ManagedObjectRecord<MastodonUser>,
|
||||
replyTo: ManagedObjectRecord<Status>?,
|
||||
replyTo: MastodonStatus?,
|
||||
isContentWarningComposing: Bool,
|
||||
contentWarning: String,
|
||||
content: String,
|
||||
|
@ -162,7 +162,7 @@ extension MastodonStatusPublisher: StatusPublisher {
|
|||
return self.pollExpireConfigurationOption.seconds
|
||||
}()
|
||||
let inReplyToID: Mastodon.Entity.Status.ID? = try await api.backgroundManagedObjectContext.perform {
|
||||
guard let replyTo = self.replyTo?.object(in: api.backgroundManagedObjectContext) else { return nil }
|
||||
guard let replyTo = self.replyTo else { return nil }
|
||||
return replyTo.id
|
||||
}
|
||||
|
||||
|
|
|
@ -180,7 +180,7 @@ extension MediaView.Configuration {
|
|||
}
|
||||
|
||||
extension MediaView {
|
||||
public static func configuration(status: StatusCompatible) -> [MediaView.Configuration] {
|
||||
public static func configuration(status: MastodonStatus) -> [MediaView.Configuration] {
|
||||
func videoInfo(from attachment: MastodonAttachment) -> MediaView.Configuration.VideoInfo {
|
||||
MediaView.Configuration.VideoInfo(
|
||||
aspectRadio: attachment.size,
|
||||
|
@ -191,8 +191,8 @@ extension MediaView {
|
|||
)
|
||||
}
|
||||
|
||||
let status: StatusCompatible = status.reblog ?? status
|
||||
let attachments = status.attachments
|
||||
// let status: StatusCompatible = status.reblog ?? status
|
||||
let attachments = status.entity.mastodonAttachments
|
||||
let configurations = attachments.enumerated().map { (idx, attachment) -> MediaView.Configuration in
|
||||
let configuration: MediaView.Configuration = {
|
||||
switch attachment.kind {
|
||||
|
@ -236,7 +236,7 @@ extension MediaView {
|
|||
}()
|
||||
|
||||
configuration.load()
|
||||
configuration.isReveal = status.isMediaSensitive ? status.isSensitiveToggled : true
|
||||
configuration.isReveal = status.entity.sensitive == true ? status.isSensitiveToggled : true
|
||||
|
||||
return configuration
|
||||
}
|
||||
|
|
|
@ -19,7 +19,7 @@ import CoreDataStack
|
|||
extension NotificationView {
|
||||
public final class ViewModel: ObservableObject {
|
||||
public var disposeBag = Set<AnyCancellable>()
|
||||
public var objects = Set<NSManagedObject>()
|
||||
public var objects = Set<AnyHashable>()
|
||||
|
||||
@Published public var context: AppContext?
|
||||
@Published public var authContext: AuthContext?
|
||||
|
|
|
@ -13,6 +13,7 @@ import MastodonLocalization
|
|||
import CoreDataStack
|
||||
import UIKit
|
||||
import WebKit
|
||||
import MastodonSDK
|
||||
|
||||
public protocol StatusCardControlDelegate: AnyObject {
|
||||
func statusCardControl(_ statusCardControl: StatusCardControl, didTapURL url: URL)
|
||||
|
@ -133,20 +134,24 @@ public final class StatusCardControl: UIControl {
|
|||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
public func configure(card: Card) {
|
||||
public func configure(card: Mastodon.Entity.Card) {
|
||||
let title = card.title.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
if let host = card.url?.host {
|
||||
let url = URL(string: card.url)
|
||||
if let host = url?.host {
|
||||
accessibilityLabel = "\(title) \(host)"
|
||||
} else {
|
||||
accessibilityLabel = title
|
||||
}
|
||||
|
||||
titleLabel.text = title
|
||||
linkLabel.text = card.url?.host
|
||||
linkLabel.text = url?.host
|
||||
imageView.contentMode = .center
|
||||
|
||||
imageView.sd_setImage(
|
||||
with: card.imageURL,
|
||||
with: {
|
||||
guard let image = card.image else { return nil }
|
||||
return URL(string: image)
|
||||
}(),
|
||||
placeholderImage: icon(for: card.layout)
|
||||
) { [weak self] image, _, _, _ in
|
||||
if image != nil {
|
||||
|
@ -333,6 +338,18 @@ private extension Card {
|
|||
}
|
||||
}
|
||||
|
||||
private extension Mastodon.Entity.Card {
|
||||
var layout: StatusCardControl.Layout {
|
||||
var aspectRatio = CGFloat(width ?? 1) / CGFloat(height ?? 1)
|
||||
if !aspectRatio.isFinite {
|
||||
aspectRatio = 1
|
||||
}
|
||||
return (abs(aspectRatio - 1) < 0.05 || image == nil) && html == nil
|
||||
? .compact
|
||||
: .large(aspectRatio: aspectRatio)
|
||||
}
|
||||
}
|
||||
|
||||
private extension UILayoutPriority {
|
||||
static let zero = UILayoutPriority(rawValue: 0)
|
||||
}
|
||||
|
|
|
@ -19,7 +19,7 @@ extension StatusView {
|
|||
|
||||
static let statusFilterWorkingQueue = DispatchQueue(label: "StatusFilterWorkingQueue")
|
||||
|
||||
public func configure(feed: Feed) {
|
||||
public func configure(feed: MastodonFeed) {
|
||||
switch feed.kind {
|
||||
case .home:
|
||||
guard let status = feed.status else {
|
||||
|
@ -40,18 +40,12 @@ extension StatusView {
|
|||
|
||||
extension StatusView {
|
||||
|
||||
public func configure(status: Status, statusEdit: Mastodon.Entity.StatusEdit) {
|
||||
viewModel.objects.insert(status)
|
||||
if let reblog = status.reblog {
|
||||
viewModel.objects.insert(reblog)
|
||||
}
|
||||
|
||||
public func configure(status: MastodonStatus, statusEdit: Mastodon.Entity.StatusEdit) {
|
||||
configureHeader(status: status)
|
||||
let author = (status.reblog ?? status).author
|
||||
let author = (status.reblog ?? status).entity.account
|
||||
configureAuthor(author: author)
|
||||
let timestamp = (status.reblog ?? status).publisher(for: \.createdAt)
|
||||
configureTimestamp(timestamp: timestamp.eraseToAnyPublisher())
|
||||
configureApplicationName(status.application?.name)
|
||||
configureTimestamp(timestamp: (status.reblog ?? status).entity.createdAt)
|
||||
configureApplicationName(status.entity.application?.name)
|
||||
configureMedia(status: status)
|
||||
configurePollHistory(statusEdit: statusEdit)
|
||||
configureCard(status: status)
|
||||
|
@ -66,18 +60,13 @@ extension StatusView {
|
|||
viewModel.isContentReveal = true
|
||||
}
|
||||
|
||||
public func configure(status: Status) {
|
||||
viewModel.objects.insert(status)
|
||||
if let reblog = status.reblog {
|
||||
viewModel.objects.insert(reblog)
|
||||
}
|
||||
|
||||
public func configure(status: MastodonStatus) {
|
||||
configureHeader(status: status)
|
||||
let author = (status.reblog ?? status).author
|
||||
let author = (status.reblog ?? status).entity.account
|
||||
configureAuthor(author: author)
|
||||
let timestamp = (status.reblog ?? status).publisher(for: \.createdAt)
|
||||
configureTimestamp(timestamp: timestamp.eraseToAnyPublisher())
|
||||
configureApplicationName(status.application?.name)
|
||||
let timestamp = (status.reblog ?? status).entity.createdAt
|
||||
configureTimestamp(timestamp: timestamp)
|
||||
configureApplicationName(status.entity.application?.name)
|
||||
configureContent(status: status)
|
||||
configureMedia(status: status)
|
||||
configurePoll(status: status)
|
||||
|
@ -96,14 +85,13 @@ extension StatusView {
|
|||
}
|
||||
|
||||
extension StatusView {
|
||||
private func configureHeader(status: Status) {
|
||||
private func configureHeader(status: MastodonStatus) {
|
||||
if let _ = status.reblog {
|
||||
Publishers.CombineLatest(
|
||||
status.author.publisher(for: \.displayName),
|
||||
status.author.publisher(for: \.emojis)
|
||||
)
|
||||
.map { name, emojis -> StatusView.ViewModel.Header in
|
||||
let text = L10n.Common.Controls.Status.userReblogged(status.author.displayNameWithFallback)
|
||||
let name = status.entity.account.displayName
|
||||
let emojis = status.entity.account.emojis ?? []
|
||||
|
||||
viewModel.header = {
|
||||
let text = L10n.Common.Controls.Status.userReblogged(status.entity.account.displayNameWithFallback)
|
||||
let content = MastodonContent(content: text, emojis: emojis.asDictionary)
|
||||
do {
|
||||
let metaContent = try MastodonMetaContent.convert(document: content)
|
||||
|
@ -112,12 +100,10 @@ extension StatusView {
|
|||
let metaContent = PlaintextMetaContent(string: name)
|
||||
return .repost(info: .init(header: metaContent))
|
||||
}
|
||||
|
||||
}
|
||||
.assign(to: \.header, on: viewModel)
|
||||
.store(in: &disposeBag)
|
||||
} else if let _ = status.inReplyToID,
|
||||
let inReplyToAccountID = status.inReplyToAccountID
|
||||
}()
|
||||
|
||||
} else if let _ = status.entity.inReplyToID,
|
||||
let inReplyToAccountID = status.entity.inReplyToAccountID
|
||||
{
|
||||
func createHeader(
|
||||
name: String?,
|
||||
|
@ -139,20 +125,31 @@ extension StatusView {
|
|||
return header
|
||||
}
|
||||
|
||||
if let replyTo = status.replyTo {
|
||||
if let inReplyToID = status.entity.inReplyToID {
|
||||
// A. replyTo status exist
|
||||
let header = createHeader(name: replyTo.author.displayNameWithFallback, emojis: replyTo.author.emojis.asDictionary)
|
||||
viewModel.header = header
|
||||
if let authenticationBox = viewModel.authContext?.mastodonAuthenticationBox {
|
||||
Task {
|
||||
if let replyTo = try? await Mastodon.API.Statuses.status(
|
||||
session: .shared,
|
||||
domain: authenticationBox.domain,
|
||||
statusID: inReplyToID,
|
||||
authorization: authenticationBox.userAuthorization
|
||||
).singleOutput().value {
|
||||
let header = createHeader(name: replyTo.account.displayNameWithFallback, emojis: replyTo.account.emojis?.asDictionary ?? [:])
|
||||
viewModel.header = header
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// B. replyTo status not exist
|
||||
|
||||
let request = MastodonUser.sortedFetchRequest
|
||||
request.predicate = MastodonUser.predicate(domain: status.domain, id: inReplyToAccountID)
|
||||
if let user = status.managedObjectContext?.safeFetch(request).first {
|
||||
// B1. replyTo user exist
|
||||
let header = createHeader(name: user.displayNameWithFallback, emojis: user.emojis.asDictionary)
|
||||
viewModel.header = header
|
||||
} else {
|
||||
// let request = MastodonUser.sortedFetchRequest
|
||||
// request.predicate = MastodonUser.predicate(domain: status.domain, id: inReplyToAccountID)
|
||||
// if let user = status.managedObjectContext?.safeFetch(request).first {
|
||||
// // B1. replyTo user exist
|
||||
// let header = createHeader(name: user.displayNameWithFallback, emojis: user.emojis.asDictionary)
|
||||
// viewModel.header = header
|
||||
// } else {
|
||||
// B2. replyTo user not exist
|
||||
let header = createHeader(name: nil, emojis: nil)
|
||||
viewModel.header = header
|
||||
|
@ -178,7 +175,7 @@ extension StatusView {
|
|||
}
|
||||
.store(in: &disposeBag)
|
||||
} // end if let
|
||||
} // end else B2.
|
||||
// } // end else B2.
|
||||
} // end else B.
|
||||
|
||||
} else {
|
||||
|
@ -186,90 +183,56 @@ extension StatusView {
|
|||
}
|
||||
}
|
||||
|
||||
public func configureAuthor(author: MastodonUser) {
|
||||
public func configureAuthor(author: Mastodon.Entity.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()
|
||||
let emojis = author.emojis?.asDictionary ?? [:]
|
||||
|
||||
// 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: emojis)
|
||||
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)
|
||||
// locked
|
||||
author.publisher(for: \.locked)
|
||||
.assign(to: \.locked, on: viewModel)
|
||||
.store(in: &disposeBag)
|
||||
// isMuting
|
||||
author.publisher(for: \.mutingBy)
|
||||
.map { [weak viewModel] mutingBy in
|
||||
guard let viewModel = viewModel else { return false }
|
||||
guard let authContext = viewModel.authContext else { return false }
|
||||
return mutingBy.contains(where: {
|
||||
$0.id == authContext.mastodonAuthenticationBox.userID && $0.domain == authContext.mastodonAuthenticationBox.domain
|
||||
})
|
||||
}
|
||||
.assign(to: \.isMuting, on: viewModel)
|
||||
.store(in: &disposeBag)
|
||||
// isBlocking
|
||||
author.publisher(for: \.blockingBy)
|
||||
.map { [weak viewModel] blockingBy in
|
||||
guard let viewModel = viewModel else { return false }
|
||||
guard let authContext = viewModel.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)
|
||||
// isMyself
|
||||
Publishers.CombineLatest(
|
||||
author.publisher(for: \.domain),
|
||||
author.publisher(for: \.id)
|
||||
)
|
||||
.map { [weak viewModel] domain, id in
|
||||
guard let viewModel = viewModel else { return false }
|
||||
guard let authContext = viewModel.authContext else { return false }
|
||||
return authContext.mastodonAuthenticationBox.domain == domain && authContext.mastodonAuthenticationBox.userID == id
|
||||
}
|
||||
.assign(to: \.isMyself, on: viewModel)
|
||||
.store(in: &disposeBag)
|
||||
viewModel.authorUsername = author.acct
|
||||
|
||||
// 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
|
||||
})
|
||||
// locked
|
||||
viewModel.locked = author.locked
|
||||
|
||||
// isMuting, isBlocking, Following
|
||||
Task {
|
||||
guard let auth = viewModel.authContext?.mastodonAuthenticationBox else { return }
|
||||
if let relationship = try? await Mastodon.API.Account.relationships(
|
||||
session: .shared,
|
||||
domain: auth.domain,
|
||||
query: .init(ids: [author.id]),
|
||||
authorization: auth.userAuthorization
|
||||
).singleOutput().value {
|
||||
guard let rel = relationship.first else { return }
|
||||
DispatchQueue.main.async { [self] in
|
||||
viewModel.isMuting = rel.muting ?? false
|
||||
viewModel.isBlocking = rel.blocking
|
||||
viewModel.isFollowed = rel.followedBy
|
||||
}
|
||||
}
|
||||
.assign(to: \.isFollowed, on: viewModel)
|
||||
.store(in: &disposeBag)
|
||||
}
|
||||
|
||||
// isMyself
|
||||
viewModel.isMyself = {
|
||||
guard let authContext = viewModel.authContext else { return false }
|
||||
return authContext.mastodonAuthenticationBox.domain == author.domain && authContext.mastodonAuthenticationBox.userID == author.id
|
||||
}()
|
||||
|
||||
}
|
||||
|
||||
private func configureTimestamp(timestamp: AnyPublisher<Date, Never>) {
|
||||
private func configureTimestamp(timestamp: Date) {
|
||||
// timestamp
|
||||
viewModel.timestampFormatter = { (date: Date, isEdited: Bool) in
|
||||
if isEdited {
|
||||
|
@ -277,10 +240,7 @@ extension StatusView {
|
|||
}
|
||||
return date.localizedSlowedTimeAgoSinceNow
|
||||
}
|
||||
timestamp
|
||||
.map { $0 as Date? }
|
||||
.assign(to: \.timestamp, on: viewModel)
|
||||
.store(in: &disposeBag)
|
||||
viewModel.timestamp = timestamp
|
||||
}
|
||||
|
||||
private func configureApplicationName(_ applicationName: String?) {
|
||||
|
@ -294,7 +254,7 @@ extension StatusView {
|
|||
configure(status: originalStatus)
|
||||
}
|
||||
|
||||
func configureTranslated(status: Status) {
|
||||
func configureTranslated(status: MastodonStatus) {
|
||||
guard let translation = viewModel.translation,
|
||||
let translatedContent = translation.content else {
|
||||
viewModel.isCurrentlyTranslating = false
|
||||
|
@ -303,7 +263,7 @@ extension StatusView {
|
|||
|
||||
// content
|
||||
do {
|
||||
let content = MastodonContent(content: translatedContent, emojis: status.emojis.asDictionary)
|
||||
let content = MastodonContent(content: translatedContent, emojis: status.entity.emojis?.asDictionary ?? [:])
|
||||
let metaContent = try MastodonMetaContent.convert(document: content)
|
||||
viewModel.content = metaContent
|
||||
viewModel.isCurrentlyTranslating = false
|
||||
|
@ -313,13 +273,13 @@ extension StatusView {
|
|||
}
|
||||
}
|
||||
|
||||
private func configureContent(statusEdit: Mastodon.Entity.StatusEdit, status: Status) {
|
||||
private func configureContent(statusEdit: Mastodon.Entity.StatusEdit, status: MastodonStatus) {
|
||||
statusEdit.spoilerText.map {
|
||||
viewModel.spoilerContent = PlaintextMetaContent(string: $0)
|
||||
}
|
||||
|
||||
// language
|
||||
viewModel.language = (status.reblog ?? status).language
|
||||
viewModel.language = (status.reblog ?? status).entity.language
|
||||
// content
|
||||
do {
|
||||
let content = MastodonContent(content: statusEdit.content, emojis: statusEdit.emojis?.asDictionary ?? [:])
|
||||
|
@ -332,7 +292,7 @@ extension StatusView {
|
|||
}
|
||||
}
|
||||
|
||||
private func configureContent(status: Status) {
|
||||
private func configureContent(status: MastodonStatus) {
|
||||
guard viewModel.translation == nil else {
|
||||
return configureTranslated(status: status)
|
||||
}
|
||||
|
@ -340,9 +300,9 @@ extension StatusView {
|
|||
let status = status.reblog ?? status
|
||||
|
||||
// spoilerText
|
||||
if let spoilerText = status.spoilerText, !spoilerText.isEmpty {
|
||||
if let spoilerText = status.entity.spoilerText, !spoilerText.isEmpty {
|
||||
do {
|
||||
let content = MastodonContent(content: spoilerText, emojis: status.emojis.asDictionary)
|
||||
let content = MastodonContent(content: spoilerText, emojis: status.entity.emojis?.asDictionary ?? [:])
|
||||
let metaContent = try MastodonMetaContent.convert(document: content)
|
||||
viewModel.spoilerContent = metaContent
|
||||
} catch {
|
||||
|
@ -353,10 +313,10 @@ extension StatusView {
|
|||
viewModel.spoilerContent = nil
|
||||
}
|
||||
// language
|
||||
viewModel.language = (status.reblog ?? status).language
|
||||
viewModel.language = (status.reblog ?? status).entity.language
|
||||
// content
|
||||
do {
|
||||
let content = MastodonContent(content: status.content, emojis: status.emojis.asDictionary)
|
||||
let content = MastodonContent(content: status.entity.content ?? "", emojis: status.entity.emojis?.asDictionary ?? [:])
|
||||
let metaContent = try MastodonMetaContent.convert(document: content)
|
||||
viewModel.content = metaContent
|
||||
viewModel.isCurrentlyTranslating = false
|
||||
|
@ -365,21 +325,18 @@ extension StatusView {
|
|||
viewModel.content = PlaintextMetaContent(string: "")
|
||||
}
|
||||
// visibility
|
||||
status.publisher(for: \.visibilityRaw)
|
||||
.compactMap { MastodonVisibility(rawValue: $0) }
|
||||
.assign(to: \.visibility, on: viewModel)
|
||||
.store(in: &disposeBag)
|
||||
viewModel.visibility = status.entity.mastodonVisibility
|
||||
|
||||
// sensitive
|
||||
viewModel.isContentSensitive = status.isContentSensitive
|
||||
status.publisher(for: \.isSensitiveToggled)
|
||||
.assign(to: \.isSensitiveToggled, on: viewModel)
|
||||
.store(in: &disposeBag)
|
||||
viewModel.isContentSensitive = status.entity.sensitive == true
|
||||
viewModel.isSensitiveToggled = status.isSensitiveToggled
|
||||
|
||||
}
|
||||
|
||||
private func configureMedia(status: StatusCompatible) {
|
||||
private func configureMedia(status: MastodonStatus) {
|
||||
let status = status.reblog ?? status
|
||||
|
||||
viewModel.isMediaSensitive = status.isMediaSensitive
|
||||
viewModel.isMediaSensitive = status.entity.sensitive == true
|
||||
|
||||
let configurations = MediaView.configuration(status: status)
|
||||
viewModel.mediaViewConfigurations = configurations
|
||||
|
@ -405,39 +362,32 @@ extension StatusView {
|
|||
pollTableViewDiffableDataSource?.applySnapshotUsingReloadData(_snapshot)
|
||||
}
|
||||
|
||||
private func configurePoll(status: Status) {
|
||||
let status = status.reblog ?? status
|
||||
|
||||
if let poll = status.poll {
|
||||
viewModel.objects.insert(poll)
|
||||
}
|
||||
private func configurePoll(status: MastodonStatus) {
|
||||
Task {
|
||||
guard
|
||||
let context = viewModel.context?.managedObjectContext,
|
||||
let poll = await status.getPoll(in: context)
|
||||
else { return }
|
||||
|
||||
let status = status.reblog ?? status
|
||||
|
||||
viewModel.managedObjects.insert(poll)
|
||||
|
||||
// pollItems
|
||||
status.publisher(for: \.poll)
|
||||
.sink { [weak self] poll in
|
||||
guard let self = self else { return }
|
||||
guard let poll = poll else {
|
||||
self.viewModel.pollItems = []
|
||||
return
|
||||
// pollItems
|
||||
let options = poll.options.sorted(by: { $0.index < $1.index })
|
||||
let items: [PollItem] = options.map { .option(record: .init(objectID: $0.objectID)) }
|
||||
self.viewModel.pollItems = items
|
||||
|
||||
// isVoteButtonEnabled
|
||||
poll.publisher(for: \.updatedAt)
|
||||
.sink { [weak self] _ in
|
||||
guard let self = self else { return }
|
||||
let options = poll.options
|
||||
let hasSelectedOption = options.contains(where: { $0.isSelected })
|
||||
self.viewModel.isVoteButtonEnabled = hasSelectedOption
|
||||
}
|
||||
|
||||
let options = poll.options.sorted(by: { $0.index < $1.index })
|
||||
let items: [PollItem] = options.map { .option(record: .init(objectID: $0.objectID)) }
|
||||
self.viewModel.pollItems = items
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
// isVoteButtonEnabled
|
||||
status.poll?.publisher(for: \.updatedAt)
|
||||
.sink { [weak self] _ in
|
||||
guard let self = self else { return }
|
||||
guard let poll = status.poll else { return }
|
||||
let options = poll.options
|
||||
let hasSelectedOption = options.contains(where: { $0.isSelected })
|
||||
self.viewModel.isVoteButtonEnabled = hasSelectedOption
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
// isVotable
|
||||
if let poll = status.poll {
|
||||
.store(in: &disposeBag)
|
||||
// isVotable
|
||||
Publishers.CombineLatest(
|
||||
poll.publisher(for: \.votedBy),
|
||||
poll.publisher(for: \.expired)
|
||||
|
@ -451,100 +401,62 @@ extension StatusView {
|
|||
return !isVoted && !expired
|
||||
}
|
||||
.assign(to: &viewModel.$isVotable)
|
||||
|
||||
// votesCount
|
||||
poll.publisher(for: \.votesCount)
|
||||
.map { Int($0) }
|
||||
.assign(to: \.voteCount, on: viewModel)
|
||||
.store(in: &disposeBag)
|
||||
// voterCount
|
||||
poll.publisher(for: \.votersCount)
|
||||
.map { Int($0) }
|
||||
.assign(to: \.voterCount, on: viewModel)
|
||||
.store(in: &disposeBag)
|
||||
// expireAt
|
||||
poll.publisher(for: \.expiresAt)
|
||||
.assign(to: \.expireAt, on: viewModel)
|
||||
.store(in: &disposeBag)
|
||||
// expired
|
||||
poll.publisher(for: \.expired)
|
||||
.assign(to: \.expired, on: viewModel)
|
||||
.store(in: &disposeBag)
|
||||
// isVoting
|
||||
poll.publisher(for: \.isVoting)
|
||||
.assign(to: \.isVoting, on: viewModel)
|
||||
.store(in: &disposeBag)
|
||||
}
|
||||
// votesCount
|
||||
status.poll?.publisher(for: \.votesCount)
|
||||
.map { Int($0) }
|
||||
.assign(to: \.voteCount, on: viewModel)
|
||||
.store(in: &disposeBag)
|
||||
// voterCount
|
||||
status.poll?.publisher(for: \.votersCount)
|
||||
.map { Int($0) }
|
||||
.assign(to: \.voterCount, on: viewModel)
|
||||
.store(in: &disposeBag)
|
||||
// expireAt
|
||||
status.poll?.publisher(for: \.expiresAt)
|
||||
.assign(to: \.expireAt, on: viewModel)
|
||||
.store(in: &disposeBag)
|
||||
// expired
|
||||
status.poll?.publisher(for: \.expired)
|
||||
.assign(to: \.expired, on: viewModel)
|
||||
.store(in: &disposeBag)
|
||||
// isVoting
|
||||
status.poll?.publisher(for: \.isVoting)
|
||||
.assign(to: \.isVoting, on: viewModel)
|
||||
.store(in: &disposeBag)
|
||||
}
|
||||
|
||||
private func configureCard(status: Status) {
|
||||
private func configureCard(status: MastodonStatus) {
|
||||
let status = status.reblog ?? status
|
||||
if viewModel.mediaViewConfigurations.isEmpty {
|
||||
status.publisher(for: \.card)
|
||||
.assign(to: \.card, on: viewModel)
|
||||
.store(in: &disposeBag)
|
||||
viewModel.card = status.entity.card
|
||||
} else {
|
||||
viewModel.card = nil
|
||||
}
|
||||
}
|
||||
|
||||
private func configureToolbar(status: Status) {
|
||||
private func configureToolbar(status: MastodonStatus) {
|
||||
let status = status.reblog ?? status
|
||||
|
||||
status.publisher(for: \.repliesCount)
|
||||
.map(Int.init)
|
||||
.assign(to: \.replyCount, on: viewModel)
|
||||
.store(in: &disposeBag)
|
||||
status.publisher(for: \.reblogsCount)
|
||||
.map(Int.init)
|
||||
.assign(to: \.reblogCount, on: viewModel)
|
||||
.store(in: &disposeBag)
|
||||
status.publisher(for: \.favouritesCount)
|
||||
.map(Int.init)
|
||||
.assign(to: \.favoriteCount, on: viewModel)
|
||||
.store(in: &disposeBag)
|
||||
status.publisher(for: \.editedAt)
|
||||
.assign(to: \.editedAt, on: viewModel)
|
||||
.store(in: &disposeBag)
|
||||
viewModel.replyCount = status.entity.repliesCount ?? 0
|
||||
|
||||
viewModel.reblogCount = status.entity.reblogsCount
|
||||
|
||||
viewModel.favoriteCount = status.entity.favouritesCount
|
||||
|
||||
viewModel.editedAt = status.entity.editedAt
|
||||
|
||||
// relationship
|
||||
status.publisher(for: \.rebloggedBy)
|
||||
.map { [weak viewModel] rebloggedBy in
|
||||
guard let viewModel = viewModel else { return false }
|
||||
guard let authContext = viewModel.authContext else { return false }
|
||||
return rebloggedBy.contains(where: {
|
||||
$0.id == authContext.mastodonAuthenticationBox.userID && $0.domain == authContext.mastodonAuthenticationBox.domain
|
||||
})
|
||||
}
|
||||
.assign(to: \.isReblog, on: viewModel)
|
||||
.store(in: &disposeBag)
|
||||
|
||||
status.publisher(for: \.favouritedBy)
|
||||
.map { [weak viewModel]favouritedBy in
|
||||
guard let viewModel = viewModel else { return false }
|
||||
guard let authContext = viewModel.authContext else { return false }
|
||||
return favouritedBy.contains(where: {
|
||||
$0.id == authContext.mastodonAuthenticationBox.userID && $0.domain == authContext.mastodonAuthenticationBox.domain
|
||||
})
|
||||
}
|
||||
.assign(to: \.isFavorite, on: viewModel)
|
||||
.store(in: &disposeBag)
|
||||
|
||||
status.publisher(for: \.bookmarkedBy)
|
||||
.map { [weak viewModel] bookmarkedBy in
|
||||
guard let viewModel = viewModel else { return false }
|
||||
guard let authContext = viewModel.authContext else { return false }
|
||||
return bookmarkedBy.contains(where: {
|
||||
$0.id == authContext.mastodonAuthenticationBox.userID && $0.domain == authContext.mastodonAuthenticationBox.domain
|
||||
})
|
||||
}
|
||||
.assign(to: \.isBookmark, on: viewModel)
|
||||
.store(in: &disposeBag)
|
||||
viewModel.isReblog = status.entity.reblogged == true
|
||||
viewModel.isFavorite = status.entity.favourited == true
|
||||
viewModel.isBookmark = status.entity.bookmarked == true
|
||||
}
|
||||
|
||||
private func configureFilter(status: Status) {
|
||||
private func configureFilter(status: MastodonStatus) {
|
||||
let status = status.reblog ?? status
|
||||
|
||||
let content = status.content.lowercased()
|
||||
guard let content = status.entity.content?.lowercased() else { return }
|
||||
|
||||
Publishers.CombineLatest(
|
||||
viewModel.$activeFilters,
|
||||
|
@ -595,3 +507,16 @@ extension StatusView {
|
|||
}
|
||||
|
||||
}
|
||||
|
||||
extension MastodonStatus {
|
||||
func getPoll(in context: NSManagedObjectContext) async -> Poll? {
|
||||
guard
|
||||
let domain = entity.account.domain,
|
||||
let pollId = entity.poll?.id
|
||||
else { return nil }
|
||||
return try? await context.perform {
|
||||
let predicate = Poll.predicate(domain: domain, id: pollId)
|
||||
return Poll.findOrFetch(in: context, matching: predicate)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -22,11 +22,12 @@ extension StatusView {
|
|||
public final class ViewModel: ObservableObject {
|
||||
var disposeBag = Set<AnyCancellable>()
|
||||
var observations = Set<NSKeyValueObservation>()
|
||||
public var objects = Set<NSManagedObject>()
|
||||
public var objects = Set<MastodonStatus>()
|
||||
public var managedObjects = Set<NSManagedObject>()
|
||||
|
||||
public var context: AppContext?
|
||||
public var authContext: AuthContext?
|
||||
public var originalStatus: Status?
|
||||
public var originalStatus: MastodonStatus?
|
||||
|
||||
// Header
|
||||
@Published public var header: Header = .none
|
||||
|
@ -77,7 +78,7 @@ extension StatusView {
|
|||
@Published public var expired: Bool = false
|
||||
|
||||
// Card
|
||||
@Published public var card: Card?
|
||||
@Published public var card: Mastodon.Entity.Card?
|
||||
|
||||
// Visibility
|
||||
@Published public var visibility: MastodonVisibility = .public
|
||||
|
|
|
@ -404,7 +404,7 @@ extension StatusView {
|
|||
}
|
||||
|
||||
@objc private func statusCardControlPressed(_ sender: StatusCardControl) {
|
||||
guard let url = viewModel.card?.url else { return }
|
||||
guard let urlString = viewModel.card?.url, let url = URL(string: urlString) else { return }
|
||||
delegate?.statusView(self, didTapCardWithURL: url)
|
||||
}
|
||||
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
|
||||
import UIKit
|
||||
import Combine
|
||||
import CoreDataStack
|
||||
import MastodonSDK
|
||||
|
||||
extension TimelineMiddleLoaderTableViewCell {
|
||||
public class ViewModel {
|
||||
|
@ -34,15 +34,10 @@ extension TimelineMiddleLoaderTableViewCell.ViewModel {
|
|||
|
||||
extension TimelineMiddleLoaderTableViewCell {
|
||||
public func configure(
|
||||
feed: Feed,
|
||||
feed: MastodonFeed,
|
||||
delegate: TimelineMiddleLoaderTableViewCellDelegate?
|
||||
) {
|
||||
feed.publisher(for: \.isLoadingMore)
|
||||
.sink { [weak self] isLoadingMore in
|
||||
guard let self = self else { return }
|
||||
self.viewModel.isFetching = isLoadingMore
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
self.viewModel.isFetching = feed.isLoadingMore
|
||||
|
||||
self.delegate = delegate
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue