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>
|
<key>CoreDataStack.xcscheme_^#shared#^_</key>
|
||||||
<dict>
|
<dict>
|
||||||
<key>orderHint</key>
|
<key>orderHint</key>
|
||||||
<integer>17</integer>
|
<integer>16</integer>
|
||||||
</dict>
|
</dict>
|
||||||
<key>Mastodon - RTL.xcscheme_^#shared#^_</key>
|
<key>Mastodon - RTL.xcscheme_^#shared#^_</key>
|
||||||
<dict>
|
<dict>
|
||||||
|
@ -32,7 +32,7 @@
|
||||||
<key>NotificationService.xcscheme_^#shared#^_</key>
|
<key>NotificationService.xcscheme_^#shared#^_</key>
|
||||||
<dict>
|
<dict>
|
||||||
<key>orderHint</key>
|
<key>orderHint</key>
|
||||||
<integer>16</integer>
|
<integer>17</integer>
|
||||||
</dict>
|
</dict>
|
||||||
</dict>
|
</dict>
|
||||||
<key>SuppressBuildableAutocreation</key>
|
<key>SuppressBuildableAutocreation</key>
|
||||||
|
|
|
@ -47,14 +47,18 @@ extension StatusSection {
|
||||||
|
|
||||||
// configure cell
|
// configure cell
|
||||||
managedObjectContext.performAndWait {
|
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(
|
StatusSection.configure(
|
||||||
cell: cell,
|
cell: cell,
|
||||||
dependency: dependency,
|
dependency: dependency,
|
||||||
readableLayoutFrame: tableView.readableContentGuide.layoutFrame,
|
readableLayoutFrame: tableView.readableContentGuide.layoutFrame,
|
||||||
timestampUpdatePublisher: timestampUpdatePublisher,
|
timestampUpdatePublisher: timestampUpdatePublisher,
|
||||||
status: timelineIndex.status,
|
status: status,
|
||||||
requestUserID: timelineIndex.userID,
|
requestUserID: userID,
|
||||||
statusItemAttribute: attribute
|
statusItemAttribute: attribute
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -752,12 +756,13 @@ extension StatusSection {
|
||||||
return L10n.Common.Controls.Timeline.Accessibility.countReblogs(status.favouritesCount.intValue)
|
return L10n.Common.Controls.Timeline.Accessibility.countReblogs(status.favouritesCount.intValue)
|
||||||
}()
|
}()
|
||||||
Publishers.CombineLatest(
|
Publishers.CombineLatest(
|
||||||
dependency.context.blockDomainService.blockedDomains,
|
dependency.context.blockDomainService.blockedDomains.setFailureType(to: ManagedObjectObserver.Error.self),
|
||||||
ManagedObjectObserver.observe(object: status.authorForUserProvider)
|
ManagedObjectObserver.observe(object: status.authorForUserProvider)
|
||||||
.assertNoFailure()
|
)
|
||||||
)
|
|
||||||
.receive(on: RunLoop.main)
|
.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 cell = cell else { return }
|
||||||
guard let dependency = dependency else { return }
|
guard let dependency = dependency else { return }
|
||||||
switch change.changeType {
|
switch change.changeType {
|
||||||
|
@ -769,7 +774,7 @@ extension StatusSection {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
StatusSection.setupStatusMoreButtonMenu(cell: cell, dependency: dependency, status: status)
|
StatusSection.setupStatusMoreButtonMenu(cell: cell, dependency: dependency, status: status)
|
||||||
}
|
})
|
||||||
.store(in: &cell.disposeBag)
|
.store(in: &cell.disposeBag)
|
||||||
self.setupStatusMoreButtonMenu(cell: cell, dependency: dependency, status: status)
|
self.setupStatusMoreButtonMenu(cell: cell, dependency: dependency, status: status)
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,6 +8,8 @@
|
||||||
import UIKit
|
import UIKit
|
||||||
import Combine
|
import Combine
|
||||||
import CoreData
|
import CoreData
|
||||||
|
import CoreDataStack
|
||||||
|
import MastodonSDK
|
||||||
|
|
||||||
extension ThreadViewModel {
|
extension ThreadViewModel {
|
||||||
|
|
||||||
|
@ -41,13 +43,29 @@ extension ThreadViewModel {
|
||||||
diffableDataSource?.apply(snapshot, animatingDifferences: false, completion: nil)
|
diffableDataSource?.apply(snapshot, animatingDifferences: false, completion: nil)
|
||||||
|
|
||||||
Publishers.CombineLatest3(
|
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,
|
rootItem,
|
||||||
ancestorItems,
|
ancestorItems,
|
||||||
descendantItems
|
descendantItems,
|
||||||
|
existStatusFetchedResultsController.objectIDs
|
||||||
)
|
)
|
||||||
.debounce(for: .milliseconds(100), scheduler: DispatchQueue.main) // some magic to avoid jitter
|
.debounce(for: .milliseconds(100), scheduler: RunLoop.main) // some magic to avoid jitter
|
||||||
.receive(on: DispatchQueue.main)
|
.sink { [weak self] rootItem, ancestorItems, descendantItems, existObjectIDs in
|
||||||
.sink { [weak self] rootItem, ancestorItems, descendantItems in
|
|
||||||
guard let self = self else { return }
|
guard let self = self else { return }
|
||||||
guard let tableView = self.tableView,
|
guard let tableView = self.tableView,
|
||||||
let navigationBar = self.contentOffsetAdjustableTimelineViewControllerDelegate?.navigationBar()
|
let navigationBar = self.contentOffsetAdjustableTimelineViewControllerDelegate?.navigationBar()
|
||||||
|
@ -65,31 +83,42 @@ extension ThreadViewModel {
|
||||||
if self.rootNode.value?.replyToID != nil, !(currentState is LoadThreadState.NoMore) {
|
if self.rootNode.value?.replyToID != nil, !(currentState is LoadThreadState.NoMore) {
|
||||||
newSnapshot.appendItems([.topLoader], toSection: .main)
|
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)
|
newSnapshot.appendItems(ancestorItems, toSection: .main)
|
||||||
|
|
||||||
// root
|
// root
|
||||||
if let rootItem = rootItem {
|
if let rootItem = rootItem,
|
||||||
switch rootItem {
|
case let .root(objectID, _) = rootItem,
|
||||||
case .root:
|
existObjectIDs.contains(objectID) {
|
||||||
newSnapshot.appendItems([rootItem], toSection: .main)
|
newSnapshot.appendItems([rootItem], toSection: .main)
|
||||||
default:
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// leaf
|
// leaf
|
||||||
if !(currentState is LoadThreadState.NoMore) {
|
if !(currentState is LoadThreadState.NoMore) {
|
||||||
newSnapshot.appendItems([.bottomLoader], toSection: .main)
|
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)
|
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 {
|
guard let difference = self.calculateReloadSnapshotDifference(navigationBar: navigationBar, tableView: tableView, oldSnapshot: oldSnapshot, newSnapshot: newSnapshot) else {
|
||||||
diffableDataSource.apply(newSnapshot)
|
diffableDataSource.apply(newSnapshot)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// addtional margin for .topLoader
|
// additional margin for .topLoader
|
||||||
let oldTopMargin: CGFloat = {
|
let oldTopMargin: CGFloat = {
|
||||||
let marginHeight = TimelineTopLoaderTableViewCell.cellHeight
|
let marginHeight = TimelineTopLoaderTableViewCell.cellHeight
|
||||||
if oldSnapshot.itemIdentifiers.contains(.topLoader) {
|
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 {
|
class ThreadViewModel {
|
||||||
|
|
||||||
var disposeBag = Set<AnyCancellable>()
|
var disposeBag = Set<AnyCancellable>()
|
||||||
|
var rootItemObserver: AnyCancellable?
|
||||||
|
|
||||||
// input
|
// input
|
||||||
let context: AppContext
|
let context: AppContext
|
||||||
let rootNode: CurrentValueSubject<RootNode?, Never>
|
let rootNode: CurrentValueSubject<RootNode?, Never>
|
||||||
let rootItem: CurrentValueSubject<Item?, Never>
|
let rootItem: CurrentValueSubject<Item?, Never>
|
||||||
let cellFrameCache = NSCache<NSNumber, NSValue>()
|
let cellFrameCache = NSCache<NSNumber, NSValue>()
|
||||||
|
let existStatusFetchedResultsController: StatusFetchedResultsController
|
||||||
|
|
||||||
weak var contentOffsetAdjustableTimelineViewControllerDelegate: ContentOffsetAdjustableTimelineViewControllerDelegate?
|
weak var contentOffsetAdjustableTimelineViewControllerDelegate: ContentOffsetAdjustableTimelineViewControllerDelegate?
|
||||||
weak var tableView: UITableView?
|
weak var tableView: UITableView?
|
||||||
|
@ -49,10 +51,20 @@ class ThreadViewModel {
|
||||||
self.context = context
|
self.context = context
|
||||||
self.rootNode = CurrentValueSubject(optionalStatus.flatMap { RootNode(domain: $0.domain, statusID: $0.id, replyToID: $0.inReplyToID) })
|
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.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(
|
self.navigationBarTitle = CurrentValueSubject(
|
||||||
optionalStatus.flatMap { L10n.Scene.Thread.title($0.author.displayNameWithFallback) }
|
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
|
rootNode
|
||||||
.receive(on: DispatchQueue.main)
|
.receive(on: DispatchQueue.main)
|
||||||
.sink { [weak self] rootNode in
|
.sink { [weak self] rootNode in
|
||||||
|
@ -79,8 +91,32 @@ class ThreadViewModel {
|
||||||
.store(in: &disposeBag)
|
.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
|
ancestorNodes
|
||||||
.receive(on: DispatchQueue.main)
|
.receive(on: DispatchQueue.main)
|
||||||
.compactMap { [weak self] nodes -> [Item]? in
|
.compactMap { [weak self] nodes -> [Item]? in
|
||||||
|
@ -276,4 +312,3 @@ extension ThreadViewModel {
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue