Browse Source

display nested publications

master
silberengel 7 months ago
parent
commit
f0400533ea
  1. 34
      src/lib/components/publications/Publication.svelte
  2. 110
      src/lib/data_structures/publication_tree.ts

34
src/lib/components/publications/Publication.svelte

@ -109,24 +109,31 @@ @@ -109,24 +109,31 @@
// #endregion
// AI-NOTE: Load initial content when publicationTree becomes available
$effect(() => {
if (publicationTree && leaves.length === 0 && !isLoading && !isDone && !hasInitialized) {
console.log("[Publication] Loading initial content");
hasInitialized = true;
loadMore(12);
}
});
// AI-NOTE: Reset state when publicationTree changes
// AI-NOTE: 2025-01-24 - Combined effect to handle publicationTree changes and initial loading
// This prevents conflicts between separate effects that could cause duplicate loading
$effect(() => {
if (publicationTree) {
// Reset state when publicationTree changes
leaves = [];
isLoading = false;
isDone = false;
lastElementRef = null;
loadedAddresses = new Set();
hasInitialized = false;
// Reset the publication tree iterator to prevent duplicate events
if (typeof publicationTree.resetIterator === 'function') {
publicationTree.resetIterator();
}
// AI-NOTE: 2025-01-24 - Use setTimeout to ensure iterator reset completes before loading
// This prevents race conditions where loadMore is called before the iterator is fully reset
setTimeout(() => {
// Load initial content after reset
console.log("[Publication] Loading initial content after reset");
hasInitialized = true;
loadMore(12);
}, 0);
}
});
@ -228,10 +235,9 @@ @@ -228,10 +235,9 @@
{ threshold: 0.5 },
);
// Only load initial content if publicationTree is available
if (publicationTree) {
loadMore(12);
}
// AI-NOTE: 2025-01-24 - Removed duplicate loadMore call
// Initial content loading is handled by the $effect that watches publicationTree
// This prevents duplicate loading when both onMount and $effect trigger
return () => {
observer.disconnect();

110
src/lib/data_structures/publication_tree.ts

@ -69,6 +69,12 @@ export class PublicationTree implements AsyncIterable<NDKEvent | null> { @@ -69,6 +69,12 @@ export class PublicationTree implements AsyncIterable<NDKEvent | null> {
*/
#bookmark?: string;
/**
* AI-NOTE: 2025-01-24 - Track visited nodes to prevent duplicate iteration
* This ensures that each node is only yielded once during iteration
*/
#visitedNodes: Set<string> = new Set();
/**
* The NDK instance used to fetch events.
*/
@ -227,6 +233,38 @@ export class PublicationTree implements AsyncIterable<NDKEvent | null> { @@ -227,6 +233,38 @@ export class PublicationTree implements AsyncIterable<NDKEvent | null> {
});
}
/**
* AI-NOTE: 2025-01-24 - Reset the cursor to the beginning of the tree
* This is useful when the component state is reset and we want to start iteration from the beginning
*/
resetCursor() {
this.#bookmark = undefined;
this.#cursor.target = null;
}
/**
* AI-NOTE: 2025-01-24 - Reset the iterator state to start from the beginning
* This ensures that when the component resets, the iterator starts fresh
*/
resetIterator() {
this.resetCursor();
// Clear visited nodes to allow fresh iteration
this.#visitedNodes.clear();
// Clear all nodes except the root to force fresh loading
const rootAddress = this.#root.address;
this.#nodes.clear();
this.#nodes.set(rootAddress, new Lazy<PublicationTreeNode>(() => Promise.resolve(this.#root)));
// Clear events cache to ensure fresh data
this.#events.clear();
this.#eventCache.clear();
// Force the cursor to move to the root node to restart iteration
this.#cursor.tryMoveTo().then((success) => {
if (!success) {
console.warn("[PublicationTree] Failed to reset iterator to root node");
}
});
}
onBookmarkMoved(observer: (address: string) => void) {
this.#bookmarkMovedObservers.push(observer);
}
@ -458,7 +496,19 @@ export class PublicationTree implements AsyncIterable<NDKEvent | null> { @@ -458,7 +496,19 @@ export class PublicationTree implements AsyncIterable<NDKEvent | null> {
if (!this.#cursor.target) {
return { done, value: null };
}
const value = (await this.getEvent(this.#cursor.target.address)) ?? null;
const address = this.#cursor.target.address;
// AI-NOTE: 2025-01-24 - Check if this node has already been visited
if (this.#visitedNodes.has(address)) {
console.debug(`[PublicationTree] Skipping already visited node: ${address}`);
return { done: false, value: null };
}
// Mark this node as visited
this.#visitedNodes.add(address);
const value = (await this.getEvent(address)) ?? null;
return { done, value };
}
@ -711,6 +761,9 @@ export class PublicationTree implements AsyncIterable<NDKEvent | null> { @@ -711,6 +761,9 @@ export class PublicationTree implements AsyncIterable<NDKEvent | null> {
}
#addNode(address: string, parentNode: PublicationTreeNode) {
// AI-NOTE: 2025-01-24 - Add debugging to track node addition
console.debug(`[PublicationTree] Adding node ${address} to parent ${parentNode.address}`);
const lazyNode = new Lazy<PublicationTreeNode>(() =>
this.#resolveNode(address, parentNode)
);
@ -961,11 +1014,10 @@ export class PublicationTree implements AsyncIterable<NDKEvent | null> { @@ -961,11 +1014,10 @@ export class PublicationTree implements AsyncIterable<NDKEvent | null> {
}
});
// Note: We can't await here since this is a synchronous method
// The e-tag resolution will happen when the children are processed
// For now, we'll add the e-tags as potential child addresses
const eTagAddresses = eTags.map((tag) => tag[1]);
childAddresses.push(...eTagAddresses);
// AI-NOTE: 2025-01-24 - Remove e-tag processing from synchronous method
// E-tags should be resolved asynchronously in #resolveNode method
// Adding raw event IDs here causes duplicate processing
console.debug(`[PublicationTree] Found ${eTags.length} e-tags but skipping processing in buildNodeFromEvent`);
}
const node: PublicationTreeNode = {
@ -976,17 +1028,21 @@ export class PublicationTree implements AsyncIterable<NDKEvent | null> { @@ -976,17 +1028,21 @@ export class PublicationTree implements AsyncIterable<NDKEvent | null> {
children: [],
};
// AI-NOTE: 2025-01-24 - Fixed child node addition in buildNodeFromEvent
// Previously called addEventByAddress which expected parent to be in tree
// Now directly adds child nodes to current node's children array
// Add children in the order they appear in the a-tags to preserve section order
// Use sequential processing to ensure order is maintained
console.log(`[PublicationTree] Adding ${childAddresses.length} children in order:`, childAddresses);
for (const address of childAddresses) {
console.log(`[PublicationTree] Adding child: ${address}`);
for (const childAddress of childAddresses) {
console.log(`[PublicationTree] Adding child: ${childAddress}`);
try {
await this.addEventByAddress(address, event);
console.log(`[PublicationTree] Successfully added child: ${address}`);
// Add the child node directly to the current node's children
this.#addNode(childAddress, node);
console.log(`[PublicationTree] Successfully added child: ${childAddress}`);
} catch (error) {
console.warn(
`[PublicationTree] Error adding child ${address} for ${node.address}:`,
`[PublicationTree] Error adding child ${childAddress} for ${node.address}:`,
error,
);
}
@ -998,18 +1054,30 @@ export class PublicationTree implements AsyncIterable<NDKEvent | null> { @@ -998,18 +1054,30 @@ export class PublicationTree implements AsyncIterable<NDKEvent | null> {
}
#getNodeType(event: NDKEvent): PublicationTreeNodeType {
if (
event.kind === 30040 && (
event.tags.some((tag) => tag[0] === "a") ||
event.tags.some((tag) =>
tag[0] === "e" && tag[1] && /^[0-9a-fA-F]{64}$/.test(tag[1])
)
)
) {
return PublicationTreeNodeType.Branch;
// AI-NOTE: 2025-01-24 - Show nested 30040s and their zettel kind leaves
// Only 30040 events with children should be branches
// Zettel kinds (30041, 30818, 30023) are always leaves
if (event.kind === 30040) {
// Check if this 30040 has any children (a-tags only, since e-tags are handled separately)
const hasChildren = event.tags.some((tag) => tag[0] === "a");
console.debug(`[PublicationTree] Node type for ${event.kind}:${event.pubkey}:${event.tags.find(t => t[0] === 'd')?.[1]} - hasChildren: ${hasChildren}, type: ${hasChildren ? 'Branch' : 'Leaf'}`);
return hasChildren ? PublicationTreeNodeType.Branch : PublicationTreeNodeType.Leaf;
}
return PublicationTreeNodeType.Leaf;
// Zettel kinds are always leaves
if ([30041, 30818, 30023].includes(event.kind)) {
console.debug(`[PublicationTree] Node type for ${event.kind}:${event.pubkey}:${event.tags.find(t => t[0] === 'd')?.[1]} - Zettel kind, type: Leaf`);
return PublicationTreeNodeType.Leaf;
}
// For other kinds, check if they have children (a-tags only)
const hasChildren = event.tags.some((tag) => tag[0] === "a");
console.debug(`[PublicationTree] Node type for ${event.kind}:${event.pubkey}:${event.tags.find(t => t[0] === 'd')?.[1]} - hasChildren: ${hasChildren}, type: ${hasChildren ? 'Branch' : 'Leaf'}`);
return hasChildren ? PublicationTreeNodeType.Branch : PublicationTreeNodeType.Leaf;
}
// #endregion

Loading…
Cancel
Save