fix: handle status delete UI updater in thread scene
This commit is contained in:
parent
a9cce7b3e3
commit
2dfd6168a9
|
@ -12,7 +12,7 @@
|
|||
<key>CoreDataStack.xcscheme_^#shared#^_</key>
|
||||
<dict>
|
||||
<key>orderHint</key>
|
||||
<integer>17</integer>
|
||||
<integer>16</integer>
|
||||
</dict>
|
||||
<key>Mastodon - RTL.xcscheme_^#shared#^_</key>
|
||||
<dict>
|
||||
|
@ -32,7 +32,7 @@
|
|||
<key>NotificationService.xcscheme_^#shared#^_</key>
|
||||
<dict>
|
||||
<key>orderHint</key>
|
||||
<integer>16</integer>
|
||||
<integer>17</integer>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>SuppressBuildableAutocreation</key>
|
||||
|
|
|
@ -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 <uninitialized> 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)
|
||||
}
|
||||
|
|
|
@ -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:
|
||||
if let rootItem = rootItem,
|
||||
case let .root(objectID, _) = rootItem,
|
||||
existObjectIDs.contains(objectID) {
|
||||
newSnapshot.appendItems([rootItem], toSection: .main)
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,12 +16,14 @@ import MastodonSDK
|
|||
class ThreadViewModel {
|
||||
|
||||
var disposeBag = Set<AnyCancellable>()
|
||||
var rootItemObserver: AnyCancellable?
|
||||
|
||||
// input
|
||||
let context: AppContext
|
||||
let rootNode: CurrentValueSubject<RootNode?, Never>
|
||||
let rootItem: CurrentValueSubject<Item?, Never>
|
||||
let cellFrameCache = NSCache<NSNumber, NSValue>()
|
||||
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,7 +91,31 @@ 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)
|
||||
|
@ -276,4 +312,3 @@ extension ThreadViewModel {
|
|||
}
|
||||
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue