mirror of
https://github.com/mastodon/mastodon-ios.git
synced 2025-02-03 02:37:37 +01:00
Improve status updating mechanism (#1210)
This commit is contained in:
parent
c0c795e473
commit
383a75ea48
@ -29,6 +29,7 @@
|
||||
2A3D9B7E29A8F33A00F30313 /* StatusHistoryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A3D9B7D29A8F33A00F30313 /* StatusHistoryView.swift */; };
|
||||
2A3F6FE3292ECB5E002E6DA7 /* FollowedTagsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A3F6FE2292ECB5E002E6DA7 /* FollowedTagsViewModel.swift */; };
|
||||
2A3F6FE5292F6E44002E6DA7 /* FollowedTagsTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A3F6FE4292F6E44002E6DA7 /* FollowedTagsTableViewCell.swift */; };
|
||||
2A409F832B5955290044E472 /* MastodonStatusThreadViewModel+State.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A409F822B5955290044E472 /* MastodonStatusThreadViewModel+State.swift */; };
|
||||
2A506CF4292CD85800059C37 /* FollowedTagsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A506CF3292CD85800059C37 /* FollowedTagsViewController.swift */; };
|
||||
2A506CF6292D040100059C37 /* HashtagTimelineHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A506CF5292D040100059C37 /* HashtagTimelineHeaderView.swift */; };
|
||||
2A64515E29642A8A00CD8B8A /* UniformTypeIdentifiers.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2A6451022964223800CD8B8A /* UniformTypeIdentifiers.framework */; };
|
||||
@ -642,6 +643,7 @@
|
||||
2A3D9B7D29A8F33A00F30313 /* StatusHistoryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusHistoryView.swift; sourceTree = "<group>"; };
|
||||
2A3F6FE2292ECB5E002E6DA7 /* FollowedTagsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowedTagsViewModel.swift; sourceTree = "<group>"; };
|
||||
2A3F6FE4292F6E44002E6DA7 /* FollowedTagsTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowedTagsTableViewCell.swift; sourceTree = "<group>"; };
|
||||
2A409F822B5955290044E472 /* MastodonStatusThreadViewModel+State.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MastodonStatusThreadViewModel+State.swift"; sourceTree = "<group>"; };
|
||||
2A506CF3292CD85800059C37 /* FollowedTagsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowedTagsViewController.swift; sourceTree = "<group>"; };
|
||||
2A506CF5292D040100059C37 /* HashtagTimelineHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HashtagTimelineHeaderView.swift; sourceTree = "<group>"; };
|
||||
2A6451022964223800CD8B8A /* UniformTypeIdentifiers.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UniformTypeIdentifiers.framework; path = System/Library/Frameworks/UniformTypeIdentifiers.framework; sourceTree = SDKROOT; };
|
||||
@ -2690,6 +2692,7 @@
|
||||
DB938F0E2624119800E5B6C1 /* ThreadViewModel+LoadThreadState.swift */,
|
||||
DB938F0826240F3C00E5B6C1 /* RemoteThreadViewModel.swift */,
|
||||
DB0FCB7F27968F70006C02E2 /* MastodonStatusThreadViewModel.swift */,
|
||||
2A409F822B5955290044E472 /* MastodonStatusThreadViewModel+State.swift */,
|
||||
);
|
||||
path = Thread;
|
||||
sourceTree = "<group>";
|
||||
@ -4018,6 +4021,7 @@
|
||||
0FB3D31E25E534C700AAD544 /* PickServerCategoryCollectionViewCell.swift in Sources */,
|
||||
DB0FCB882796BDA9006C02E2 /* SearchItem.swift in Sources */,
|
||||
DB6180ED26391C6C0018D199 /* TransitioningMath.swift in Sources */,
|
||||
2A409F832B5955290044E472 /* MastodonStatusThreadViewModel+State.swift in Sources */,
|
||||
2D6DE40026141DF600A63F6A /* SearchViewModel.swift in Sources */,
|
||||
D886FBD329DF710F00272017 /* WelcomeSeparatorView.swift in Sources */,
|
||||
DB0617FD27855BFE0030EE79 /* ServerRuleItem.swift in Sources */,
|
||||
|
@ -26,11 +26,39 @@ extension StatusItem {
|
||||
case leaf(context: Context)
|
||||
|
||||
public var record: MastodonStatus {
|
||||
switch self {
|
||||
case .root(let threadContext),
|
||||
.reply(let threadContext),
|
||||
.leaf(let threadContext):
|
||||
return threadContext.status
|
||||
get {
|
||||
switch self {
|
||||
case .root(let threadContext),
|
||||
.reply(let threadContext),
|
||||
.leaf(let threadContext):
|
||||
return threadContext.status
|
||||
}
|
||||
}
|
||||
|
||||
set {
|
||||
switch self {
|
||||
case let .root(threadContext):
|
||||
self = .root(context: .init(
|
||||
status: newValue,
|
||||
displayUpperConversationLink: threadContext.displayUpperConversationLink,
|
||||
displayBottomConversationLink: threadContext.displayBottomConversationLink)
|
||||
)
|
||||
|
||||
case let .reply(threadContext):
|
||||
self = .reply(context: .init(
|
||||
status: newValue,
|
||||
displayUpperConversationLink: threadContext.displayUpperConversationLink,
|
||||
displayBottomConversationLink: threadContext.displayBottomConversationLink)
|
||||
)
|
||||
|
||||
case let .leaf(threadContext):
|
||||
self = .leaf(context: .init(
|
||||
status: newValue,
|
||||
displayUpperConversationLink: threadContext.displayUpperConversationLink,
|
||||
displayBottomConversationLink: threadContext.displayBottomConversationLink)
|
||||
)
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -12,12 +12,13 @@ import MastodonCore
|
||||
import MastodonSDK
|
||||
|
||||
extension DataSourceFacade {
|
||||
@MainActor
|
||||
public static func responseToStatusBookmarkAction(
|
||||
provider: NeedsDependency & AuthContextProvider & DataSourceProvider,
|
||||
status: MastodonStatus
|
||||
) async throws {
|
||||
let selectionFeedbackGenerator = await UISelectionFeedbackGenerator()
|
||||
await selectionFeedbackGenerator.selectionChanged()
|
||||
let selectionFeedbackGenerator = UISelectionFeedbackGenerator()
|
||||
selectionFeedbackGenerator.selectionChanged()
|
||||
|
||||
let updatedStatus = try await provider.context.apiService.bookmark(
|
||||
record: status,
|
||||
@ -27,6 +28,6 @@ extension DataSourceFacade {
|
||||
let newStatus: MastodonStatus = .fromEntity(updatedStatus)
|
||||
newStatus.isSensitiveToggled = status.isSensitiveToggled
|
||||
|
||||
provider.update(status: newStatus)
|
||||
provider.update(status: newStatus, intent: .bookmark(updatedStatus.bookmarked == true))
|
||||
}
|
||||
}
|
||||
|
@ -11,12 +11,13 @@ import MastodonSDK
|
||||
import MastodonCore
|
||||
|
||||
extension DataSourceFacade {
|
||||
@MainActor
|
||||
public static func responseToStatusFavoriteAction(
|
||||
provider: DataSourceProvider & AuthContextProvider,
|
||||
status: MastodonStatus
|
||||
) async throws {
|
||||
let selectionFeedbackGenerator = await UISelectionFeedbackGenerator()
|
||||
await selectionFeedbackGenerator.selectionChanged()
|
||||
let selectionFeedbackGenerator = UISelectionFeedbackGenerator()
|
||||
selectionFeedbackGenerator.selectionChanged()
|
||||
|
||||
let updatedStatus = try await provider.context.apiService.favorite(
|
||||
status: status,
|
||||
@ -26,6 +27,6 @@ extension DataSourceFacade {
|
||||
let newStatus: MastodonStatus = .fromEntity(updatedStatus)
|
||||
newStatus.isSensitiveToggled = status.isSensitiveToggled
|
||||
|
||||
provider.update(status: newStatus)
|
||||
provider.update(status: newStatus, intent: .favorite(updatedStatus.favourited == true))
|
||||
}
|
||||
}
|
||||
|
@ -11,12 +11,13 @@ import MastodonUI
|
||||
import MastodonSDK
|
||||
|
||||
extension DataSourceFacade {
|
||||
@MainActor
|
||||
static func responseToStatusReblogAction(
|
||||
provider: DataSourceProvider & AuthContextProvider,
|
||||
status: MastodonStatus
|
||||
) async throws {
|
||||
let selectionFeedbackGenerator = await UISelectionFeedbackGenerator()
|
||||
await selectionFeedbackGenerator.selectionChanged()
|
||||
let selectionFeedbackGenerator = UISelectionFeedbackGenerator()
|
||||
selectionFeedbackGenerator.selectionChanged()
|
||||
|
||||
let updatedStatus = try await provider.context.apiService.reblog(
|
||||
status: status,
|
||||
@ -27,6 +28,6 @@ extension DataSourceFacade {
|
||||
newStatus.reblog?.isSensitiveToggled = status.isSensitiveToggled
|
||||
newStatus.isSensitiveToggled = status.isSensitiveToggled
|
||||
|
||||
provider.update(status: newStatus)
|
||||
provider.update(status: newStatus, intent: .reblog(updatedStatus.reblogged == true))
|
||||
}
|
||||
}
|
||||
|
@ -28,7 +28,7 @@ extension DataSourceFacade {
|
||||
authenticationBox: dependency.authContext.mastodonAuthenticationBox
|
||||
).value.asMastodonStatus
|
||||
|
||||
dependency.delete(status: deletedStatus)
|
||||
dependency.update(status: deletedStatus, intent: .delete)
|
||||
}
|
||||
|
||||
}
|
||||
@ -430,7 +430,7 @@ extension DataSourceFacade {
|
||||
}
|
||||
|
||||
extension DataSourceFacade {
|
||||
|
||||
@MainActor
|
||||
static func responseToToggleSensitiveAction(
|
||||
dependency: NeedsDependency & DataSourceProvider,
|
||||
status: MastodonStatus
|
||||
@ -440,7 +440,7 @@ extension DataSourceFacade {
|
||||
let newStatus: MastodonStatus = .fromEntity(_status.entity)
|
||||
newStatus.isSensitiveToggled = !_status.isSensitiveToggled
|
||||
|
||||
dependency.update(status: newStatus)
|
||||
dependency.update(status: newStatus, intent: .toggleSensitive(newStatus.isSensitiveToggled))
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -89,6 +89,7 @@ extension NotificationTableViewCellDelegate where Self: DataSourceProvider & Aut
|
||||
// MARK: - Follow Request
|
||||
extension NotificationTableViewCellDelegate where Self: DataSourceProvider & AuthContextProvider {
|
||||
|
||||
@MainActor
|
||||
func tableViewCell(
|
||||
_ cell: UITableViewCell,
|
||||
notificationView: NotificationView,
|
||||
@ -105,14 +106,30 @@ extension NotificationTableViewCellDelegate where Self: DataSourceProvider & Aut
|
||||
return
|
||||
}
|
||||
|
||||
try await DataSourceFacade.responseToUserFollowRequestAction(
|
||||
dependency: self,
|
||||
notification: notification,
|
||||
query: .accept
|
||||
)
|
||||
let originalTransientFollowRequestState = notificationView.viewModel.transientFollowRequestState
|
||||
let originalFollowRequestState = notificationView.viewModel.followRequestState
|
||||
|
||||
notificationView.viewModel.transientFollowRequestState = .init(state: .isAccepting)
|
||||
notificationView.viewModel.followRequestState = .init(state: .isAccepting)
|
||||
|
||||
do {
|
||||
try await DataSourceFacade.responseToUserFollowRequestAction(
|
||||
dependency: self,
|
||||
notification: notification,
|
||||
query: .accept
|
||||
)
|
||||
|
||||
notificationView.viewModel.transientFollowRequestState = .init(state: .isAccept)
|
||||
notificationView.viewModel.followRequestState = .init(state: .isAccept)
|
||||
} catch {
|
||||
notificationView.viewModel.transientFollowRequestState = originalTransientFollowRequestState
|
||||
notificationView.viewModel.followRequestState = originalFollowRequestState
|
||||
throw error
|
||||
}
|
||||
} // end Task
|
||||
}
|
||||
|
||||
@MainActor
|
||||
func tableViewCell(
|
||||
_ cell: UITableViewCell,
|
||||
notificationView: NotificationView,
|
||||
@ -129,11 +146,26 @@ extension NotificationTableViewCellDelegate where Self: DataSourceProvider & Aut
|
||||
return
|
||||
}
|
||||
|
||||
try await DataSourceFacade.responseToUserFollowRequestAction(
|
||||
dependency: self,
|
||||
notification: notification,
|
||||
query: .reject
|
||||
)
|
||||
let originalTransientFollowRequestState = notificationView.viewModel.transientFollowRequestState
|
||||
let originalFollowRequestState = notificationView.viewModel.followRequestState
|
||||
|
||||
notificationView.viewModel.transientFollowRequestState = .init(state: .isRejecting)
|
||||
notificationView.viewModel.followRequestState = .init(state: .isRejecting)
|
||||
|
||||
do {
|
||||
try await DataSourceFacade.responseToUserFollowRequestAction(
|
||||
dependency: self,
|
||||
notification: notification,
|
||||
query: .reject
|
||||
)
|
||||
|
||||
notificationView.viewModel.transientFollowRequestState = .init(state: .isReject)
|
||||
notificationView.viewModel.followRequestState = .init(state: .isReject)
|
||||
} catch {
|
||||
notificationView.viewModel.transientFollowRequestState = originalTransientFollowRequestState
|
||||
notificationView.viewModel.followRequestState = originalFollowRequestState
|
||||
throw error
|
||||
}
|
||||
} // end Task
|
||||
}
|
||||
|
||||
|
@ -39,6 +39,5 @@ extension DataSourceItem {
|
||||
|
||||
protocol DataSourceProvider: ViewControllerWithDependencies {
|
||||
func item(from source: DataSourceItem.Source) async -> DataSourceItem?
|
||||
func update(status: MastodonStatus)
|
||||
func delete(status: MastodonStatus)
|
||||
func update(status: MastodonStatus, intent: MastodonStatus.UpdateIntent)
|
||||
}
|
||||
|
@ -28,16 +28,10 @@ extension DiscoveryCommunityViewController: DataSourceProvider {
|
||||
}
|
||||
}
|
||||
|
||||
func update(status: MastodonStatus) {
|
||||
viewModel.dataController.update(status: status)
|
||||
func update(status: MastodonStatus, intent: MastodonStatus.UpdateIntent) {
|
||||
viewModel.dataController.update(status: status, intent: intent)
|
||||
}
|
||||
|
||||
func delete(status: MastodonStatus) {
|
||||
viewModel.dataController.setRecords(
|
||||
viewModel.dataController.records.filter { $0.id != status.id }
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@MainActor
|
||||
private func indexPath(for cell: UITableViewCell) async -> IndexPath? {
|
||||
return tableView.indexPath(for: cell)
|
||||
|
@ -28,16 +28,10 @@ extension DiscoveryPostsViewController: DataSourceProvider {
|
||||
}
|
||||
}
|
||||
|
||||
func update(status: MastodonStatus) {
|
||||
viewModel.dataController.update(status: status)
|
||||
func update(status: MastodonStatus, intent: MastodonStatus.UpdateIntent) {
|
||||
viewModel.dataController.update(status: status, intent: intent)
|
||||
}
|
||||
|
||||
func delete(status: MastodonStatus) {
|
||||
viewModel.dataController.setRecords(
|
||||
viewModel.dataController.records.filter { $0.id != status.id }
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@MainActor
|
||||
private func indexPath(for cell: UITableViewCell) async -> IndexPath? {
|
||||
return tableView.indexPath(for: cell)
|
||||
|
@ -28,14 +28,10 @@ extension HashtagTimelineViewController: DataSourceProvider {
|
||||
}
|
||||
}
|
||||
|
||||
func update(status: MastodonStatus) {
|
||||
viewModel.dataController.update(status: status)
|
||||
func update(status: MastodonStatus, intent: MastodonStatus.UpdateIntent) {
|
||||
viewModel.dataController.update(status: status, intent: intent)
|
||||
}
|
||||
|
||||
func delete(status: MastodonStatus) {
|
||||
viewModel.dataController.deleteRecord(status)
|
||||
}
|
||||
|
||||
|
||||
@MainActor
|
||||
private func indexPath(for cell: UITableViewCell) async -> IndexPath? {
|
||||
return tableView.indexPath(for: cell)
|
||||
|
@ -33,14 +33,10 @@ extension HomeTimelineViewController: DataSourceProvider {
|
||||
}
|
||||
}
|
||||
|
||||
func update(status: MastodonStatus) {
|
||||
viewModel.dataController.update(status: status)
|
||||
func update(status: MastodonStatus, intent: MastodonStatus.UpdateIntent) {
|
||||
viewModel.dataController.update(status: status, intent: intent)
|
||||
}
|
||||
|
||||
func delete(status: MastodonStatus) {
|
||||
viewModel.dataController.records = viewModel.dataController.records.filter { $0.id != status.id }
|
||||
}
|
||||
|
||||
|
||||
@MainActor
|
||||
private func indexPath(for cell: UITableViewCell) async -> IndexPath? {
|
||||
return tableView.indexPath(for: cell)
|
||||
|
@ -168,9 +168,10 @@ extension HomeTimelineViewController {
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
|
||||
context.publisherService.statusPublishResult.sink { result in
|
||||
if case .success(.edit) = result {
|
||||
context.publisherService.statusPublishResult.receive(on: DispatchQueue.main).sink { result in
|
||||
if case .success(.edit(let status)) = result {
|
||||
self.viewModel.hasPendingStatusEditReload = true
|
||||
self.viewModel.dataController.update(status: .fromEntity(status.value), intent: .edit)
|
||||
}
|
||||
}.store(in: &disposeBag)
|
||||
|
||||
|
@ -88,7 +88,7 @@ extension HomeTimelineViewModel.LoadLatestState {
|
||||
|
||||
Task {
|
||||
let latestStatusIDs: [Status.ID] = latestFeedRecords.compactMap { record in
|
||||
return record.status?.id
|
||||
return record.status?.reblog?.id ?? record.status?.id
|
||||
}
|
||||
|
||||
do {
|
||||
@ -103,7 +103,7 @@ extension HomeTimelineViewModel.LoadLatestState {
|
||||
|
||||
// stop refresher if no new statuses
|
||||
let statuses = response.value
|
||||
let newStatuses = statuses.filter { !latestStatusIDs.contains($0.id) }
|
||||
let newStatuses = statuses.filter { status in !latestStatusIDs.contains(where: { $0 == status.reblog?.id || $0 == status.id }) }
|
||||
|
||||
if newStatuses.isEmpty {
|
||||
viewModel.didLoadLatest.send()
|
||||
@ -112,10 +112,10 @@ extension HomeTimelineViewModel.LoadLatestState {
|
||||
viewModel.homeTimelineNavigationBarTitleViewModel.newPostsIncoming()
|
||||
}
|
||||
|
||||
var newRecords: [MastodonFeed] = newStatuses.map {
|
||||
MastodonFeed.fromStatus(.fromEntity($0), kind: .home)
|
||||
}
|
||||
viewModel.dataController.records = {
|
||||
var newRecords: [MastodonFeed] = newStatuses.map {
|
||||
MastodonFeed.fromStatus(.fromEntity($0), kind: .home)
|
||||
}
|
||||
var oldRecords = viewModel.dataController.records
|
||||
for (i, record) in newRecords.enumerated() {
|
||||
if let index = oldRecords.firstIndex(where: { $0.status?.reblog?.id == record.id || $0.status?.id == record.id }) {
|
||||
|
@ -37,14 +37,10 @@ extension NotificationTimelineViewController: DataSourceProvider {
|
||||
}
|
||||
}
|
||||
|
||||
func update(status: MastodonStatus) {
|
||||
viewModel.dataController.update(status: status)
|
||||
func update(status: MastodonStatus, intent: MastodonStatus.UpdateIntent) {
|
||||
viewModel.dataController.update(status: status, intent: intent)
|
||||
}
|
||||
|
||||
func delete(status: MastodonStatus) {
|
||||
viewModel.dataController.delete(status: status)
|
||||
}
|
||||
|
||||
|
||||
@MainActor
|
||||
private func indexPath(for cell: UITableViewCell) async -> IndexPath? {
|
||||
return tableView.indexPath(for: cell)
|
||||
|
@ -44,7 +44,7 @@ extension NotificationTimelineViewModel {
|
||||
}
|
||||
var snapshot = NSDiffableDataSourceSnapshot<NotificationSection, NotificationItem>()
|
||||
snapshot.appendSections([.main])
|
||||
snapshot.appendItems(newItems, toSection: .main)
|
||||
snapshot.appendItems(newItems.removingDuplicates(), toSection: .main)
|
||||
return snapshot
|
||||
}()
|
||||
|
||||
|
@ -28,14 +28,8 @@ extension BookmarkViewController: DataSourceProvider {
|
||||
}
|
||||
}
|
||||
|
||||
func update(status: MastodonStatus) {
|
||||
viewModel.dataController.update(status: status)
|
||||
}
|
||||
|
||||
func delete(status: MastodonStatus) {
|
||||
viewModel.dataController.setRecords(
|
||||
viewModel.dataController.records.filter { $0.id != status.id }
|
||||
)
|
||||
func update(status: MastodonStatus, intent: MastodonStatus.UpdateIntent) {
|
||||
viewModel.dataController.update(status: status, intent: intent)
|
||||
}
|
||||
|
||||
@MainActor
|
||||
|
@ -103,11 +103,7 @@ extension FamiliarFollowersViewController: DataSourceProvider {
|
||||
}
|
||||
}
|
||||
|
||||
func update(status: MastodonStatus) {
|
||||
assertionFailure("Not required")
|
||||
}
|
||||
|
||||
func delete(status: MastodonStatus) {
|
||||
func update(status: MastodonStatus, intent: MastodonStatus.UpdateIntent) {
|
||||
assertionFailure("Not required")
|
||||
}
|
||||
|
||||
|
@ -28,16 +28,10 @@ extension FavoriteViewController: DataSourceProvider {
|
||||
}
|
||||
}
|
||||
|
||||
func update(status: MastodonStatus) {
|
||||
viewModel.dataController.update(status: status)
|
||||
func update(status: MastodonStatus, intent: MastodonStatus.UpdateIntent) {
|
||||
viewModel.dataController.update(status: status, intent: intent)
|
||||
}
|
||||
|
||||
func delete(status: MastodonStatus) {
|
||||
viewModel.dataController.setRecords(
|
||||
viewModel.dataController.records.filter { $0.id != status.id }
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@MainActor
|
||||
private func indexPath(for cell: UITableViewCell) async -> IndexPath? {
|
||||
return tableView.indexPath(for: cell)
|
||||
|
@ -154,11 +154,7 @@ extension FollowerListViewController: DataSourceProvider {
|
||||
}
|
||||
}
|
||||
|
||||
func update(status: MastodonStatus) {
|
||||
assertionFailure("Not required")
|
||||
}
|
||||
|
||||
func delete(status: MastodonStatus) {
|
||||
func update(status: MastodonStatus, intent: MastodonStatus.UpdateIntent) {
|
||||
assertionFailure("Not required")
|
||||
}
|
||||
|
||||
|
@ -150,11 +150,7 @@ extension FollowingListViewController: DataSourceProvider {
|
||||
}
|
||||
}
|
||||
|
||||
func update(status: MastodonStatus) {
|
||||
assertionFailure("Not required")
|
||||
}
|
||||
|
||||
func delete(status: MastodonStatus) {
|
||||
func update(status: MastodonStatus, intent: MastodonStatus.UpdateIntent) {
|
||||
assertionFailure("Not required")
|
||||
}
|
||||
|
||||
|
@ -254,6 +254,12 @@ extension ProfileViewController {
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
|
||||
context.publisherService.statusPublishResult.sink { [weak self] result in
|
||||
if case .success(.edit(let status)) = result {
|
||||
self?.updateViewModelsWithDataControllers(status: .fromEntity(status.value), intent: .edit)
|
||||
}
|
||||
}.store(in: &disposeBag)
|
||||
|
||||
addChild(tabBarPagerController)
|
||||
tabBarPagerController.view.translatesAutoresizingMaskIntoConstraints = false
|
||||
view.addSubview(tabBarPagerController.view)
|
||||
@ -971,11 +977,13 @@ extension ProfileViewController: DataSourceProvider {
|
||||
return nil
|
||||
}
|
||||
|
||||
func update(status: MastodonStatus) {
|
||||
assertionFailure("Not required")
|
||||
func update(status: MastodonStatus, intent: MastodonStatus.UpdateIntent) {
|
||||
updateViewModelsWithDataControllers(status: status, intent: intent)
|
||||
}
|
||||
|
||||
func delete(status: MastodonStatus) {
|
||||
assertionFailure("Not required")
|
||||
func updateViewModelsWithDataControllers(status: MastodonStatus, intent: MastodonStatus.UpdateIntent) {
|
||||
viewModel.postsUserTimelineViewModel.dataController.update(status: status, intent: intent)
|
||||
viewModel.repliesUserTimelineViewModel.dataController.update(status: status, intent: intent)
|
||||
viewModel.mediaUserTimelineViewModel.dataController.update(status: status, intent: intent)
|
||||
}
|
||||
}
|
||||
|
@ -28,14 +28,10 @@ extension UserTimelineViewController: DataSourceProvider {
|
||||
}
|
||||
}
|
||||
|
||||
func update(status: MastodonStatus) {
|
||||
viewModel.dataController.update(status: status)
|
||||
func update(status: MastodonStatus, intent: MastodonStatus.UpdateIntent) {
|
||||
viewModel.dataController.update(status: status, intent: intent)
|
||||
}
|
||||
|
||||
func delete(status: MastodonStatus) {
|
||||
viewModel.dataController.deleteRecord(status)
|
||||
}
|
||||
|
||||
|
||||
@MainActor
|
||||
private func indexPath(for cell: UITableViewCell) async -> IndexPath? {
|
||||
return tableView.indexPath(for: cell)
|
||||
|
@ -28,14 +28,10 @@ extension FavoritedByViewController: DataSourceProvider {
|
||||
}
|
||||
}
|
||||
|
||||
func update(status: MastodonStatus) {
|
||||
func update(status: MastodonStatus, intent: MastodonStatus.UpdateIntent) {
|
||||
assertionFailure("Not required")
|
||||
}
|
||||
|
||||
func delete(status: MastodonStatus) {
|
||||
assertionFailure("Not required")
|
||||
}
|
||||
|
||||
|
||||
@MainActor
|
||||
private func indexPath(for cell: UITableViewCell) async -> IndexPath? {
|
||||
return tableView.indexPath(for: cell)
|
||||
|
@ -29,11 +29,7 @@ extension RebloggedByViewController: DataSourceProvider {
|
||||
}
|
||||
}
|
||||
|
||||
func update(status: MastodonStatus) {
|
||||
assertionFailure("Not required")
|
||||
}
|
||||
|
||||
func delete(status: MastodonStatus) {
|
||||
func update(status: MastodonStatus, intent: MastodonStatus.UpdateIntent) {
|
||||
assertionFailure("Not required")
|
||||
}
|
||||
|
||||
|
@ -29,14 +29,10 @@ extension SearchHistoryViewController: DataSourceProvider {
|
||||
}
|
||||
}
|
||||
|
||||
func update(status: MastodonStatus) {
|
||||
func update(status: MastodonStatus, intent: MastodonStatus.UpdateIntent) {
|
||||
assertionFailure("Not required")
|
||||
}
|
||||
|
||||
func delete(status: MastodonStatus) {
|
||||
assertionFailure("Not required")
|
||||
}
|
||||
|
||||
|
||||
@MainActor
|
||||
private func indexPath(for cell: UICollectionViewCell) async -> IndexPath? {
|
||||
return collectionView.indexPath(for: cell)
|
||||
|
@ -33,12 +33,8 @@ extension SearchResultViewController: DataSourceProvider {
|
||||
}
|
||||
}
|
||||
|
||||
func update(status: MastodonStatus) {
|
||||
viewModel.dataController.update(status: status)
|
||||
}
|
||||
|
||||
func delete(status: MastodonStatus) {
|
||||
viewModel.dataController.deleteRecord(status)
|
||||
func update(status: MastodonStatus, intent: MastodonStatus.UpdateIntent) {
|
||||
viewModel.dataController.update(status: status, intent: intent)
|
||||
}
|
||||
|
||||
@MainActor
|
||||
|
@ -14,7 +14,7 @@ final class ListBatchFetchViewModel {
|
||||
var disposeBag = Set<AnyCancellable>()
|
||||
|
||||
// timer running on `common` mode
|
||||
let timerPublisher = Timer.publish(every: 1.0, on: .main, in: .common)
|
||||
let timerPublisher = Timer.publish(every: 30.0, on: .main, in: .common)
|
||||
.autoconnect()
|
||||
.share()
|
||||
.eraseToAnyPublisher()
|
||||
|
165
Mastodon/Scene/Thread/MastodonStatusThreadViewModel+State.swift
Normal file
165
Mastodon/Scene/Thread/MastodonStatusThreadViewModel+State.swift
Normal file
@ -0,0 +1,165 @@
|
||||
// Copyright © 2024 Mastodon gGmbH. All rights reserved.
|
||||
|
||||
import Foundation
|
||||
import MastodonSDK
|
||||
|
||||
extension MastodonStatusThreadViewModel {
|
||||
// Bookmark
|
||||
func handleBookmark(_ status: MastodonStatus) {
|
||||
ancestors = handleBookmark(status, items: ancestors)
|
||||
descendants = handleBookmark(status, items: descendants)
|
||||
}
|
||||
|
||||
private func handleBookmark(_ status: MastodonStatus, items: [StatusItem]) -> [StatusItem] {
|
||||
var newRecords = Array(items)
|
||||
guard let index = newRecords.firstIndex(where: { $0.mastodonStatus?.id == status.id }) else {
|
||||
return items
|
||||
}
|
||||
var newRecord = newRecords[index]
|
||||
newRecord.mastodonStatus = status
|
||||
newRecords[index] = newRecord
|
||||
return newRecords
|
||||
}
|
||||
|
||||
// Favorite
|
||||
func handleFavorite(_ status: MastodonStatus) {
|
||||
ancestors = handleFavorite(status, items: ancestors)
|
||||
descendants = handleFavorite(status, items: descendants)
|
||||
}
|
||||
|
||||
private func handleFavorite(_ status: MastodonStatus, items: [StatusItem]) -> [StatusItem] {
|
||||
var newRecords = Array(items)
|
||||
guard let index = newRecords.firstIndex(where: { $0.mastodonStatus?.id == status.id }) else {
|
||||
return items
|
||||
}
|
||||
var newRecord = newRecords[index]
|
||||
newRecord.mastodonStatus = status
|
||||
newRecords[index] = newRecord
|
||||
return newRecords
|
||||
}
|
||||
|
||||
// Reblog
|
||||
func handleReblog(_ status: MastodonStatus, _ isReblogged: Bool) {
|
||||
ancestors = handleReblog(status, isReblogged, items: ancestors)
|
||||
descendants = handleReblog(status, isReblogged, items: descendants)
|
||||
}
|
||||
|
||||
private func handleReblog(_ status: MastodonStatus, _ isReblogged: Bool, items: [StatusItem]) -> [StatusItem] {
|
||||
var newRecords = Array(items)
|
||||
|
||||
switch isReblogged {
|
||||
case true:
|
||||
let index: Int
|
||||
if let idx = newRecords.firstIndex(where: { $0.mastodonStatus?.reblog?.id == status.reblog?.id }) {
|
||||
index = idx
|
||||
} else if let idx = newRecords.firstIndex(where: { $0.mastodonStatus?.id == status.reblog?.id }) {
|
||||
index = idx
|
||||
} else {
|
||||
logger.warning("\(Self.entryNotFoundMessage)")
|
||||
return newRecords
|
||||
}
|
||||
var newRecord = newRecords[index]
|
||||
newRecord.mastodonStatus = status.inheritSensitivityToggled(from: newRecord.mastodonStatus)
|
||||
newRecords[index] = newRecord
|
||||
case false:
|
||||
let index: Int
|
||||
if let idx = newRecords.firstIndex(where: { $0.mastodonStatus?.reblog?.id == status.id }) {
|
||||
index = idx
|
||||
} else if let idx = newRecords.firstIndex(where: { $0.mastodonStatus?.id == status.id }) {
|
||||
index = idx
|
||||
} else {
|
||||
logger.warning("\(Self.entryNotFoundMessage)")
|
||||
return newRecords
|
||||
}
|
||||
var newRecord = newRecords[index]
|
||||
newRecord.mastodonStatus = status.inheritSensitivityToggled(from: newRecord.mastodonStatus)
|
||||
newRecords[index] = newRecord
|
||||
}
|
||||
|
||||
return newRecords
|
||||
}
|
||||
|
||||
// Sensitive
|
||||
func handleSensitive(_ status: MastodonStatus, _ isVisible: Bool) {
|
||||
ancestors = handleSensitive(status, isVisible, ancestors)
|
||||
descendants = handleSensitive(status, isVisible, descendants)
|
||||
}
|
||||
|
||||
private func handleSensitive(_ status: MastodonStatus, _ isVisible: Bool, _ items: [StatusItem]) -> [StatusItem] {
|
||||
var newRecords = Array(items)
|
||||
guard let index = newRecords.firstIndex(where: { $0.mastodonStatus?.id == status.id }) else {
|
||||
return items
|
||||
}
|
||||
var newRecord = newRecords[index]
|
||||
newRecord.mastodonStatus = status
|
||||
newRecords[index] = newRecord
|
||||
return newRecords
|
||||
}
|
||||
|
||||
// Edit
|
||||
func handleEdit(_ status: MastodonStatus) {
|
||||
ancestors = handleEdit(status, items: ancestors)
|
||||
descendants = handleEdit(status, items: descendants)
|
||||
}
|
||||
|
||||
private func handleEdit(_ status: MastodonStatus, items: [StatusItem]) -> [StatusItem] {
|
||||
var newRecords = Array(items)
|
||||
guard let index = newRecords.firstIndex(where: { $0.mastodonStatus?.id == status.id }) else {
|
||||
return items
|
||||
}
|
||||
var newRecord = newRecords[index]
|
||||
newRecord.mastodonStatus = status
|
||||
newRecords[index] = newRecord
|
||||
return newRecords
|
||||
}
|
||||
|
||||
// Delete
|
||||
func handleDelete(_ status: MastodonStatus) {
|
||||
ancestors = handleDelete(status, ancestors)
|
||||
descendants = handleDelete(status, descendants)
|
||||
}
|
||||
|
||||
private func handleDelete(_ status: MastodonStatus, _ items: [StatusItem]) -> [StatusItem] {
|
||||
var newRecords = Array(items)
|
||||
newRecords.removeAll(where: { $0.mastodonStatus?.id == status.id })
|
||||
return newRecords
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private extension StatusItem {
|
||||
var mastodonStatus: MastodonStatus? {
|
||||
get {
|
||||
switch self {
|
||||
case .feed(let record):
|
||||
return record.status
|
||||
case .feedLoader(let record):
|
||||
return record.status
|
||||
case .status(let record):
|
||||
return record
|
||||
case .thread(let thread):
|
||||
return thread.record
|
||||
case .topLoader, .bottomLoader:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
set {
|
||||
guard let status = newValue else { return }
|
||||
switch self {
|
||||
case .feed(let record):
|
||||
self = .feed(record: .fromStatus(status, kind: record.kind))
|
||||
case .feedLoader(let record):
|
||||
self = .feedLoader(record: .fromStatus(status, kind: record.kind))
|
||||
case .status:
|
||||
self = .status(record: status)
|
||||
case let .thread(thread):
|
||||
var newThread = thread
|
||||
newThread.record = status
|
||||
self = .thread(newThread)
|
||||
case .topLoader, .bottomLoader:
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -13,9 +13,12 @@ import CoreDataStack
|
||||
import MastodonSDK
|
||||
import MastodonCore
|
||||
import MastodonMeta
|
||||
import os.log
|
||||
|
||||
final class MastodonStatusThreadViewModel {
|
||||
|
||||
let logger = Logger(subsystem: "MastodonStatusThreadViewModel", category: "Data")
|
||||
static let entryNotFoundMessage = "Failed to find suitable record. Depending on the context this might result in errors (data not being updated) or can be discarded (e.g. when there are mixed data sources where an entry might or might not exist)."
|
||||
|
||||
var disposeBag = Set<AnyCancellable>()
|
||||
|
||||
// input
|
||||
|
@ -29,121 +29,63 @@ extension ThreadViewController: DataSourceProvider {
|
||||
}
|
||||
}
|
||||
|
||||
func update(status: MastodonStatus) {
|
||||
func update(status _status: MastodonStatus, intent: MastodonStatus.UpdateIntent) {
|
||||
let status = _status.reblog ?? _status
|
||||
if case MastodonStatus.UpdateIntent.delete = intent {
|
||||
return handleDelete(status)
|
||||
}
|
||||
|
||||
switch viewModel.root {
|
||||
case let .root(context):
|
||||
if context.status.id == status.id {
|
||||
viewModel.root = .root(context: .init(status: status))
|
||||
} else {
|
||||
handle(status: status)
|
||||
handleUpdate(status: status, viewModel: viewModel.mastodonStatusThreadViewModel, intent: intent)
|
||||
}
|
||||
case let .reply(context):
|
||||
if context.status.id == status.id {
|
||||
viewModel.root = .reply(context: .init(status: status))
|
||||
} else {
|
||||
handle(status: status)
|
||||
handleUpdate(status: status, viewModel: viewModel.mastodonStatusThreadViewModel, intent: intent)
|
||||
}
|
||||
case let .leaf(context):
|
||||
if context.status.id == status.id {
|
||||
viewModel.root = .leaf(context: .init(status: status))
|
||||
} else {
|
||||
handle(status: status)
|
||||
handleUpdate(status: status, viewModel: viewModel.mastodonStatusThreadViewModel, intent: intent)
|
||||
}
|
||||
case .none:
|
||||
assertionFailure("This should not have happened")
|
||||
}
|
||||
}
|
||||
|
||||
private func handle(status: MastodonStatus) {
|
||||
viewModel.mastodonStatusThreadViewModel.ancestors.handleUpdate(status: status, for: viewModel)
|
||||
viewModel.mastodonStatusThreadViewModel.descendants.handleUpdate(status: status, for: viewModel)
|
||||
}
|
||||
|
||||
func delete(status: MastodonStatus) {
|
||||
|
||||
private func handleDelete(_ status: MastodonStatus) {
|
||||
if viewModel.root?.record.id == status.id {
|
||||
viewModel.root = nil
|
||||
viewModel.onDismiss.send(status)
|
||||
}
|
||||
viewModel.mastodonStatusThreadViewModel.ancestors.handleDelete(status: status, for: viewModel)
|
||||
viewModel.mastodonStatusThreadViewModel.descendants.handleDelete(status: status, for: viewModel)
|
||||
viewModel.mastodonStatusThreadViewModel.handleDelete(status)
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private func indexPath(for cell: UITableViewCell) async -> IndexPath? {
|
||||
return tableView.indexPath(for: cell)
|
||||
}
|
||||
}
|
||||
|
||||
private extension [StatusItem] {
|
||||
mutating func handleUpdate(status: MastodonStatus, for viewModel: ThreadViewModel) {
|
||||
for (index, ancestor) in enumerated() {
|
||||
switch ancestor {
|
||||
case let .feed(record):
|
||||
if record.status?.id == status.id {
|
||||
self[index] = .feed(record: .fromStatus(status, kind: record.kind))
|
||||
}
|
||||
case let.feedLoader(record):
|
||||
if record.status?.id == status.id {
|
||||
self[index] = .feedLoader(record: .fromStatus(status, kind: record.kind))
|
||||
}
|
||||
case let .status(record):
|
||||
if record.id == status.id {
|
||||
self[index] = .status(record: status)
|
||||
}
|
||||
case let .thread(thread):
|
||||
switch thread {
|
||||
case let .root(context):
|
||||
if context.status.id == status.id {
|
||||
self[index] = .thread(.root(context: .init(status: status)))
|
||||
}
|
||||
case let .reply(context):
|
||||
if context.status.id == status.id {
|
||||
self[index] = .thread(.reply(context: .init(status: status)))
|
||||
}
|
||||
case let .leaf(context):
|
||||
if context.status.id == status.id {
|
||||
self[index] = .thread(.leaf(context: .init(status: status)))
|
||||
}
|
||||
}
|
||||
case .bottomLoader, .topLoader:
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mutating func handleDelete(status: MastodonStatus, for viewModel: ThreadViewModel) {
|
||||
for (index, ancestor) in enumerated() {
|
||||
switch ancestor {
|
||||
case let .feed(record):
|
||||
if record.status?.id == status.id {
|
||||
self.remove(at: index)
|
||||
}
|
||||
case let.feedLoader(record):
|
||||
if record.status?.id == status.id {
|
||||
self.remove(at: index)
|
||||
}
|
||||
case let .status(record):
|
||||
if record.id == status.id {
|
||||
self.remove(at: index)
|
||||
}
|
||||
case let .thread(thread):
|
||||
switch thread {
|
||||
case let .root(context):
|
||||
if context.status.id == status.id {
|
||||
self.remove(at: index)
|
||||
}
|
||||
case let .reply(context):
|
||||
if context.status.id == status.id {
|
||||
self.remove(at: index)
|
||||
}
|
||||
case let .leaf(context):
|
||||
if context.status.id == status.id {
|
||||
self.remove(at: index)
|
||||
}
|
||||
}
|
||||
case .bottomLoader, .topLoader:
|
||||
break
|
||||
}
|
||||
private func handleUpdate(status: MastodonStatus, viewModel: MastodonStatusThreadViewModel, intent: MastodonStatus.UpdateIntent) {
|
||||
switch intent {
|
||||
case .bookmark:
|
||||
viewModel.handleBookmark(status)
|
||||
case let .reblog(isReblogged):
|
||||
viewModel.handleReblog(status, isReblogged)
|
||||
case .favorite:
|
||||
viewModel.handleFavorite(status)
|
||||
case let .toggleSensitive(isVisible):
|
||||
viewModel.handleSensitive(status, isVisible)
|
||||
case .edit:
|
||||
viewModel.handleEdit(status)
|
||||
case .delete:
|
||||
break // this case has already been handled
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -82,7 +82,7 @@ extension ThreadViewController {
|
||||
viewModel.onEdit
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink(receiveValue: { [weak self] status in
|
||||
self?.navigationController?.notifyChildrenAboutStatusUpdate(status)
|
||||
self?.navigationController?.notifyChildrenAboutStatusEdit(status)
|
||||
})
|
||||
.store(in: &disposeBag)
|
||||
|
||||
@ -202,13 +202,13 @@ extension ThreadViewController: StatusTableViewControllerNavigateable {
|
||||
extension UINavigationController {
|
||||
func notifyChildrenAboutStatusDeletion(_ status: MastodonStatus) {
|
||||
viewControllers.compactMap { $0 as? DataSourceProvider }.forEach { provider in
|
||||
provider?.delete(status: status )
|
||||
provider?.update(status: status, intent: .delete)
|
||||
}
|
||||
}
|
||||
|
||||
func notifyChildrenAboutStatusUpdate(_ status: MastodonStatus) {
|
||||
func notifyChildrenAboutStatusEdit(_ status: MastodonStatus) {
|
||||
viewControllers.compactMap { $0 as? DataSourceProvider }.forEach { provider in
|
||||
provider?.update(status: status )
|
||||
provider?.update(status: status, intent: .edit)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -2,8 +2,11 @@ import Foundation
|
||||
import UIKit
|
||||
import Combine
|
||||
import MastodonSDK
|
||||
import os.log
|
||||
|
||||
final public class FeedDataController {
|
||||
private let logger = Logger(subsystem: "FeedDataController", category: "Data")
|
||||
private static let entryNotFoundMessage = "Failed to find suitable record. Depending on the context this might result in errors (data not being updated) or can be discarded (e.g. when there are mixed data sources where an entry might or might not exist)."
|
||||
|
||||
@Published public var records: [MastodonFeed] = []
|
||||
|
||||
@ -17,7 +20,7 @@ final public class FeedDataController {
|
||||
|
||||
public func loadInitial(kind: MastodonFeed.Kind) {
|
||||
Task {
|
||||
records = try await load(kind: kind, sinceId: nil)
|
||||
records = try await load(kind: kind, maxID: nil)
|
||||
}
|
||||
}
|
||||
|
||||
@ -26,58 +29,145 @@ final public class FeedDataController {
|
||||
guard let lastId = records.last?.status?.id else {
|
||||
return loadInitial(kind: kind)
|
||||
}
|
||||
|
||||
records = try await load(kind: kind, sinceId: lastId)
|
||||
|
||||
records += try await load(kind: kind, maxID: lastId)
|
||||
}
|
||||
}
|
||||
|
||||
public func update(status: MastodonStatus) {
|
||||
@MainActor
|
||||
public func update(status: MastodonStatus, intent: MastodonStatus.UpdateIntent) {
|
||||
switch intent {
|
||||
case .delete:
|
||||
delete(status)
|
||||
case .edit:
|
||||
updateEdited(status)
|
||||
case let .bookmark(isBookmarked):
|
||||
updateBookmarked(status, isBookmarked)
|
||||
case let .favorite(isFavorited):
|
||||
updateFavorited(status, isFavorited)
|
||||
case let .reblog(isReblogged):
|
||||
updateReblogged(status, isReblogged)
|
||||
case let .toggleSensitive(isVisible):
|
||||
updateSensitive(status, isVisible)
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private func delete(_ status: MastodonStatus) {
|
||||
records.removeAll { $0.id == status.id }
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private func updateEdited(_ status: MastodonStatus) {
|
||||
var newRecords = Array(records)
|
||||
for (i, record) in newRecords.enumerated() {
|
||||
if record.status?.id == status.id {
|
||||
newRecords[i] = .fromStatus(status, kind: record.kind)
|
||||
} else if let reblog = status.reblog, reblog.id == record.status?.id {
|
||||
newRecords[i] = .fromStatus(status, kind: record.kind)
|
||||
} else if let reblog = record.status?.reblog, reblog.id == status.id {
|
||||
// Handle reblogged state
|
||||
let isRebloggedByAnyOne: Bool = records[i].status!.reblog != nil
|
||||
|
||||
let newStatus: MastodonStatus
|
||||
if isRebloggedByAnyOne {
|
||||
// if status was previously reblogged by me: remove reblogged status
|
||||
if records[i].status!.entity.reblogged == true && status.entity.reblogged == false {
|
||||
newStatus = .fromEntity(status.entity)
|
||||
} else {
|
||||
newStatus = .fromEntity(records[i].status!.entity)
|
||||
}
|
||||
|
||||
} else {
|
||||
newStatus = .fromEntity(status.entity)
|
||||
}
|
||||
|
||||
newStatus.isSensitiveToggled = status.isSensitiveToggled
|
||||
newStatus.reblog = isRebloggedByAnyOne ? .fromEntity(status.entity) : nil
|
||||
|
||||
newRecords[i] = .fromStatus(newStatus, kind: record.kind)
|
||||
|
||||
} else if let reblog = record.status?.reblog, reblog.id == status.reblog?.id {
|
||||
// Handle re-reblogged state
|
||||
newRecords[i] = .fromStatus(status, kind: record.kind)
|
||||
guard let index = newRecords.firstIndex(where: { $0.id == status.id }) else {
|
||||
logger.warning("\(Self.entryNotFoundMessage)")
|
||||
return
|
||||
}
|
||||
let existingRecord = newRecords[index]
|
||||
let newStatus = status.inheritSensitivityToggled(from: existingRecord.status)
|
||||
newRecords[index] = .fromStatus(newStatus, kind: existingRecord.kind)
|
||||
records = newRecords
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private func updateBookmarked(_ status: MastodonStatus, _ isBookmarked: Bool) {
|
||||
var newRecords = Array(records)
|
||||
guard let index = newRecords.firstIndex(where: { $0.id == status.id }) else {
|
||||
logger.warning("\(Self.entryNotFoundMessage)")
|
||||
return
|
||||
}
|
||||
let existingRecord = newRecords[index]
|
||||
let newStatus = status.inheritSensitivityToggled(from: existingRecord.status)
|
||||
newRecords[index] = .fromStatus(newStatus, kind: existingRecord.kind)
|
||||
records = newRecords
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private func updateFavorited(_ status: MastodonStatus, _ isFavorited: Bool) {
|
||||
var newRecords = Array(records)
|
||||
if let index = newRecords.firstIndex(where: { $0.id == status.id }) {
|
||||
// Replace old status entity
|
||||
let existingRecord = newRecords[index]
|
||||
let newStatus = status.inheritSensitivityToggled(from: existingRecord.status).withOriginal(status: existingRecord.status?.originalStatus)
|
||||
newRecords[index] = .fromStatus(newStatus, kind: existingRecord.kind)
|
||||
} else if let index = newRecords.firstIndex(where: { $0.status?.reblog?.id == status.id }) {
|
||||
// Replace reblogged entity of old "parent" status
|
||||
let newStatus: MastodonStatus
|
||||
if let existingEntity = newRecords[index].status?.entity {
|
||||
newStatus = .fromEntity(existingEntity)
|
||||
newStatus.originalStatus = newRecords[index].status?.originalStatus
|
||||
newStatus.reblog = status
|
||||
} else {
|
||||
newStatus = status
|
||||
}
|
||||
newRecords[index] = .fromStatus(newStatus, kind: newRecords[index].kind)
|
||||
} else {
|
||||
logger.warning("\(Self.entryNotFoundMessage)")
|
||||
}
|
||||
records = newRecords
|
||||
}
|
||||
|
||||
public func delete(status: MastodonStatus) {
|
||||
self.records.removeAll { $0.id == status.id }
|
||||
@MainActor
|
||||
private func updateReblogged(_ status: MastodonStatus, _ isReblogged: Bool) {
|
||||
var newRecords = Array(records)
|
||||
|
||||
switch isReblogged {
|
||||
case true:
|
||||
let index: Int
|
||||
if let idx = newRecords.firstIndex(where: { $0.status?.reblog?.id == status.reblog?.id }) {
|
||||
index = idx
|
||||
} else if let idx = newRecords.firstIndex(where: { $0.id == status.reblog?.id }) {
|
||||
index = idx
|
||||
} else {
|
||||
logger.warning("\(Self.entryNotFoundMessage)")
|
||||
return
|
||||
}
|
||||
let existingRecord = newRecords[index]
|
||||
newRecords[index] = .fromStatus(status.withOriginal(status: existingRecord.status), kind: existingRecord.kind)
|
||||
case false:
|
||||
let index: Int
|
||||
if let idx = newRecords.firstIndex(where: { $0.status?.reblog?.id == status.id }) {
|
||||
index = idx
|
||||
} else if let idx = newRecords.firstIndex(where: { $0.status?.id == status.id }) {
|
||||
index = idx
|
||||
} else {
|
||||
logger.warning("\(Self.entryNotFoundMessage)")
|
||||
return
|
||||
}
|
||||
let existingRecord = newRecords[index]
|
||||
let newStatus = existingRecord.status?.originalStatus ?? status.inheritSensitivityToggled(from: existingRecord.status)
|
||||
newRecords[index] = .fromStatus(newStatus, kind: existingRecord.kind)
|
||||
}
|
||||
records = newRecords
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private func updateSensitive(_ status: MastodonStatus, _ isVisible: Bool) {
|
||||
var newRecords = Array(records)
|
||||
if let index = newRecords.firstIndex(where: { $0.status?.reblog?.id == status.id }), let existingEntity = newRecords[index].status?.entity {
|
||||
let existingRecord = newRecords[index]
|
||||
let newStatus: MastodonStatus = .fromEntity(existingEntity)
|
||||
newStatus.reblog = status
|
||||
newRecords[index] = .fromStatus(newStatus, kind: existingRecord.kind)
|
||||
} else if let index = newRecords.firstIndex(where: { $0.id == status.id }), let existingEntity = newRecords[index].status?.entity {
|
||||
let existingRecord = newRecords[index]
|
||||
let newStatus: MastodonStatus = .fromEntity(existingEntity)
|
||||
.inheritSensitivityToggled(from: status)
|
||||
newRecords[index] = .fromStatus(newStatus, kind: existingRecord.kind)
|
||||
} else {
|
||||
logger.warning("\(Self.entryNotFoundMessage)")
|
||||
return
|
||||
}
|
||||
records = newRecords
|
||||
}
|
||||
}
|
||||
|
||||
private extension FeedDataController {
|
||||
func load(kind: MastodonFeed.Kind, sinceId: MastodonStatus.ID?) async throws -> [MastodonFeed] {
|
||||
func load(kind: MastodonFeed.Kind, maxID: MastodonStatus.ID?) async throws -> [MastodonFeed] {
|
||||
switch kind {
|
||||
case .home:
|
||||
return try await context.apiService.homeTimeline(sinceID: sinceId, authenticationBox: authContext.mastodonAuthenticationBox)
|
||||
return try await context.apiService.homeTimeline(maxID: maxID, authenticationBox: authContext.mastodonAuthenticationBox)
|
||||
.value.map { .fromStatus(.fromEntity($0), kind: .home) }
|
||||
case .notificationAll:
|
||||
return try await context.apiService.notifications(maxID: nil, scope: .everything, authenticationBox: authContext.mastodonAuthenticationBox)
|
||||
|
@ -3,8 +3,12 @@ import Combine
|
||||
import CoreData
|
||||
import CoreDataStack
|
||||
import MastodonSDK
|
||||
import os.log
|
||||
|
||||
public final class StatusDataController {
|
||||
private let logger = Logger(subsystem: "StatusDataController", category: "Data")
|
||||
private static let entryNotFoundMessage = "Failed to find suitable record. Depending on the context this might result in errors (data not being updated) or can be discarded (e.g. when there are mixed data sources where an entry might or might not exist)."
|
||||
|
||||
@MainActor
|
||||
@Published
|
||||
public private(set) var records: [MastodonStatus] = []
|
||||
@ -35,39 +39,118 @@ public final class StatusDataController {
|
||||
}
|
||||
|
||||
@MainActor
|
||||
public func update(status: MastodonStatus) {
|
||||
public func update(status: MastodonStatus, intent: MastodonStatus.UpdateIntent) {
|
||||
switch intent {
|
||||
case .delete:
|
||||
deleteRecord(status)
|
||||
case .edit:
|
||||
updateEdited(status)
|
||||
case let .bookmark(isBookmarked):
|
||||
updateBookmarked(status, isBookmarked)
|
||||
case let .favorite(isFavorited):
|
||||
updateFavorited(status, isFavorited)
|
||||
case let .reblog(isReblogged):
|
||||
updateReblogged(status, isReblogged)
|
||||
case let .toggleSensitive(isVisible):
|
||||
updateSensitive(status, isVisible)
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private func updateEdited(_ status: MastodonStatus) {
|
||||
var newRecords = Array(records)
|
||||
for (i, record) in newRecords.enumerated() {
|
||||
if record.id == status.id {
|
||||
newRecords[i] = status
|
||||
} else if let reblog = status.reblog, reblog.id == record.id {
|
||||
newRecords[i] = status
|
||||
} else if let reblog = record.reblog, reblog.id == status.id {
|
||||
// Handle reblogged state
|
||||
let isRebloggedByAnyOne: Bool = records[i].reblog != nil
|
||||
|
||||
let newStatus: MastodonStatus
|
||||
if isRebloggedByAnyOne {
|
||||
// if status was previously reblogged by me: remove reblogged status
|
||||
if records[i].entity.reblogged == true && status.entity.reblogged == false {
|
||||
newStatus = .fromEntity(status.entity)
|
||||
} else {
|
||||
newStatus = .fromEntity(records[i].entity)
|
||||
}
|
||||
|
||||
} else {
|
||||
newStatus = .fromEntity(status.entity)
|
||||
}
|
||||
|
||||
newStatus.isSensitiveToggled = status.isSensitiveToggled
|
||||
newStatus.reblog = isRebloggedByAnyOne ? .fromEntity(status.entity) : nil
|
||||
|
||||
newRecords[i] = newStatus
|
||||
} else if let reblog = record.reblog, reblog.id == status.reblog?.id {
|
||||
// Handle re-reblogged state
|
||||
newRecords[i] = status
|
||||
}
|
||||
guard let index = newRecords.firstIndex(where: { $0.id == status.id }) else {
|
||||
logger.warning("\(Self.entryNotFoundMessage)")
|
||||
return
|
||||
}
|
||||
newRecords[index] = status.inheritSensitivityToggled(from: newRecords[index])
|
||||
records = newRecords
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private func updateBookmarked(_ status: MastodonStatus, _ isBookmarked: Bool) {
|
||||
var newRecords = Array(records)
|
||||
guard let index = newRecords.firstIndex(where: { $0.id == status.id }) else {
|
||||
logger.warning("\(Self.entryNotFoundMessage)")
|
||||
return
|
||||
}
|
||||
newRecords[index] = status.inheritSensitivityToggled(from: newRecords[index])
|
||||
records = newRecords
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private func updateFavorited(_ status: MastodonStatus, _ isFavorited: Bool) {
|
||||
var newRecords = Array(records)
|
||||
if let index = newRecords.firstIndex(where: { $0.id == status.id }) {
|
||||
// Replace old status entity
|
||||
let existingRecord = newRecords[index]
|
||||
let newStatus = status.inheritSensitivityToggled(from: existingRecord)
|
||||
.withOriginal(status: existingRecord)
|
||||
newRecords[index] = newStatus
|
||||
} else if let index = newRecords.firstIndex(where: { $0.reblog?.id == status.id }) {
|
||||
// Replace reblogged entity of old "parent" status
|
||||
let existingRecord = newRecords[index]
|
||||
let newStatus = status.inheritSensitivityToggled(from: existingRecord)
|
||||
.withOriginal(status: existingRecord)
|
||||
newStatus.reblog = status
|
||||
newRecords[index] = newStatus
|
||||
} else {
|
||||
logger.warning("\(Self.entryNotFoundMessage)")
|
||||
}
|
||||
records = newRecords
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private func updateReblogged(_ status: MastodonStatus, _ isReblogged: Bool) {
|
||||
var newRecords = Array(records)
|
||||
|
||||
switch isReblogged {
|
||||
case true:
|
||||
let index: Int
|
||||
if let idx = newRecords.firstIndex(where: { $0.reblog?.id == status.reblog?.id }) {
|
||||
index = idx
|
||||
} else if let idx = newRecords.firstIndex(where: { $0.id == status.reblog?.id }) {
|
||||
index = idx
|
||||
} else {
|
||||
logger.warning("\(Self.entryNotFoundMessage)")
|
||||
return
|
||||
}
|
||||
let existingStatus = newRecords[index]
|
||||
newRecords[index] = status.withOriginal(status: existingStatus)
|
||||
case false:
|
||||
let index: Int
|
||||
if let idx = newRecords.firstIndex(where: { $0.reblog?.id == status.id }) {
|
||||
index = idx
|
||||
} else if let idx = newRecords.firstIndex(where: { $0.id == status.id }) {
|
||||
index = idx
|
||||
} else {
|
||||
logger.warning("\(Self.entryNotFoundMessage)")
|
||||
return
|
||||
}
|
||||
let existingRecord = newRecords[index]
|
||||
let newStatus = existingRecord.originalStatus ?? status.inheritSensitivityToggled(from: existingRecord)
|
||||
newRecords[index] = newStatus
|
||||
}
|
||||
|
||||
records = newRecords
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private func updateSensitive(_ status: MastodonStatus, _ isVisible: Bool) {
|
||||
var newRecords = Array(records)
|
||||
if let index = newRecords.firstIndex(where: { $0.reblog?.id == status.id }) {
|
||||
let newStatus: MastodonStatus = .fromEntity(newRecords[index].entity)
|
||||
newStatus.reblog = status
|
||||
newRecords[index] = newStatus
|
||||
} else if let index = newRecords.firstIndex(where: { $0.id == status.id }) {
|
||||
let newStatus: MastodonStatus = .fromEntity(newRecords[index].entity)
|
||||
.inheritSensitivityToggled(from: status)
|
||||
newRecords[index] = newStatus
|
||||
} else {
|
||||
logger.warning("\(Self.entryNotFoundMessage)")
|
||||
return
|
||||
}
|
||||
records = newRecords
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -95,6 +95,7 @@ public final class AuthenticationService: NSObject {
|
||||
super.init()
|
||||
|
||||
$mastodonAuthenticationBoxes
|
||||
.throttle(for: 3, scheduler: DispatchQueue.main, latest: true)
|
||||
.sink { [weak self] boxes in
|
||||
Task { [weak self] in
|
||||
for authBox in boxes {
|
||||
|
@ -142,7 +142,8 @@ extension Mastodon.Entity.Status: Hashable {
|
||||
lhs.favourited == rhs.favourited &&
|
||||
lhs.reblogged == rhs.reblogged &&
|
||||
lhs.bookmarked == rhs.bookmarked &&
|
||||
lhs.pinned == rhs.pinned
|
||||
lhs.pinned == rhs.pinned &&
|
||||
lhs.content == rhs.content
|
||||
}
|
||||
|
||||
public func hash(into hasher: inout Hasher) {
|
||||
@ -153,5 +154,6 @@ extension Mastodon.Entity.Status: Hashable {
|
||||
hasher.combine(reblogged)
|
||||
hasher.combine(bookmarked)
|
||||
hasher.combine(pinned)
|
||||
hasher.combine(content)
|
||||
}
|
||||
}
|
||||
|
@ -62,7 +62,8 @@ extension MastodonFeed: Hashable {
|
||||
lhs.id == rhs.id &&
|
||||
lhs.status?.entity == rhs.status?.entity &&
|
||||
lhs.status?.reblog?.entity == rhs.status?.reblog?.entity &&
|
||||
lhs.status?.isSensitiveToggled == rhs.status?.isSensitiveToggled
|
||||
lhs.status?.isSensitiveToggled == rhs.status?.isSensitiveToggled &&
|
||||
lhs.status?.reblog?.isSensitiveToggled == rhs.status?.reblog?.isSensitiveToggled
|
||||
}
|
||||
|
||||
public func hash(into hasher: inout Hasher) {
|
||||
@ -70,6 +71,7 @@ extension MastodonFeed: Hashable {
|
||||
hasher.combine(status?.entity)
|
||||
hasher.combine(status?.reblog?.entity)
|
||||
hasher.combine(status?.isSensitiveToggled)
|
||||
hasher.combine(status?.reblog?.isSensitiveToggled)
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -7,6 +7,10 @@ import CoreDataStack
|
||||
public final class MastodonStatus: ObservableObject {
|
||||
public typealias ID = Mastodon.Entity.Status.ID
|
||||
|
||||
/// `originalStatus` is used to restore a previously re-blogged state when a status
|
||||
/// has been originally reblogged by another account
|
||||
@Published public var originalStatus: MastodonStatus?
|
||||
|
||||
@Published public var entity: Mastodon.Entity.Status
|
||||
@Published public var reblog: MastodonStatus?
|
||||
|
||||
@ -32,19 +36,32 @@ extension MastodonStatus {
|
||||
public static func fromEntity(_ entity: Mastodon.Entity.Status) -> MastodonStatus {
|
||||
return MastodonStatus(entity: entity, isSensitiveToggled: false)
|
||||
}
|
||||
|
||||
public func inheritSensitivityToggled(from status: MastodonStatus?) -> MastodonStatus {
|
||||
self.isSensitiveToggled = status?.isSensitiveToggled ?? false
|
||||
self.reblog?.isSensitiveToggled = status?.reblog?.isSensitiveToggled ?? false
|
||||
return self
|
||||
}
|
||||
|
||||
public func withOriginal(status: MastodonStatus?) -> MastodonStatus {
|
||||
originalStatus = status
|
||||
return self
|
||||
}
|
||||
}
|
||||
|
||||
extension MastodonStatus: Hashable {
|
||||
public static func == (lhs: MastodonStatus, rhs: MastodonStatus) -> Bool {
|
||||
lhs.entity == rhs.entity &&
|
||||
lhs.reblog?.entity == rhs.reblog?.entity &&
|
||||
lhs.isSensitiveToggled == rhs.isSensitiveToggled
|
||||
lhs.isSensitiveToggled == rhs.isSensitiveToggled &&
|
||||
lhs.reblog?.isSensitiveToggled == rhs.reblog?.isSensitiveToggled
|
||||
}
|
||||
|
||||
public func hash(into hasher: inout Hasher) {
|
||||
hasher.combine(entity)
|
||||
hasher.combine(reblog?.entity)
|
||||
hasher.combine(isSensitiveToggled)
|
||||
hasher.combine(reblog?.isSensitiveToggled)
|
||||
}
|
||||
}
|
||||
|
||||
@ -59,6 +76,17 @@ public extension Mastodon.Entity.Status {
|
||||
}
|
||||
}
|
||||
|
||||
public extension MastodonStatus {
|
||||
enum UpdateIntent {
|
||||
case bookmark(Bool)
|
||||
case reblog(Bool)
|
||||
case favorite(Bool)
|
||||
case toggleSensitive(Bool)
|
||||
case delete
|
||||
case edit
|
||||
}
|
||||
}
|
||||
|
||||
public extension MastodonStatus {
|
||||
func getPoll(in context: NSManagedObjectContext, domain: String) async -> Poll? {
|
||||
guard
|
||||
|
@ -155,17 +155,22 @@ extension StatusView {
|
||||
viewModel.header = createHeader(name: "", emojis: [:])
|
||||
/// finally we can load the status information and display the correct header
|
||||
if let authenticationBox = viewModel.authContext?.mastodonAuthenticationBox {
|
||||
Task { @MainActor in
|
||||
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
|
||||
}
|
||||
}
|
||||
Mastodon.API.Statuses.status(
|
||||
session: .shared,
|
||||
domain: authenticationBox.domain,
|
||||
statusID: inReplyToID,
|
||||
authorization: authenticationBox.userAuthorization
|
||||
)
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink(receiveCompletion: { completion in
|
||||
// no-op
|
||||
}, receiveValue: { [weak self] response in
|
||||
guard let self else { return }
|
||||
let replyTo = response.value
|
||||
let header = createHeader(name: replyTo.account.displayNameWithFallback, emojis: replyTo.account.emojis?.asDictionary ?? [:])
|
||||
self.viewModel.header = header
|
||||
})
|
||||
.store(in: &disposeBag)
|
||||
}
|
||||
} else {
|
||||
// B. replyTo status not exist
|
||||
@ -219,6 +224,8 @@ extension StatusView {
|
||||
}
|
||||
}()
|
||||
|
||||
viewModel.authorId = author.id
|
||||
|
||||
// author username
|
||||
viewModel.authorUsername = author.acct
|
||||
|
||||
@ -232,27 +239,13 @@ extension StatusView {
|
||||
}()
|
||||
|
||||
// isMuting, isBlocking, Following
|
||||
guard let auth = viewModel.authContext?.mastodonAuthenticationBox else { return }
|
||||
guard viewModel.authContext?.mastodonAuthenticationBox != nil else { return }
|
||||
guard !viewModel.isMyself else {
|
||||
viewModel.isMuting = false
|
||||
viewModel.isBlocking = false
|
||||
viewModel.isFollowed = false
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -46,6 +46,7 @@ extension StatusView {
|
||||
@Published public var authorAvatarImage: UIImage?
|
||||
@Published public var authorAvatarImageURL: URL?
|
||||
@Published public var authorName: MetaContent?
|
||||
@Published public var authorId: String?
|
||||
@Published public var authorUsername: String?
|
||||
|
||||
@Published public var locked = false
|
||||
@ -277,21 +278,20 @@ extension StatusView.ViewModel {
|
||||
// timestamp
|
||||
Publishers.CombineLatest3(
|
||||
$timestamp,
|
||||
$editedAt,
|
||||
$editedAt.removeDuplicates(),
|
||||
timestampUpdatePublisher.prepend(Date()).eraseToAnyPublisher()
|
||||
)
|
||||
.compactMap { [weak self] timestamp, editedAt, _ -> String? in
|
||||
guard let self = self else { return nil }
|
||||
.sink(receiveValue: { [weak self] timestamp, editedAt, _ in
|
||||
guard let self = self else { return }
|
||||
if let timestamp = editedAt, let text = self.timestampFormatter?(timestamp, true) {
|
||||
return text
|
||||
self.editedAt = editedAt
|
||||
timestampText = text
|
||||
} else if let timestamp = timestamp, let text = self.timestampFormatter?(timestamp, false) {
|
||||
return text
|
||||
timestampText = text
|
||||
}
|
||||
return ""
|
||||
}
|
||||
.removeDuplicates()
|
||||
.assign(to: &$timestampText)
|
||||
|
||||
})
|
||||
.store(in: &disposeBag)
|
||||
|
||||
$timestampText
|
||||
.sink { [weak self] text in
|
||||
guard let _ = self else { return }
|
||||
@ -655,16 +655,12 @@ extension StatusView.ViewModel {
|
||||
|
||||
private func bindMenu(statusView: StatusView) {
|
||||
let authorView = statusView.authorView
|
||||
let publisherOne = Publishers.CombineLatest(
|
||||
let publisherOne = Publishers.CombineLatest3(
|
||||
$authorName,
|
||||
$authorId,
|
||||
$isMyself
|
||||
)
|
||||
let publishersTwo = Publishers.CombineLatest4(
|
||||
$isMuting,
|
||||
$isBlocking,
|
||||
$isBookmark,
|
||||
$isFollowed
|
||||
)
|
||||
|
||||
let publishersThree = Publishers.CombineLatest(
|
||||
$translation,
|
||||
$language
|
||||
@ -672,15 +668,14 @@ extension StatusView.ViewModel {
|
||||
|
||||
Publishers.CombineLatest3(
|
||||
publisherOne.eraseToAnyPublisher(),
|
||||
publishersTwo.eraseToAnyPublisher(),
|
||||
$isBookmark,
|
||||
publishersThree.eraseToAnyPublisher()
|
||||
).eraseToAnyPublisher()
|
||||
.sink { tupleOne, tupleTwo, tupleThree in
|
||||
let (authorName, isMyself) = tupleOne
|
||||
let (isMuting, isBlocking, isBookmark, isFollowed) = tupleTwo
|
||||
.sink { tupleOne, isBookmark, tupleThree in
|
||||
let (authorName, authorId, isMyself) = tupleOne
|
||||
let (translatedFromLanguage, language) = tupleThree
|
||||
|
||||
guard let name = authorName?.string, let context = self.context, let authContext = self.authContext else {
|
||||
guard let name = authorName?.string, let authorId = authorId, let context = self.context, let authContext = self.authContext else {
|
||||
statusView.authorView.menuButton.menu = nil
|
||||
return
|
||||
}
|
||||
@ -689,21 +684,45 @@ extension StatusView.ViewModel {
|
||||
let instance = authentication.instance(in: context.managedObjectContext)
|
||||
let isTranslationEnabled = instance?.isTranslationEnabled ?? false
|
||||
|
||||
let menuContext = StatusAuthorView.AuthorMenuContext(
|
||||
name: name,
|
||||
isMuting: isMuting,
|
||||
isBlocking: isBlocking,
|
||||
isMyself: isMyself,
|
||||
isBookmarking: isBookmark,
|
||||
isFollowed: isFollowed,
|
||||
isTranslationEnabled: isTranslationEnabled,
|
||||
isTranslated: translatedFromLanguage != nil,
|
||||
statusLanguage: language
|
||||
)
|
||||
|
||||
let (menu, actions) = authorView.setupAuthorMenu(menuContext: menuContext)
|
||||
authorView.menuButton.menu = menu
|
||||
authorView.authorActions = actions
|
||||
authorView.menuButton.menu = UIMenu(children: [
|
||||
UIDeferredMenuElement({ menuElement in
|
||||
|
||||
let domain = authContext.mastodonAuthenticationBox.domain
|
||||
|
||||
Task { @MainActor in
|
||||
if let relationship = try? await Mastodon.API.Account.relationships(
|
||||
session: .shared,
|
||||
domain: domain,
|
||||
query: .init(ids: [authorId]),
|
||||
authorization: authContext.mastodonAuthenticationBox.userAuthorization
|
||||
).singleOutput().value {
|
||||
guard let rel = relationship.first else { return }
|
||||
DispatchQueue.main.async {
|
||||
|
||||
let menuContext = StatusAuthorView.AuthorMenuContext(
|
||||
name: name,
|
||||
isMuting: rel.muting ?? false,
|
||||
isBlocking: rel.blocking,
|
||||
isMyself: isMyself,
|
||||
isBookmarking: isBookmark,
|
||||
isFollowed: rel.followedBy,
|
||||
isTranslationEnabled: isTranslationEnabled,
|
||||
isTranslated: translatedFromLanguage != nil,
|
||||
statusLanguage: language
|
||||
)
|
||||
|
||||
let (menu, actions) = authorView.setupAuthorMenu(menuContext: menuContext)
|
||||
authorView.authorActions = actions
|
||||
|
||||
menuElement(menu.children)
|
||||
}
|
||||
} else {
|
||||
menuElement(MastodonMenu.setupMenu(actions: [[.shareStatus]], delegate: statusView).children)
|
||||
}
|
||||
}
|
||||
})
|
||||
])
|
||||
|
||||
authorView.menuButton.showsMenuAsPrimaryAction = true
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
|
@ -327,6 +327,9 @@ public final class StatusView: UIView {
|
||||
setPollDisplay(isDisplay: false)
|
||||
setFilterHintLabelDisplay(isDisplay: false)
|
||||
setStatusCardControlDisplay(isDisplay: false)
|
||||
|
||||
headerInfoLabel.text = nil
|
||||
headerIconImageView.image = nil
|
||||
}
|
||||
|
||||
public override init(frame: CGRect) {
|
||||
|
Loading…
x
Reference in New Issue
Block a user