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