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 { } } -