forked from shadowfacts/Tusker
Extract out conversation tree-building code
This commit is contained in:
parent
ab3bad0e16
commit
a314521b96
|
@ -297,6 +297,7 @@
|
|||
D6C1B2082545D1EC00DAAA66 /* StatusCardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C1B2072545D1EC00DAAA66 /* StatusCardView.swift */; };
|
||||
D6C3F4F5298ED0890009FCFF /* LocalPredicateStatusesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C3F4F4298ED0890009FCFF /* LocalPredicateStatusesViewController.swift */; };
|
||||
D6C3F4F7298ED7F70009FCFF /* FavoritesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C3F4F6298ED7F70009FCFF /* FavoritesViewController.swift */; };
|
||||
D6C3F4F9298EDBF20009FCFF /* ConversationTree.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C3F4F8298EDBF20009FCFF /* ConversationTree.swift */; };
|
||||
D6C693EF216192C2007D6A6D /* TuskerNavigationDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C693EE216192C2007D6A6D /* TuskerNavigationDelegate.swift */; };
|
||||
D6C693FC2162FE6F007D6A6D /* LoadingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C693FB2162FE6F007D6A6D /* LoadingViewController.swift */; };
|
||||
D6C693FE2162FEEA007D6A6D /* UIViewController+Children.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C693FD2162FEEA007D6A6D /* UIViewController+Children.swift */; };
|
||||
|
@ -706,6 +707,7 @@
|
|||
D6C1B2072545D1EC00DAAA66 /* StatusCardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusCardView.swift; sourceTree = "<group>"; };
|
||||
D6C3F4F4298ED0890009FCFF /* LocalPredicateStatusesViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalPredicateStatusesViewController.swift; sourceTree = "<group>"; };
|
||||
D6C3F4F6298ED7F70009FCFF /* FavoritesViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavoritesViewController.swift; sourceTree = "<group>"; };
|
||||
D6C3F4F8298EDBF20009FCFF /* ConversationTree.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConversationTree.swift; sourceTree = "<group>"; };
|
||||
D6C693EE216192C2007D6A6D /* TuskerNavigationDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TuskerNavigationDelegate.swift; sourceTree = "<group>"; };
|
||||
D6C693FB2162FE6F007D6A6D /* LoadingViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadingViewController.swift; sourceTree = "<group>"; };
|
||||
D6C693FD2162FEEA007D6A6D /* UIViewController+Children.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIViewController+Children.swift"; sourceTree = "<group>"; };
|
||||
|
@ -1088,6 +1090,7 @@
|
|||
D65B4B6329771EFF00DABDFB /* ConversationViewController.swift */,
|
||||
D601FA5C297B2E6F00A8E8B5 /* ConversationCollectionViewController.swift */,
|
||||
D601FA5E297B339100A8E8B5 /* ExpandThreadCollectionViewCell.swift */,
|
||||
D6C3F4F8298EDBF20009FCFF /* ConversationTree.swift */,
|
||||
);
|
||||
path = Conversation;
|
||||
sourceTree = "<group>";
|
||||
|
@ -2025,6 +2028,7 @@
|
|||
D6E77D0F286F773900D8B732 /* SplitNavigationController.swift in Sources */,
|
||||
D6B30E09254BAF63009CAEE5 /* ImageGrayscalifier.swift in Sources */,
|
||||
D6A6C10F25B62D2400298D0F /* DiskCache.swift in Sources */,
|
||||
D6C3F4F9298EDBF20009FCFF /* ConversationTree.swift in Sources */,
|
||||
D6B81F3C2560365300F6E31D /* RefreshableViewController.swift in Sources */,
|
||||
D646C958213B367000269FB5 /* LargeImageShrinkAnimationController.swift in Sources */,
|
||||
D6674AEA23341F7600E8DF94 /* AppShortcutItems.swift in Sources */,
|
||||
|
|
|
@ -9,16 +9,6 @@
|
|||
import UIKit
|
||||
import Pachyderm
|
||||
|
||||
class ConversationNode {
|
||||
let status: StatusMO
|
||||
var children: [ConversationNode]
|
||||
|
||||
init(status: StatusMO) {
|
||||
self.status = status
|
||||
self.children = []
|
||||
}
|
||||
}
|
||||
|
||||
class ConversationCollectionViewController: UIViewController, CollectionViewController {
|
||||
|
||||
private let mastodonController: MastodonController
|
||||
|
@ -136,32 +126,27 @@ class ConversationCollectionViewController: UIViewController, CollectionViewCont
|
|||
}
|
||||
|
||||
func addContext(_ context: ConversationContext, for mainStatus: StatusMO) async {
|
||||
let parentIDs = getDirectParents(inReplyTo: mainStatus.inReplyToID, from: context.ancestors)
|
||||
let parentStatuses = context.ancestors.filter { parentIDs.contains($0.id) }
|
||||
|
||||
await mastodonController.persistentContainer.addAll(statuses: parentStatuses + context.descendants)
|
||||
|
||||
var snapshot = dataSource.snapshot()
|
||||
snapshot.deleteItems([.loadingIndicator])
|
||||
let mainStatusItem = Item.status(id: mainStatusID, state: mainStatusState, prevLink: mainStatus.inReplyToID != nil, nextLink: false)
|
||||
let parentItems = parentIDs.enumerated().map { index, id in
|
||||
Item.status(id: id, state: .unknown, prevLink: index > 0, nextLink: true)
|
||||
}
|
||||
snapshot.insertItems(parentItems, beforeItem: mainStatusItem)
|
||||
snapshot.reloadItems([mainStatusItem])
|
||||
await mastodonController.persistentContainer.addAll(statuses: context.ancestors + context.descendants)
|
||||
|
||||
// fetch all descendant status managed objects
|
||||
let descendantIDs = context.descendants.map(\.id)
|
||||
let request = StatusMO.fetchRequest()
|
||||
request.predicate = NSPredicate(format: "id IN %@", descendantIDs)
|
||||
let descendants = try? mastodonController.persistentContainer.viewContext.fetch(request)
|
||||
|
||||
if let descendants = try? mastodonController.persistentContainer.viewContext.fetch(request) {
|
||||
// convert array of descendant statuses into tree of sub-threads
|
||||
let childThreads = self.getDescendantThreads(mainStatus: mainStatus, descendants: descendants)
|
||||
|
||||
// convert sub-threads into items for section and add to snapshot
|
||||
self.addFlattenedChildThreadsToSnapshot(childThreads, mainStatus: mainStatus, snapshot: &snapshot)
|
||||
let tree = ConversationTree.build(from: context, mainStatus: mainStatus, descendants: descendants ?? [])
|
||||
|
||||
var snapshot = dataSource.snapshot()
|
||||
snapshot.deleteItems([.loadingIndicator])
|
||||
let mainStatusItem = Item.status(id: mainStatusID, state: mainStatusState, prevLink: mainStatus.inReplyToID != nil, nextLink: false)
|
||||
let parentItems = tree.ancestors.enumerated().map { index, id in
|
||||
Item.status(id: id, state: .unknown, prevLink: index > 0, nextLink: true)
|
||||
}
|
||||
snapshot.insertItems(parentItems, beforeItem: mainStatusItem)
|
||||
snapshot.reloadItems([mainStatusItem])
|
||||
|
||||
// convert sub-threads into items for section and add to snapshot
|
||||
self.addFlattenedChildThreadsToSnapshot(tree.descendants, mainStatus: mainStatus, snapshot: &snapshot)
|
||||
|
||||
self.dataSource.apply(snapshot, animatingDifferences: false) {
|
||||
let item: Item
|
||||
|
@ -187,54 +172,6 @@ class ConversationCollectionViewController: UIViewController, CollectionViewCont
|
|||
}
|
||||
}
|
||||
|
||||
private func getDirectParents(inReplyTo inReplyToID: String?, from statuses: [Status]) -> [String] {
|
||||
var statuses = statuses
|
||||
var parents = [String]()
|
||||
|
||||
var parentID: String? = inReplyToID
|
||||
|
||||
while parentID != nil, let parentIndex = statuses.firstIndex(where: { $0.id == parentID }) {
|
||||
let parentStatus = statuses.remove(at: parentIndex)
|
||||
parents.insert(parentStatus.id, at: 0)
|
||||
parentID = parentStatus.inReplyToID
|
||||
}
|
||||
|
||||
return parents
|
||||
}
|
||||
|
||||
private func getDescendantThreads(mainStatus: StatusMO, descendants: [StatusMO]) -> [ConversationNode] {
|
||||
var descendants = descendants
|
||||
|
||||
func removeAllInReplyTo(id: String) -> [StatusMO] {
|
||||
let statuses = descendants.filter { $0.inReplyToID == id }
|
||||
descendants.removeAll { $0.inReplyToID == id }
|
||||
return statuses
|
||||
}
|
||||
|
||||
var nodes: [String: ConversationNode] = [
|
||||
mainStatus.id: ConversationNode(status: mainStatus)
|
||||
]
|
||||
|
||||
var idsToCheck = [mainStatusID]
|
||||
|
||||
while !idsToCheck.isEmpty {
|
||||
let inReplyToID = idsToCheck.removeFirst()
|
||||
let nodeForID = nodes[inReplyToID]!
|
||||
|
||||
let inReply = removeAllInReplyTo(id: inReplyToID)
|
||||
for reply in inReply {
|
||||
idsToCheck.append(reply.id)
|
||||
|
||||
let replyNode = ConversationNode(status: reply)
|
||||
nodes[reply.id] = replyNode
|
||||
|
||||
nodeForID.children.append(replyNode)
|
||||
}
|
||||
}
|
||||
|
||||
return nodes[mainStatusID]!.children
|
||||
}
|
||||
|
||||
private func addFlattenedChildThreadsToSnapshot(_ childThreads: [ConversationNode], mainStatus: StatusMO, snapshot: inout NSDiffableDataSourceSnapshot<Section, Item>) {
|
||||
var childThreads = childThreads
|
||||
|
||||
|
|
|
@ -0,0 +1,80 @@
|
|||
//
|
||||
// ConversationTree.swift
|
||||
// Tusker
|
||||
//
|
||||
// Created by Shadowfacts on 2/4/23.
|
||||
// Copyright © 2023 Shadowfacts. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Pachyderm
|
||||
|
||||
class ConversationNode {
|
||||
let status: StatusMO
|
||||
var children: [ConversationNode]
|
||||
|
||||
init(status: StatusMO) {
|
||||
self.status = status
|
||||
self.children = []
|
||||
}
|
||||
}
|
||||
struct ConversationTree {
|
||||
let ancestors: [String]
|
||||
let descendants: [ConversationNode]
|
||||
|
||||
static func build(from context: ConversationContext, mainStatus: StatusMO, descendants: [StatusMO]) -> ConversationTree {
|
||||
let ancestors = getDirectParents(inReplyTo: mainStatus.inReplyToID, from: context)
|
||||
let childThreads = getDescendantThreads(mainStatus: mainStatus, descendants: descendants)
|
||||
return ConversationTree(ancestors: ancestors, descendants: childThreads)
|
||||
}
|
||||
|
||||
private static func getDirectParents(inReplyTo inReplyToID: String?, from context: ConversationContext) -> [String] {
|
||||
var statuses = context.ancestors
|
||||
var parents = [String]()
|
||||
|
||||
var parentID: String? = inReplyToID
|
||||
|
||||
while let currentParentID = parentID,
|
||||
let parentIndex = statuses.firstIndex(where: { $0.id == currentParentID }) {
|
||||
let parentStatus = statuses.remove(at: parentIndex)
|
||||
parents.insert(parentStatus.id, at: 0)
|
||||
parentID = parentStatus.inReplyToID
|
||||
}
|
||||
|
||||
return parents
|
||||
}
|
||||
|
||||
private static func getDescendantThreads(mainStatus: StatusMO, descendants: [StatusMO]) -> [ConversationNode] {
|
||||
var descendants = descendants
|
||||
|
||||
func removeAllInReplyTo(id: String) -> [StatusMO] {
|
||||
let statuses = descendants.filter { $0.inReplyToID == id }
|
||||
descendants.removeAll { $0.inReplyToID == id }
|
||||
return statuses
|
||||
}
|
||||
|
||||
let mainStatusNode = ConversationNode(status: mainStatus)
|
||||
var nodes: [String: ConversationNode] = [
|
||||
mainStatus.id: mainStatusNode
|
||||
]
|
||||
|
||||
var idsToCheck = [mainStatus.id]
|
||||
|
||||
while !idsToCheck.isEmpty {
|
||||
let inReplyToID = idsToCheck.removeFirst()
|
||||
let nodeForID = nodes[inReplyToID]!
|
||||
|
||||
let inReply = removeAllInReplyTo(id: inReplyToID)
|
||||
for reply in inReply {
|
||||
idsToCheck.append(reply.id)
|
||||
|
||||
let replyNode = ConversationNode(status: reply)
|
||||
nodes[reply.id] = replyNode
|
||||
|
||||
nodeForID.children.append(replyNode)
|
||||
}
|
||||
}
|
||||
|
||||
return mainStatusNode.children
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue