import type { ArrayType, EntityID } from '@wovin/core' import type { IObservableArray } from '@wovin/core/mobx' import type { Thread } from '@wovin/core/thread' import { SetMap } from '@note3/utils' import { autorunButAlsoImmediately } from '@wovin/core' import { action, observable, runInAction } from '@wovin/core/mobx' import { Logger } from 'besonders-logger' import find from 'lodash-es/find' import { BLK, useBlk } from './VMs/BlockVM' const { WARN, LOG, DEBUG, VERBOSE, ERROR } = Logger.setup(Logger.INFO) // eslint-disable-line unused-imports/no-unused-vars // TODO: Move to AppSettings when config system is ready const SHOW_GENERIC_ENTITIES_IN_TIMELINE = true export type ORDER = 'date_asc' | 'date_desc' export interface MATCH_ROOT { blockID: EntityID } const PARENT_CHECK_DEPTH = 1 export function useMatchTree(thread: Thread, { order }: { order: ORDER }) { DEBUG(`[useMatchTree<${thread.nameAndSizeUntracked}>]`) const roots = observable.array() const complete = observable.box(false) let loadMore: (/* count?: number */) => Promise autorunButAlsoImmediately(() => { DEBUG(`[useMatchTree.autorun.start]`, { thread }) runInAction(() => { complete.set(false) if (roots.length) roots.replace([]) // lazy replace? }) const matchesGen = findMatches(thread, { order }) let matchesGenDone = false const blockToParents = new SetMap() const parentToKids = new SetMap() const loadOne = action(function loadOne() { DEBUG(`[useMatchTree.loadOne]`, { thread, roots, matchesGen }) findOne: while (!matchesGenDone) { const { value: blockID, done } = matchesGen.next() DEBUG(`[useMatchTree.loadOne.match]`, blockID, { done, roots: [...roots] }) if (done) { matchesGenDone = true complete.set(true) } if (!blockID) return false // matchGen round had no result if (find(roots, { blockID })) { DEBUG(`[useMatchTree.loadOne.match] existing root`, blockID) continue findOne // we're the kid of an existing root } if (parentToKids.has(blockID)) { // another kid has this block as parent, (maybe) remove it and use me const otherRoot = otherRootIsKidOf(roots, blockID, parentToKids, PARENT_CHECK_DEPTH) if (otherRoot) { roots.splice(roots.indexOf(otherRoot), 1, { blockID }) DEBUG(`[useMatchTree.loadOne.match] parent of existing root, replaced`, { blockID, otherRoot, roots: [...roots] }) continue findOne } } if (blockToParents.has(blockID)) WARN(`blockID already in blockToParents`, blockID, blockToParents) // Traverse parents const toTraverse = [{ blockID, depth: 0 }] let current: ArrayType = null parentsCheck: while (current = toTraverse.shift()) { const currentBlock = useBlk(current.blockID, thread) const parentIDs = currentBlock.parentRelations.map(({ childOf }) => childOf) blockToParents.addAll(current.blockID, parentIDs) for (const parentID of parentIDs) { parentToKids.add(parentID, current.blockID) if (roots.find(({ blockID }) => blockID == parentID)) { DEBUG(`[useMatchTree.loadOne.match] parent is existing root`, parentID) continue findOne } const otherRoot = otherRootIsKidOf(roots, parentID, parentToKids, PARENT_CHECK_DEPTH) if (otherRoot) { // another kid has this parent as a parent, so use the shared parent instead roots.splice(roots.indexOf(otherRoot), 1, { blockID: parentID }) DEBUG(`[useMatchTree.loadOne.match] shared parent with existing root, replaced`, { blockID, parentID, otherRoot, roots: [...roots], }) continue findOne } // if (parentToKids.has(parentID)) { // break parentsCheck // } if (current.depth < PARENT_CHECK_DEPTH) { toTraverse.push({ blockID: parentID, depth: current.depth + 1 }) } } } roots.push({ blockID }) return true } }) loadOne() loadMore = async () => loadOne() }) return { roots, complete, loadMore: loadMore! } } export function* findMatches(thread: Thread, { order, showGenericEntities = SHOW_GENERIC_ENTITIES_IN_TIMELINE }: { order: ORDER showGenericEntities?: boolean }) { const seenEntities = new Set() for ( let i = order == 'date_asc' ? 0 : thread.applogs.length - 1; order == 'date_asc' ? i < thread.applogs.length : i >= 0; order == 'date_asc' ? i++ : i-- ) { const log = thread.applogs[i] if (!showGenericEntities) { // Original behavior: only blocks with content if (log.at != BLK.content) continue yield log.en } else { // New behavior: show all entities except relations if (seenEntities.has(log.en)) continue // Already yielded this entity if (log.at.startsWith('relation/')) continue // Skip relation entities seenEntities.add(log.en) yield log.en } } } function otherRootIsKidOf( roots: IObservableArray, parentID: string, parentToKids: SetMap, depth: number, ): false | MATCH_ROOT { const kids = parentToKids.get(parentID) if (!kids) return false for (const blockID of kids) { if (find(roots, { blockID })) { return { blockID } } if (depth > 0) { const root = otherRootIsKidOf(roots, blockID, parentToKids, depth - 1) if (root) { return root } } } return false }