fix: resolve requested changes
This commit is contained in:
parent
28cfe96171
commit
a61e662f38
|
@ -8,6 +8,7 @@
|
|||
|
||||
/* Begin PBXBuildFile section */
|
||||
0F1E2D0B2615C39400C38565 /* HashtagTimelineTitleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0F1E2D0A2615C39400C38565 /* HashtagTimelineTitleView.swift */; };
|
||||
0F1E2FE9261D7FD000C38565 /* StatusWithGapFetchResultController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0F1E2FE8261D7FD000C38565 /* StatusWithGapFetchResultController.swift */; };
|
||||
0F2021FB2613262F000C64BF /* HashtagTimelineViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0F2021FA2613262F000C64BF /* HashtagTimelineViewController.swift */; };
|
||||
0F202201261326E6000C64BF /* HashtagTimelineViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0F202200261326E6000C64BF /* HashtagTimelineViewModel.swift */; };
|
||||
0F20220726134DA4000C64BF /* HashtagTimelineViewModel+Diffable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0F20220626134DA4000C64BF /* HashtagTimelineViewModel+Diffable.swift */; };
|
||||
|
@ -356,6 +357,7 @@
|
|||
|
||||
/* Begin PBXFileReference section */
|
||||
0F1E2D0A2615C39400C38565 /* HashtagTimelineTitleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HashtagTimelineTitleView.swift; sourceTree = "<group>"; };
|
||||
0F1E2FE8261D7FD000C38565 /* StatusWithGapFetchResultController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusWithGapFetchResultController.swift; sourceTree = "<group>"; };
|
||||
0F2021FA2613262F000C64BF /* HashtagTimelineViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HashtagTimelineViewController.swift; sourceTree = "<group>"; };
|
||||
0F202200261326E6000C64BF /* HashtagTimelineViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HashtagTimelineViewModel.swift; sourceTree = "<group>"; };
|
||||
0F20220626134DA4000C64BF /* HashtagTimelineViewModel+Diffable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "HashtagTimelineViewModel+Diffable.swift"; sourceTree = "<group>"; };
|
||||
|
@ -1659,6 +1661,7 @@
|
|||
isa = PBXGroup;
|
||||
children = (
|
||||
DBCBED1C26132E1A00B49291 /* StatusFetchedResultsController.swift */,
|
||||
0F1E2FE8261D7FD000C38565 /* StatusWithGapFetchResultController.swift */,
|
||||
);
|
||||
path = FetchedResultsController;
|
||||
sourceTree = "<group>";
|
||||
|
@ -2137,6 +2140,7 @@
|
|||
DB71FD3C25F8A1C500512AE1 /* APIService+Persist+PersistCache.swift in Sources */,
|
||||
2DA6055125F74407006356F9 /* AudioContainerViewModel.swift in Sources */,
|
||||
0FB3D2FE25E4CB6400AAD544 /* PickServerTitleCell.swift in Sources */,
|
||||
0F1E2FE9261D7FD000C38565 /* StatusWithGapFetchResultController.swift in Sources */,
|
||||
DBCC3B7B261443AD0045B23D /* ViewController.swift in Sources */,
|
||||
2D38F1DF25CD46A400561493 /* HomeTimelineViewController+StatusProvider.swift in Sources */,
|
||||
2D206B9225F60EA700143C56 /* UIControl.swift in Sources */,
|
||||
|
|
|
@ -0,0 +1,85 @@
|
|||
//
|
||||
// StatusWithGapFetchResultController.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by BradGao on 2021/4/7.
|
||||
//
|
||||
|
||||
import os.log
|
||||
import UIKit
|
||||
import Combine
|
||||
import CoreData
|
||||
import CoreDataStack
|
||||
import MastodonSDK
|
||||
|
||||
class StatusWithGapFetchResultController: NSObject {
|
||||
var disposeBag = Set<AnyCancellable>()
|
||||
|
||||
let fetchedResultsController: NSFetchedResultsController<Status>
|
||||
|
||||
// input
|
||||
let domain = CurrentValueSubject<String?, Never>(nil)
|
||||
let statusIDs = CurrentValueSubject<[Mastodon.Entity.Status.ID], Never>([])
|
||||
|
||||
var needLoadMiddleIndex: Int? = nil
|
||||
|
||||
// output
|
||||
let objectIDs = CurrentValueSubject<[NSManagedObjectID], Never>([])
|
||||
|
||||
init(managedObjectContext: NSManagedObjectContext, domain: String?, additionalTweetPredicate: NSPredicate) {
|
||||
self.domain.value = domain ?? ""
|
||||
self.fetchedResultsController = {
|
||||
let fetchRequest = Status.sortedFetchRequest
|
||||
fetchRequest.returnsObjectsAsFaults = false
|
||||
fetchRequest.fetchBatchSize = 20
|
||||
let controller = NSFetchedResultsController(
|
||||
fetchRequest: fetchRequest,
|
||||
managedObjectContext: managedObjectContext,
|
||||
sectionNameKeyPath: nil,
|
||||
cacheName: nil
|
||||
)
|
||||
|
||||
return controller
|
||||
}()
|
||||
super.init()
|
||||
|
||||
fetchedResultsController.delegate = self
|
||||
|
||||
Publishers.CombineLatest(
|
||||
self.domain.removeDuplicates().eraseToAnyPublisher(),
|
||||
self.statusIDs.removeDuplicates().eraseToAnyPublisher()
|
||||
)
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] domain, ids in
|
||||
guard let self = self else { return }
|
||||
self.fetchedResultsController.fetchRequest.predicate = NSCompoundPredicate(andPredicateWithSubpredicates: [
|
||||
Status.predicate(domain: domain ?? "", ids: ids),
|
||||
additionalTweetPredicate
|
||||
])
|
||||
do {
|
||||
try self.fetchedResultsController.performFetch()
|
||||
} catch {
|
||||
assertionFailure(error.localizedDescription)
|
||||
}
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - NSFetchedResultsControllerDelegate
|
||||
extension StatusWithGapFetchResultController: NSFetchedResultsControllerDelegate {
|
||||
func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChangeContentWith snapshot: NSDiffableDataSourceSnapshotReference) {
|
||||
os_log("%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
|
||||
|
||||
let indexes = statusIDs.value
|
||||
let objects = fetchedResultsController.fetchedObjects ?? []
|
||||
|
||||
let items: [NSManagedObjectID] = objects
|
||||
.compactMap { object in
|
||||
indexes.firstIndex(of: object.id).map { index in (index, object) }
|
||||
}
|
||||
.sorted { $0.0 < $1.0 }
|
||||
.map { $0.1.objectID }
|
||||
self.objectIDs.value = items
|
||||
}
|
||||
}
|
|
@ -56,7 +56,8 @@ final class ComposeViewModel {
|
|||
let isPollToolbarButtonEnabled = CurrentValueSubject<Bool, Never>(true)
|
||||
let characterCount = CurrentValueSubject<Int, Never>(0)
|
||||
|
||||
var injectedContent: String? = nil
|
||||
// In some specific scenes(hashtag scene e.g.), we need to display the compose scene with pre-inserted text(insert '#mastodon ' in #mastodon hashtag scene, e.g.), the pre-inserted text should be treated as mannually inputed by users.
|
||||
var preInsertedContent: String? = nil
|
||||
|
||||
// custom emojis
|
||||
var customEmojiViewModelSubscription: AnyCancellable?
|
||||
|
@ -74,11 +75,11 @@ final class ComposeViewModel {
|
|||
init(
|
||||
context: AppContext,
|
||||
composeKind: ComposeStatusSection.ComposeKind,
|
||||
injectedContent: String? = nil
|
||||
preInsertedContent: String? = nil
|
||||
) {
|
||||
self.context = context
|
||||
self.composeKind = composeKind
|
||||
self.injectedContent = injectedContent
|
||||
self.preInsertedContent = preInsertedContent
|
||||
switch composeKind {
|
||||
case .post: self.title = CurrentValueSubject(L10n.Scene.Compose.Title.newPost)
|
||||
case .reply: self.title = CurrentValueSubject(L10n.Scene.Compose.Title.newReply)
|
||||
|
@ -204,9 +205,9 @@ final class ComposeViewModel {
|
|||
if content.isEmpty {
|
||||
return true
|
||||
}
|
||||
// if injectedContent plus a space is equal to the content, simply dismiss the modal
|
||||
if let injectedContent = self?.injectedContent {
|
||||
return content == (injectedContent + " ")
|
||||
// if preInsertedContent plus a space is equal to the content, simply dismiss the modal
|
||||
if let preInsertedContent = self?.preInsertedContent {
|
||||
return content == (preInsertedContent + " ")
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
@ -316,9 +317,9 @@ final class ComposeViewModel {
|
|||
})
|
||||
.store(in: &disposeBag)
|
||||
|
||||
if let injectedContent = injectedContent {
|
||||
if let preInsertedContent = preInsertedContent {
|
||||
// add a space after the injected text
|
||||
composeStatusAttribute.composeContent.send(injectedContent + " ")
|
||||
composeStatusAttribute.composeContent.send(preInsertedContent + " ")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -32,11 +32,11 @@ extension HashtagTimelineViewController: StatusProvider {
|
|||
}
|
||||
|
||||
switch item {
|
||||
case .homeTimelineIndex(let objectID, _):
|
||||
case .status(let objectID, _):
|
||||
let managedObjectContext = self.viewModel.context.managedObjectContext
|
||||
managedObjectContext.perform {
|
||||
let timelineIndex = managedObjectContext.object(with: objectID) as? HomeTimelineIndex
|
||||
promise(.success(timelineIndex?.status))
|
||||
let status = managedObjectContext.object(with: objectID) as? Status
|
||||
promise(.success(status))
|
||||
}
|
||||
default:
|
||||
promise(.success(nil))
|
||||
|
|
|
@ -165,7 +165,7 @@ extension HashtagTimelineViewController {
|
|||
|
||||
@objc private func composeBarButtonItemPressed(_ sender: UIBarButtonItem) {
|
||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
|
||||
let composeViewModel = ComposeViewModel(context: context, composeKind: .post, injectedContent: "#\(viewModel.hashTag)")
|
||||
let composeViewModel = ComposeViewModel(context: context, composeKind: .post, preInsertedContent: "#\(viewModel.hashTag)")
|
||||
coordinator.present(scene: .compose(viewModel: composeViewModel), from: self, transition: .modal(animated: true, completion: nil))
|
||||
}
|
||||
|
||||
|
|
|
@ -55,14 +55,17 @@ extension HashtagTimelineViewModel: NSFetchedResultsControllerDelegate {
|
|||
let oldSnapshot = diffableDataSource.snapshot()
|
||||
let snapshot = snapshot as NSDiffableDataSourceSnapshot<Int, NSManagedObjectID>
|
||||
|
||||
var oldSnapshotAttributeDict: [NSManagedObjectID : Item.StatusAttribute] = [:]
|
||||
for item in oldSnapshot.itemIdentifiers {
|
||||
guard case let .status(objectID, attribute) = item else { continue }
|
||||
oldSnapshotAttributeDict[objectID] = attribute
|
||||
}
|
||||
|
||||
let statusItemList: [Item] = snapshot.itemIdentifiers.map {
|
||||
let status = managedObjectContext.object(with: $0) as! Status
|
||||
|
||||
let isStatusTextSensitive: Bool = {
|
||||
guard let spoilerText = status.spoilerText, !spoilerText.isEmpty else { return false }
|
||||
return true
|
||||
}()
|
||||
return Item.status(objectID: $0, attribute: Item.StatusAttribute(isStatusTextSensitive: isStatusTextSensitive, isStatusSensitive: status.sensitive))
|
||||
let attribute = oldSnapshotAttributeDict[$0] ?? Item.StatusAttribute()
|
||||
return Item.status(objectID: $0, attribute: attribute)
|
||||
}
|
||||
|
||||
var newSnapshot = NSDiffableDataSourceSnapshot<StatusSection, Item>()
|
||||
|
|
|
@ -222,6 +222,6 @@ extension WelcomeViewController: OnboardingViewControllerAppearance { }
|
|||
extension WelcomeViewController: UIAdaptivePresentationControllerDelegate {
|
||||
func adaptivePresentationStyle(for controller: UIPresentationController, traitCollection: UITraitCollection) -> UIModalPresentationStyle {
|
||||
// make underneath view controller alive to fix layout issue due to view life cycle
|
||||
return .overFullScreen
|
||||
return .fullScreen
|
||||
}
|
||||
}
|
||||
|
|
|
@ -258,5 +258,12 @@ extension UserTimelineViewModel.State {
|
|||
return false
|
||||
}
|
||||
}
|
||||
|
||||
override func didEnter(from previousState: GKState?) {
|
||||
super.didEnter(from: previousState)
|
||||
guard let viewModel = viewModel, let stateMachine = stateMachine else { return }
|
||||
|
||||
viewModel.statusFetchedResultsController.objectIDs.value = viewModel.statusFetchedResultsController.objectIDs.value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -121,7 +121,7 @@ extension Mastodon.API.Favorites {
|
|||
case destroy
|
||||
}
|
||||
|
||||
public struct ListQuery: GetQuery,PagedQueryType {
|
||||
public struct ListQuery: GetQuery, PagedQueryType {
|
||||
|
||||
public var limit: Int?
|
||||
public var minID: String?
|
||||
|
|
Loading…
Reference in New Issue