2
2
mirror of https://github.com/mastodon/mastodon-ios synced 2025-04-11 22:58:02 +02:00

Refetch status on reblog or favorite

This is a temporary fix for favorites and boosts of boosted posts not displaying properly.  A datamodel change in the future should make all of this logic less confusing.

Fixes IOS-382
This commit is contained in:
whattherestimefor 2025-04-11 13:54:34 -04:00
parent 583c6a8874
commit 02a2294330
26 changed files with 171 additions and 128 deletions

View File

@ -28,6 +28,6 @@ extension DataSourceFacade {
newStatus.showDespiteContentWarning = status.showDespiteContentWarning
newStatus.showDespiteFilter = status.showDespiteFilter
provider.update(status: newStatus, intent: .bookmark(updatedStatus.bookmarked == true))
provider.update(contentStatus: newStatus, intent: .bookmark(updatedStatus.bookmarked == true))
}
}

View File

@ -14,19 +14,23 @@ extension DataSourceFacade {
@MainActor
public static func responseToStatusFavoriteAction(
provider: DataSourceProvider & AuthContextProvider,
status: MastodonStatus
wrappingStatus: MastodonStatus,
contentStatus: MastodonStatus
) async throws {
FeedbackGenerator.shared.generate(.selectionChanged)
let updatedStatus = try await APIService.shared.favorite(
status: status,
status: contentStatus,
authenticationBox: provider.authenticationBox
).value
let newStatus: MastodonStatus = .fromEntity(updatedStatus)
newStatus.showDespiteContentWarning = status.showDespiteContentWarning
newStatus.showDespiteFilter = status.showDespiteFilter
let showDespiteContentWarning = wrappingStatus.showDespiteContentWarning
let showDespiteFilter = wrappingStatus.showDespiteFilter
provider.update(status: newStatus, intent: .favorite(updatedStatus.favourited == true))
let newStatus: MastodonStatus = .fromEntity(updatedStatus)
newStatus.showDespiteContentWarning = showDespiteContentWarning
newStatus.showDespiteFilter = showDespiteFilter
provider.update(contentStatus: newStatus, intent: .favorite(updatedStatus.favourited == true))
}
}

View File

@ -15,28 +15,29 @@ extension DataSourceFacade {
@MainActor
static func responseToStatusReblogAction(
provider: DataSourceProvider & AuthContextProvider,
status: MastodonStatus
wrappingStatus: MastodonStatus,
contentStatus: MastodonStatus
) async throws {
if UserDefaults.shared.askBeforeBoostingAPost {
let alertController = UIAlertController(
title: status.entity.reblogged == true ? L10n.Common.Alerts.BoostAPost.titleUnboost : L10n.Common.Alerts.BoostAPost.titleBoost,
title: contentStatus.entity.reblogged == true ? L10n.Common.Alerts.BoostAPost.titleUnboost : L10n.Common.Alerts.BoostAPost.titleBoost,
message: nil,
preferredStyle: .alert
)
let cancelAction = UIAlertAction(title: L10n.Common.Alerts.BoostAPost.cancel, style: .default)
alertController.addAction(cancelAction)
let confirmAction = UIAlertAction(
title: status.entity.reblogged == true ? L10n.Common.Alerts.BoostAPost.unboost : L10n.Common.Alerts.BoostAPost.boost,
title: contentStatus.entity.reblogged == true ? L10n.Common.Alerts.BoostAPost.unboost : L10n.Common.Alerts.BoostAPost.boost,
style: .default
) { _ in
Task { @MainActor in
try? await performReblog(provider: provider, status: status)
try? await performReblog(provider: provider, status: contentStatus)
}
}
alertController.addAction(confirmAction)
provider.present(alertController, animated: true)
} else {
try await performReblog(provider: provider, status: status)
try await performReblog(provider: provider, status: contentStatus)
}
}
}
@ -49,17 +50,17 @@ private extension DataSourceFacade {
) async throws {
FeedbackGenerator.shared.generate(.selectionChanged)
let updatedStatus = try await APIService.shared.reblog(
let updatedContentStatus = try await APIService.shared.reblog(
status: status,
authenticationBox: provider.authenticationBox
).value
let newStatus: MastodonStatus = .fromEntity(updatedStatus)
let newStatus: MastodonStatus = .fromEntity(updatedContentStatus)
newStatus.reblog?.showDespiteContentWarning = status.showDespiteContentWarning
newStatus.reblog?.showDespiteFilter = status.showDespiteFilter
newStatus.showDespiteContentWarning = status.showDespiteContentWarning
newStatus.showDespiteFilter = status.showDespiteFilter
provider.update(status: newStatus, intent: .reblog(updatedStatus.reblogged == true))
provider.update(contentStatus: newStatus, intent: .reblog(updatedContentStatus.reblogged == true))
}
}

View File

@ -28,7 +28,7 @@ extension DataSourceFacade {
authenticationBox: dependency.authenticationBox
).value.asMastodonStatus
dependency.update(status: deletedStatus, intent: .delete)
dependency.update(contentStatus: deletedStatus, intent: .delete)
}
}
@ -116,12 +116,14 @@ extension DataSourceFacade {
case .reblog:
try await DataSourceFacade.responseToStatusReblogAction(
provider: provider,
status: _status
wrappingStatus: status,
contentStatus: _status
)
case .like:
try await DataSourceFacade.responseToStatusFavoriteAction(
provider: provider,
status: _status
wrappingStatus: status,
contentStatus: _status
)
case .share:
try await DataSourceFacade.responseToStatusShareAction(
@ -391,19 +393,19 @@ extension DataSourceFacade {
alertController.addAction(cancelAction)
dependency.present(alertController, animated: true)
case .boostStatus(_):
guard let status: MastodonStatus = menuContext.statusViewModel?._originalStatus?.reblog ?? menuContext.statusViewModel?._originalStatus else {
guard let wrappingStatus = menuContext.statusViewModel?._originalStatus else {
assertionFailure()
return
}
try await responseToStatusReblogAction(provider: dependency, status: status)
let contentStatus = menuContext.statusViewModel?._originalStatus?.reblog ?? wrappingStatus
try await responseToStatusReblogAction(provider: dependency, wrappingStatus: wrappingStatus, contentStatus: contentStatus)
case .favoriteStatus(_):
guard let status: MastodonStatus = menuContext.statusViewModel?._originalStatus?.reblog ?? menuContext.statusViewModel?._originalStatus else {
guard let wrappingStatus: MastodonStatus = menuContext.statusViewModel?._originalStatus else {
assertionFailure()
return
}
try await responseToStatusFavoriteAction(provider: dependency, status: status)
let contentStatus = menuContext.statusViewModel?._originalStatus?.reblog ?? wrappingStatus
try await responseToStatusFavoriteAction(provider: dependency, wrappingStatus: wrappingStatus, contentStatus: contentStatus)
case .copyStatusLink:
guard let status: MastodonStatus = menuContext.statusViewModel?._originalStatus?.reblog ?? menuContext.statusViewModel?._originalStatus else {
assertionFailure()

View File

@ -585,7 +585,7 @@ extension NotificationTableViewCellDelegate where Self: DataSourceProvider & Aut
let newStatus: MastodonStatus = .fromEntity(entity)
newStatus.poll = MastodonPoll(poll: newPoll, status: newStatus)
self.update(status: newStatus, intent: .pollVote)
self.update(contentStatus: newStatus, intent: .pollVote)
} catch {
notificationView.statusView.viewModel.isVoting = false
}

View File

@ -320,7 +320,7 @@ extension StatusTableViewCellDelegate where Self: DataSourceProvider & AuthConte
let newStatus: MastodonStatus = .fromEntity(entity)
newStatus.poll = MastodonPoll(poll: newPoll, status: newStatus)
self.update(status: newStatus, intent: .pollVote)
self.update(contentStatus: newStatus, intent: .pollVote)
} catch {
let alert = UIAlertController(title: "Poll Error", message: "Something went wrong while processing your response: \(error)", preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "OK", style: .cancel))

View File

@ -139,11 +139,12 @@ extension StatusTableViewControllerNavigateableCore where Self: DataSourceProvid
@MainActor
private func toggleReblog() async {
guard let status = await statusRecord() else { return }
let contentStatus = status.reblog ?? status
do {
try await DataSourceFacade.responseToStatusReblogAction(
provider: self,
status: status
wrappingStatus: status,
contentStatus: contentStatus
)
} catch {
assertionFailure()
@ -153,11 +154,12 @@ extension StatusTableViewControllerNavigateableCore where Self: DataSourceProvid
@MainActor
private func toggleFavorite() async {
guard let status = await statusRecord() else { return }
let contentStatus = status.reblog ?? status
do {
try await DataSourceFacade.responseToStatusFavoriteAction(
provider: self,
status: status
wrappingStatus: status,
contentStatus: contentStatus
)
} catch {
assertionFailure()

View File

@ -38,7 +38,7 @@ extension DataSourceItem {
protocol DataSourceProvider: UIViewController {
func item(from source: DataSourceItem.Source) async -> DataSourceItem?
func update(status: MastodonStatus, intent: MastodonStatus.UpdateIntent)
func update(contentStatus: MastodonStatus, intent: MastodonStatus.UpdateIntent)
var filterContext: Mastodon.Entity.FilterContext? { get }
func didToggleContentWarningDisplayStatus(status: MastodonStatus)

View File

@ -36,8 +36,8 @@ extension DiscoveryPostsViewController: DataSourceProvider {
}
}
func update(status: MastodonStatus, intent: MastodonStatus.UpdateIntent) {
viewModel.dataController.update(status: status, intent: intent)
func update(contentStatus: MastodonStatus, intent: MastodonStatus.UpdateIntent) {
viewModel.dataController.update(status: contentStatus, intent: intent)
}
@MainActor

View File

@ -36,8 +36,8 @@ extension HashtagTimelineViewController: DataSourceProvider {
}
}
func update(status: MastodonStatus, intent: MastodonStatus.UpdateIntent) {
viewModel.dataController.update(status: status, intent: intent)
func update(contentStatus: MastodonStatus, intent: MastodonStatus.UpdateIntent) {
viewModel.dataController.update(status: contentStatus, intent: intent)
}
@MainActor

View File

@ -39,8 +39,8 @@ extension HomeTimelineViewController: DataSourceProvider {
}
}
func update(status: MastodonStatus, intent: MastodonStatus.UpdateIntent) {
viewModel?.dataController.update(status: status, intent: intent)
func update(contentStatus: MastodonStatus, intent: MastodonStatus.UpdateIntent) {
viewModel?.dataController.update(status: contentStatus, intent: intent)
}
private func indexPath(for cell: UITableViewCell) -> IndexPath? {

View File

@ -67,9 +67,9 @@ extension NotificationTimelineViewController: DataSourceProvider {
}
}
func update(status: MastodonStatus, intent: MastodonStatus.UpdateIntent) {
MastodonFeedItemCacheManager.shared.addToCache(status.entity)
if let reblog = status.entity.reblog {
func update(contentStatus: MastodonStatus, intent: MastodonStatus.UpdateIntent) {
MastodonFeedItemCacheManager.shared.addToCache(contentStatus.entity)
if let reblog = contentStatus.entity.reblog {
MastodonFeedItemCacheManager.shared.addToCache(reblog)
}
viewModel.reloadData()

View File

@ -36,8 +36,8 @@ extension BookmarkViewController: DataSourceProvider {
}
}
func update(status: MastodonStatus, intent: MastodonStatus.UpdateIntent) {
viewModel.dataController.update(status: status, intent: intent)
func update(contentStatus: MastodonStatus, intent: MastodonStatus.UpdateIntent) {
viewModel.dataController.update(status: contentStatus, intent: intent)
}
@MainActor

View File

@ -106,7 +106,7 @@ extension FamiliarFollowersViewController: DataSourceProvider {
}
}
func update(status: MastodonStatus, intent: MastodonStatus.UpdateIntent) {
func update(contentStatus: MastodonStatus, intent: MastodonStatus.UpdateIntent) {
assertionFailure("Not required")
}

View File

@ -36,8 +36,8 @@ extension FavoriteViewController: DataSourceProvider {
}
}
func update(status: MastodonStatus, intent: MastodonStatus.UpdateIntent) {
viewModel.dataController.update(status: status, intent: intent)
func update(contentStatus: MastodonStatus, intent: MastodonStatus.UpdateIntent) {
viewModel.dataController.update(status: contentStatus, intent: intent)
}
@MainActor

View File

@ -148,7 +148,7 @@ extension FollowerListViewController: DataSourceProvider {
}
}
func update(status: MastodonStatus, intent: MastodonStatus.UpdateIntent) {
func update(contentStatus: MastodonStatus, intent: MastodonStatus.UpdateIntent) {
assertionFailure("Not required")
}

View File

@ -153,7 +153,7 @@ extension FollowingListViewController: DataSourceProvider {
}
}
func update(status: MastodonStatus, intent: MastodonStatus.UpdateIntent) {
func update(contentStatus: MastodonStatus, intent: MastodonStatus.UpdateIntent) {
assertionFailure("Not required")
}

View File

@ -1030,15 +1030,15 @@ extension ProfileViewController: DataSourceProvider {
profilePagingViewController?.reloadTables()
}
func update(status: MastodonStatus, intent: MastodonStatus.UpdateIntent) {
updateViewModelsWithDataControllers(status: status, intent: intent)
func update(contentStatus: MastodonStatus, intent: MastodonStatus.UpdateIntent) {
updateViewModelsWithDataControllers(status: contentStatus, intent: intent)
}
func updateViewModelsWithDataControllers(status: MastodonStatus, intent: MastodonStatus.UpdateIntent) {
profilePagingViewController?.viewModel?.postUserTimelineViewController.update(status: status, intent: intent)
profilePagingViewController?.viewModel?.repliesUserTimelineViewController.update(status: status, intent: intent)
profilePagingViewController?.viewModel?.mediaUserTimelineViewController.update(status: status, intent: intent)
profilePagingViewController?.viewModel?.postUserTimelineViewController.update(contentStatus: status, intent: intent)
profilePagingViewController?.viewModel?.repliesUserTimelineViewController.update(contentStatus: status, intent: intent)
profilePagingViewController?.viewModel?.mediaUserTimelineViewController.update(contentStatus: status, intent: intent)
}
}

View File

@ -36,8 +36,8 @@ extension UserTimelineViewController: DataSourceProvider {
}
}
func update(status: MastodonStatus, intent: MastodonStatus.UpdateIntent) {
viewModel.dataController.update(status: status, intent: intent)
func update(contentStatus: MastodonStatus, intent: MastodonStatus.UpdateIntent) {
viewModel.dataController.update(status: contentStatus, intent: intent)
}
@MainActor

View File

@ -36,7 +36,7 @@ extension FavoritedByViewController: DataSourceProvider {
}
}
func update(status: MastodonStatus, intent: MastodonStatus.UpdateIntent) {
func update(contentStatus: MastodonStatus, intent: MastodonStatus.UpdateIntent) {
assertionFailure("Not required")
}

View File

@ -37,7 +37,7 @@ extension RebloggedByViewController: DataSourceProvider {
}
}
func update(status: MastodonStatus, intent: MastodonStatus.UpdateIntent) {
func update(contentStatus: MastodonStatus, intent: MastodonStatus.UpdateIntent) {
assertionFailure("Not required")
}

View File

@ -37,7 +37,7 @@ extension SearchHistoryViewController: DataSourceProvider {
}
}
func update(status: MastodonStatus, intent: MastodonStatus.UpdateIntent) {
func update(contentStatus: MastodonStatus, intent: MastodonStatus.UpdateIntent) {
assertionFailure("Not required")
}

View File

@ -41,8 +41,8 @@ extension SearchResultViewController: DataSourceProvider {
}
}
func update(status: MastodonStatus, intent: MastodonStatus.UpdateIntent) {
viewModel.dataController.update(status: status, intent: intent)
func update(contentStatus: MastodonStatus, intent: MastodonStatus.UpdateIntent) {
viewModel.dataController.update(status: contentStatus, intent: intent)
}
@MainActor

View File

@ -37,7 +37,7 @@ extension ThreadViewController: DataSourceProvider {
}
}
func update(status _status: MastodonStatus, intent: MastodonStatus.UpdateIntent) {
func update(contentStatus _status: MastodonStatus, intent: MastodonStatus.UpdateIntent) {
let status = _status.reblog ?? _status
if case MastodonStatus.UpdateIntent.delete = intent {
return handleDelete(status)

View File

@ -197,13 +197,13 @@ extension ThreadViewController: StatusTableViewControllerNavigateable {
extension UINavigationController {
func notifyChildrenAboutStatusDeletion(_ status: MastodonStatus) {
viewControllers.compactMap { $0 as? DataSourceProvider }.forEach { provider in
provider?.update(status: status, intent: .delete)
provider?.update(contentStatus: status, intent: .delete)
}
}
func notifyChildrenAboutStatusEdit(_ status: MastodonStatus) {
viewControllers.compactMap { $0 as? DataSourceProvider }.forEach { provider in
provider?.update(status: status, intent: .edit)
provider?.update(contentStatus: status, intent: .edit)
}
}
}

View File

@ -119,93 +119,127 @@ final public class FeedDataController {
@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 relevant = recordsContaining(statusID: status.id)
Task {
let refetched = await refetchStatuses(relevant)
for record in refetched {
if let idx = newRecords.firstIndex(where: { $0.id == record.id }) {
let existingRecord = newRecords[idx]
newRecords[idx] = .fromStatus(MastodonStatus(entity: record, showDespiteContentWarning: existingRecord.status?.showDespiteContentWarning ?? false), kind: existingRecord.kind)
} else {
logger.warning("\(Self.entryNotFoundMessage)")
return
}
}
records = newRecords
}
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
let relevant = recordsContaining(statusID: status.id)
Task {
let refetched = await refetchStatuses(relevant)
for record in refetched {
if let idx = newRecords.firstIndex(where: { $0.id == record.id }) {
let existingRecord = newRecords[idx]
newRecords[idx] = .fromStatus(MastodonStatus(entity: record, showDespiteContentWarning: existingRecord.status?.showDespiteContentWarning ?? false), kind: existingRecord.kind)
} else {
logger.warning("\(Self.entryNotFoundMessage)")
return
}
}
newRecords[index] = .fromStatus(newStatus, kind: newRecords[index].kind)
} 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.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 relevantID = isReblogged ? (status.reblog?.id ?? status.id) : status.id
let relevant = recordsContaining(statusID: relevantID)
Task {
let refetched = await refetchStatuses(relevant)
// print("found \(refetched.count) relevant statuses for \(status.id)")
for record in refetched {
if let idx = newRecords.firstIndex(where: { $0.id == record.id }) {
let existingRecord = newRecords[idx]
// print("replacing record for \(existingRecord.status?.id) (reblog of \(existingRecord.status?.reblog))...")
if existingRecord.status?.entity.reblogged == true || existingRecord.status?.reblog?.entity.reblogged == true {
// print("- was reblogged by me")
} else {
// print("- NOT reblogged by me")
}
let newRecord = MastodonFeed.fromStatus(MastodonStatus(entity: record, showDespiteContentWarning: existingRecord.status?.showDespiteContentWarning ?? false), kind: existingRecord.kind)
newRecords[idx] = newRecord
// print("replaced with \(newRecord.status?.id) (reblog of \(newRecord.status?.reblog?.id))")
if newRecord.status?.entity.reblogged == true || newRecord.status?.reblog?.entity.reblogged == true {
// print("- was reblogged by me")
} else {
// print("- NOT reblogged by me")
}
} 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
records = newRecords
}
}
@MainActor
private func updateSensitive(_ status: MastodonStatus, _ isVisible: Bool) {
let toUpdate = recordsContaining(statusID: status.id)
var newRecords = Array(records)
for record in toUpdate {
if let index = newRecords.firstIndex(where: { $0.status?.id == record.id }), let existingEntity = newRecords[index].status?.entity {
if existingEntity.id == status.id {
let existingRecord = newRecords[index]
let newStatus: MastodonStatus = .fromEntity(existingEntity)
newStatus.reblog = status
newRecords[index] = .fromStatus(newStatus, kind: existingRecord.kind)
} else if existingEntity.reblog?.id == status.id {
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
}
}
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
private func recordsContaining(statusID: Mastodon.Entity.Status.ID) -> [MastodonFeed] {
records.filter { feed in
return feed.status?.id == statusID || feed.status?.reblog?.id == statusID
}
}
@MainActor
private func refetchStatuses(_ items: [MastodonFeed]) async -> [Mastodon.Entity.Status] {
switch kind {
case .notificationAll, .notificationMentions, .notificationAccount:
return []
default:
var refetched = [Mastodon.Entity.Status]()
for item in items {
if let refetchedItem = try? await APIService.shared.status(statusID: item.id, authenticationBox: authenticationBox) {
refetched.append(refetchedItem.value)
} else {
}
}
return refetched
}
records = newRecords
}
}