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 */; };
|
D6C1B2082545D1EC00DAAA66 /* StatusCardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C1B2072545D1EC00DAAA66 /* StatusCardView.swift */; };
|
||||||
D6C3F4F5298ED0890009FCFF /* LocalPredicateStatusesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C3F4F4298ED0890009FCFF /* LocalPredicateStatusesViewController.swift */; };
|
D6C3F4F5298ED0890009FCFF /* LocalPredicateStatusesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C3F4F4298ED0890009FCFF /* LocalPredicateStatusesViewController.swift */; };
|
||||||
D6C3F4F7298ED7F70009FCFF /* FavoritesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C3F4F6298ED7F70009FCFF /* FavoritesViewController.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 */; };
|
D6C693EF216192C2007D6A6D /* TuskerNavigationDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C693EE216192C2007D6A6D /* TuskerNavigationDelegate.swift */; };
|
||||||
D6C693FC2162FE6F007D6A6D /* LoadingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C693FB2162FE6F007D6A6D /* LoadingViewController.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 */; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
D6C693FD2162FEEA007D6A6D /* UIViewController+Children.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIViewController+Children.swift"; sourceTree = "<group>"; };
|
||||||
|
@ -1088,6 +1090,7 @@
|
||||||
D65B4B6329771EFF00DABDFB /* ConversationViewController.swift */,
|
D65B4B6329771EFF00DABDFB /* ConversationViewController.swift */,
|
||||||
D601FA5C297B2E6F00A8E8B5 /* ConversationCollectionViewController.swift */,
|
D601FA5C297B2E6F00A8E8B5 /* ConversationCollectionViewController.swift */,
|
||||||
D601FA5E297B339100A8E8B5 /* ExpandThreadCollectionViewCell.swift */,
|
D601FA5E297B339100A8E8B5 /* ExpandThreadCollectionViewCell.swift */,
|
||||||
|
D6C3F4F8298EDBF20009FCFF /* ConversationTree.swift */,
|
||||||
);
|
);
|
||||||
path = Conversation;
|
path = Conversation;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -2025,6 +2028,7 @@
|
||||||
D6E77D0F286F773900D8B732 /* SplitNavigationController.swift in Sources */,
|
D6E77D0F286F773900D8B732 /* SplitNavigationController.swift in Sources */,
|
||||||
D6B30E09254BAF63009CAEE5 /* ImageGrayscalifier.swift in Sources */,
|
D6B30E09254BAF63009CAEE5 /* ImageGrayscalifier.swift in Sources */,
|
||||||
D6A6C10F25B62D2400298D0F /* DiskCache.swift in Sources */,
|
D6A6C10F25B62D2400298D0F /* DiskCache.swift in Sources */,
|
||||||
|
D6C3F4F9298EDBF20009FCFF /* ConversationTree.swift in Sources */,
|
||||||
D6B81F3C2560365300F6E31D /* RefreshableViewController.swift in Sources */,
|
D6B81F3C2560365300F6E31D /* RefreshableViewController.swift in Sources */,
|
||||||
D646C958213B367000269FB5 /* LargeImageShrinkAnimationController.swift in Sources */,
|
D646C958213B367000269FB5 /* LargeImageShrinkAnimationController.swift in Sources */,
|
||||||
D6674AEA23341F7600E8DF94 /* AppShortcutItems.swift in Sources */,
|
D6674AEA23341F7600E8DF94 /* AppShortcutItems.swift in Sources */,
|
||||||
|
|
|
@ -9,16 +9,6 @@
|
||||||
import UIKit
|
import UIKit
|
||||||
import Pachyderm
|
import Pachyderm
|
||||||
|
|
||||||
class ConversationNode {
|
|
||||||
let status: StatusMO
|
|
||||||
var children: [ConversationNode]
|
|
||||||
|
|
||||||
init(status: StatusMO) {
|
|
||||||
self.status = status
|
|
||||||
self.children = []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class ConversationCollectionViewController: UIViewController, CollectionViewController {
|
class ConversationCollectionViewController: UIViewController, CollectionViewController {
|
||||||
|
|
||||||
private let mastodonController: MastodonController
|
private let mastodonController: MastodonController
|
||||||
|
@ -136,32 +126,27 @@ class ConversationCollectionViewController: UIViewController, CollectionViewCont
|
||||||
}
|
}
|
||||||
|
|
||||||
func addContext(_ context: ConversationContext, for mainStatus: StatusMO) async {
|
func addContext(_ context: ConversationContext, for mainStatus: StatusMO) async {
|
||||||
let parentIDs = getDirectParents(inReplyTo: mainStatus.inReplyToID, from: context.ancestors)
|
await mastodonController.persistentContainer.addAll(statuses: context.ancestors + context.descendants)
|
||||||
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])
|
|
||||||
|
|
||||||
// fetch all descendant status managed objects
|
// fetch all descendant status managed objects
|
||||||
let descendantIDs = context.descendants.map(\.id)
|
let descendantIDs = context.descendants.map(\.id)
|
||||||
let request = StatusMO.fetchRequest()
|
let request = StatusMO.fetchRequest()
|
||||||
request.predicate = NSPredicate(format: "id IN %@", descendantIDs)
|
request.predicate = NSPredicate(format: "id IN %@", descendantIDs)
|
||||||
|
let descendants = try? mastodonController.persistentContainer.viewContext.fetch(request)
|
||||||
|
|
||||||
if let descendants = try? mastodonController.persistentContainer.viewContext.fetch(request) {
|
let tree = ConversationTree.build(from: context, mainStatus: mainStatus, descendants: descendants ?? [])
|
||||||
// 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
|
var snapshot = dataSource.snapshot()
|
||||||
self.addFlattenedChildThreadsToSnapshot(childThreads, mainStatus: mainStatus, snapshot: &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) {
|
self.dataSource.apply(snapshot, animatingDifferences: false) {
|
||||||
let item: Item
|
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>) {
|
private func addFlattenedChildThreadsToSnapshot(_ childThreads: [ConversationNode], mainStatus: StatusMO, snapshot: inout NSDiffableDataSourceSnapshot<Section, Item>) {
|
||||||
var childThreads = childThreads
|
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