diff --git a/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist b/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist
index 2fb2d9806..73b68ec90 100644
--- a/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist
+++ b/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist
@@ -12,7 +12,7 @@
CoreDataStack.xcscheme_^#shared#^_
orderHint
- 17
+ 16
Mastodon - RTL.xcscheme_^#shared#^_
@@ -32,7 +32,7 @@
NotificationService.xcscheme_^#shared#^_
orderHint
- 16
+ 17
SuppressBuildableAutocreation
diff --git a/Mastodon/Diffiable/Section/StatusSection.swift b/Mastodon/Diffiable/Section/StatusSection.swift
index c5d0eb19b..8a3df09b1 100644
--- a/Mastodon/Diffiable/Section/StatusSection.swift
+++ b/Mastodon/Diffiable/Section/StatusSection.swift
@@ -47,14 +47,18 @@ extension StatusSection {
// configure cell
managedObjectContext.performAndWait {
- let timelineIndex = managedObjectContext.object(with: objectID) as! HomeTimelineIndex
+ let timelineIndex = managedObjectContext.object(with: objectID) as? HomeTimelineIndex
+ // note: force check optional for status
+ // status maybe here when delete in thread scene
+ guard let status = timelineIndex?.status,
+ let userID = timelineIndex?.userID else { return }
StatusSection.configure(
cell: cell,
dependency: dependency,
readableLayoutFrame: tableView.readableContentGuide.layoutFrame,
timestampUpdatePublisher: timestampUpdatePublisher,
- status: timelineIndex.status,
- requestUserID: timelineIndex.userID,
+ status: status,
+ requestUserID: userID,
statusItemAttribute: attribute
)
}
@@ -752,12 +756,13 @@ extension StatusSection {
return L10n.Common.Controls.Timeline.Accessibility.countReblogs(status.favouritesCount.intValue)
}()
Publishers.CombineLatest(
- dependency.context.blockDomainService.blockedDomains,
+ dependency.context.blockDomainService.blockedDomains.setFailureType(to: ManagedObjectObserver.Error.self),
ManagedObjectObserver.observe(object: status.authorForUserProvider)
- .assertNoFailure()
- )
+ )
.receive(on: RunLoop.main)
- .sink { [weak dependency, weak cell] _, change in
+ .sink(receiveCompletion: { _ in
+ // do nothing
+ }, receiveValue: { [weak dependency, weak cell] _, change in
guard let cell = cell else { return }
guard let dependency = dependency else { return }
switch change.changeType {
@@ -769,7 +774,7 @@ extension StatusSection {
break
}
StatusSection.setupStatusMoreButtonMenu(cell: cell, dependency: dependency, status: status)
- }
+ })
.store(in: &cell.disposeBag)
self.setupStatusMoreButtonMenu(cell: cell, dependency: dependency, status: status)
}
diff --git a/Mastodon/Scene/Thread/ThreadViewModel+Diffable.swift b/Mastodon/Scene/Thread/ThreadViewModel+Diffable.swift
index 323a7a545..58e618f89 100644
--- a/Mastodon/Scene/Thread/ThreadViewModel+Diffable.swift
+++ b/Mastodon/Scene/Thread/ThreadViewModel+Diffable.swift
@@ -8,6 +8,8 @@
import UIKit
import Combine
import CoreData
+import CoreDataStack
+import MastodonSDK
extension ThreadViewModel {
@@ -41,13 +43,29 @@ extension ThreadViewModel {
diffableDataSource?.apply(snapshot, animatingDifferences: false, completion: nil)
Publishers.CombineLatest3(
+ rootItem.removeDuplicates(),
+ ancestorItems.removeDuplicates(),
+ descendantItems.removeDuplicates()
+ )
+ .receive(on: RunLoop.main)
+ .sink { [weak self] rootItem, ancestorItems, descendantItems in
+ guard let self = self else { return }
+ var items: [Item] = []
+ rootItem.flatMap { items.append($0) }
+ items.append(contentsOf: ancestorItems)
+ items.append(contentsOf: descendantItems)
+ self.updateDeletedStatus(for: items)
+ }
+ .store(in: &disposeBag)
+
+ Publishers.CombineLatest4(
rootItem,
ancestorItems,
- descendantItems
+ descendantItems,
+ existStatusFetchedResultsController.objectIDs
)
- .debounce(for: .milliseconds(100), scheduler: DispatchQueue.main) // some magic to avoid jitter
- .receive(on: DispatchQueue.main)
- .sink { [weak self] rootItem, ancestorItems, descendantItems in
+ .debounce(for: .milliseconds(100), scheduler: RunLoop.main) // some magic to avoid jitter
+ .sink { [weak self] rootItem, ancestorItems, descendantItems, existObjectIDs in
guard let self = self else { return }
guard let tableView = self.tableView,
let navigationBar = self.contentOffsetAdjustableTimelineViewControllerDelegate?.navigationBar()
@@ -65,31 +83,42 @@ extension ThreadViewModel {
if self.rootNode.value?.replyToID != nil, !(currentState is LoadThreadState.NoMore) {
newSnapshot.appendItems([.topLoader], toSection: .main)
}
+
+ let ancestorItems = ancestorItems.filter { item in
+ guard case let .reply(statusObjectID, _) = item else { return false }
+ return existObjectIDs.contains(statusObjectID)
+ }
newSnapshot.appendItems(ancestorItems, toSection: .main)
// root
- if let rootItem = rootItem {
- switch rootItem {
- case .root:
- newSnapshot.appendItems([rootItem], toSection: .main)
- default:
- break
- }
+ if let rootItem = rootItem,
+ case let .root(objectID, _) = rootItem,
+ existObjectIDs.contains(objectID) {
+ newSnapshot.appendItems([rootItem], toSection: .main)
}
// leaf
if !(currentState is LoadThreadState.NoMore) {
newSnapshot.appendItems([.bottomLoader], toSection: .main)
}
+
+ let descendantItems = descendantItems.filter { item in
+ switch item {
+ case .leaf(let statusObjectID, _):
+ return existObjectIDs.contains(statusObjectID)
+ default:
+ return true
+ }
+ }
newSnapshot.appendItems(descendantItems, toSection: .main)
- // difference for first visiable item exclude .topLoader
+ // difference for first visible item exclude .topLoader
guard let difference = self.calculateReloadSnapshotDifference(navigationBar: navigationBar, tableView: tableView, oldSnapshot: oldSnapshot, newSnapshot: newSnapshot) else {
diffableDataSource.apply(newSnapshot)
return
}
- // addtional margin for .topLoader
+ // additional margin for .topLoader
let oldTopMargin: CGFloat = {
let marginHeight = TimelineTopLoaderTableViewCell.cellHeight
if oldSnapshot.itemIdentifiers.contains(.topLoader) {
@@ -184,3 +213,33 @@ extension ThreadViewModel {
)
}
}
+
+extension ThreadViewModel {
+ private func updateDeletedStatus(for items: [Item]) {
+ let parentManagedObjectContext = context.managedObjectContext
+ let managedObjectContext = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType)
+ managedObjectContext.parent = parentManagedObjectContext
+ managedObjectContext.perform {
+ var statusIDs: [Status.ID] = []
+ for item in items {
+ switch item {
+ case .root(let objectID, _):
+ guard let status = managedObjectContext.object(with: objectID) as? Status else { continue }
+ statusIDs.append(status.id)
+ case .reply(let objectID, _):
+ guard let status = managedObjectContext.object(with: objectID) as? Status else { continue }
+ statusIDs.append(status.id)
+ case .leaf(let objectID, _):
+ guard let status = managedObjectContext.object(with: objectID) as? Status else { continue }
+ statusIDs.append(status.id)
+ default:
+ continue
+ }
+ }
+ DispatchQueue.main.async { [weak self] in
+ guard let self = self else { return }
+ self.existStatusFetchedResultsController.statusIDs.value = statusIDs
+ }
+ }
+ }
+}
diff --git a/Mastodon/Scene/Thread/ThreadViewModel.swift b/Mastodon/Scene/Thread/ThreadViewModel.swift
index 50df678c6..febc34d17 100644
--- a/Mastodon/Scene/Thread/ThreadViewModel.swift
+++ b/Mastodon/Scene/Thread/ThreadViewModel.swift
@@ -16,12 +16,14 @@ import MastodonSDK
class ThreadViewModel {
var disposeBag = Set()
+ var rootItemObserver: AnyCancellable?
// input
let context: AppContext
let rootNode: CurrentValueSubject
let rootItem: CurrentValueSubject-
let cellFrameCache = NSCache()
+ let existStatusFetchedResultsController: StatusFetchedResultsController
weak var contentOffsetAdjustableTimelineViewControllerDelegate: ContentOffsetAdjustableTimelineViewControllerDelegate?
weak var tableView: UITableView?
@@ -49,10 +51,20 @@ class ThreadViewModel {
self.context = context
self.rootNode = CurrentValueSubject(optionalStatus.flatMap { RootNode(domain: $0.domain, statusID: $0.id, replyToID: $0.inReplyToID) })
self.rootItem = CurrentValueSubject(optionalStatus.flatMap { Item.root(statusObjectID: $0.objectID, attribute: Item.StatusAttribute()) })
+ self.existStatusFetchedResultsController = StatusFetchedResultsController(managedObjectContext: context.managedObjectContext, domain: nil, additionalTweetPredicate: nil)
self.navigationBarTitle = CurrentValueSubject(
optionalStatus.flatMap { L10n.Scene.Thread.title($0.author.displayNameWithFallback) }
)
+ // bind fetcher domain
+ context.authenticationService.activeMastodonAuthenticationBox
+ .receive(on: RunLoop.main)
+ .sink { [weak self] box in
+ guard let self = self else { return }
+ self.existStatusFetchedResultsController.domain.value = box?.domain
+ }
+ .store(in: &disposeBag)
+
rootNode
.receive(on: DispatchQueue.main)
.sink { [weak self] rootNode in
@@ -79,8 +91,32 @@ class ThreadViewModel {
.store(in: &disposeBag)
}
- // descendantNodes
-
+ rootItem
+ .receive(on: DispatchQueue.main)
+ .sink { [weak self] rootItem in
+ guard let self = self else { return }
+ guard case let .root(objectID, _) = rootItem else { return }
+ self.context.managedObjectContext.perform {
+ guard let status = self.context.managedObjectContext.object(with: objectID) as? Status else {
+ return
+ }
+ self.rootItemObserver = ManagedObjectObserver.observe(object: status)
+ .receive(on: DispatchQueue.main)
+ .sink(receiveCompletion: { _ in
+ // do nothing
+ }, receiveValue: { [weak self] change in
+ guard let self = self else { return }
+ switch change.changeType {
+ case .delete:
+ self.rootItem.value = nil
+ default:
+ break
+ }
+ })
+ }
+ }
+ .store(in: &disposeBag)
+
ancestorNodes
.receive(on: DispatchQueue.main)
.compactMap { [weak self] nodes -> [Item]? in
@@ -276,4 +312,3 @@ extension ThreadViewModel {
}
}
-