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 @@
// #endregion // #endregion
// AI-NOTE: Load initial content when publicationTree becomes available // AI-NOTE: 2025-01-24 - Combined effect to handle publicationTree changes and initial loading
$effect(() => { // This prevents conflicts between separate effects that could cause duplicate loading
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
$effect(() => { $effect(() => {
if (publicationTree) { if (publicationTree) {
// Reset state when publicationTree changes
leaves = []; leaves = [];
isLoading = false; isLoading = false;
isDone = false; isDone = false;
lastElementRef = null; lastElementRef = null;
loadedAddresses = new Set(); loadedAddresses = new Set();
hasInitialized = false; 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 @@
{ threshold: 0.5 }, { threshold: 0.5 },
); );
// Only load initial content if publicationTree is available // AI-NOTE: 2025-01-24 - Removed duplicate loadMore call
if (publicationTree) { // Initial content loading is handled by the $effect that watches publicationTree
loadMore(12); // This prevents duplicate loading when both onMount and $effect trigger
}
return () => { return () => {
observer.disconnect(); observer.disconnect();

110
src/lib/data_structures/publication_tree.ts

@ -69,6 +69,12 @@ export class PublicationTree implements AsyncIterable<NDKEvent | null> {
*/ */
#bookmark?: string; #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. * The NDK instance used to fetch events.
*/ */
@ -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) { onBookmarkMoved(observer: (address: string) => void) {
this.#bookmarkMovedObservers.push(observer); this.#bookmarkMovedObservers.push(observer);
} }
@ -458,7 +496,19 @@ export class PublicationTree implements AsyncIterable<NDKEvent | null> {
if (!this.#cursor.target) { if (!this.#cursor.target) {
return { done, value: null }; 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 }; return { done, value };
} }
@ -711,6 +761,9 @@ export class PublicationTree implements AsyncIterable<NDKEvent | null> {
} }
#addNode(address: string, parentNode: PublicationTreeNode) { #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>(() => const lazyNode = new Lazy<PublicationTreeNode>(() =>
this.#resolveNode(address, parentNode) this.#resolveNode(address, parentNode)
); );
@ -961,11 +1014,10 @@ export class PublicationTree implements AsyncIterable<NDKEvent | null> {
} }
}); });
// Note: We can't await here since this is a synchronous method // AI-NOTE: 2025-01-24 - Remove e-tag processing from synchronous method
// The e-tag resolution will happen when the children are processed // E-tags should be resolved asynchronously in #resolveNode method
// For now, we'll add the e-tags as potential child addresses // Adding raw event IDs here causes duplicate processing
const eTagAddresses = eTags.map((tag) => tag[1]); console.debug(`[PublicationTree] Found ${eTags.length} e-tags but skipping processing in buildNodeFromEvent`);
childAddresses.push(...eTagAddresses);
} }
const node: PublicationTreeNode = { const node: PublicationTreeNode = {
@ -976,17 +1028,21 @@ export class PublicationTree implements AsyncIterable<NDKEvent | null> {
children: [], 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 // Add children in the order they appear in the a-tags to preserve section order
// Use sequential processing to ensure order is maintained // Use sequential processing to ensure order is maintained
console.log(`[PublicationTree] Adding ${childAddresses.length} children in order:`, childAddresses); console.log(`[PublicationTree] Adding ${childAddresses.length} children in order:`, childAddresses);
for (const address of childAddresses) { for (const childAddress of childAddresses) {
console.log(`[PublicationTree] Adding child: ${address}`); console.log(`[PublicationTree] Adding child: ${childAddress}`);
try { try {
await this.addEventByAddress(address, event); // Add the child node directly to the current node's children
console.log(`[PublicationTree] Successfully added child: ${address}`); this.#addNode(childAddress, node);
console.log(`[PublicationTree] Successfully added child: ${childAddress}`);
} catch (error) { } catch (error) {
console.warn( console.warn(
`[PublicationTree] Error adding child ${address} for ${node.address}:`, `[PublicationTree] Error adding child ${childAddress} for ${node.address}:`,
error, error,
); );
} }
@ -998,18 +1054,30 @@ export class PublicationTree implements AsyncIterable<NDKEvent | null> {
} }
#getNodeType(event: NDKEvent): PublicationTreeNodeType { #getNodeType(event: NDKEvent): PublicationTreeNodeType {
if ( // AI-NOTE: 2025-01-24 - Show nested 30040s and their zettel kind leaves
event.kind === 30040 && ( // Only 30040 events with children should be branches
event.tags.some((tag) => tag[0] === "a") || // Zettel kinds (30041, 30818, 30023) are always leaves
event.tags.some((tag) => if (event.kind === 30040) {
tag[0] === "e" && tag[1] && /^[0-9a-fA-F]{64}$/.test(tag[1]) // 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 PublicationTreeNodeType.Branch;
return hasChildren ? PublicationTreeNodeType.Branch : 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;
} }
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 // #endregion

Loading…
Cancel
Save