From 715efad96e6323d41a274810dfd839c9cd766202 Mon Sep 17 00:00:00 2001 From: silberengel Date: Wed, 6 Aug 2025 17:12:27 +0200 Subject: [PATCH] fixed kind 1111 comments and added highlights --- src/lib/components/CommentViewer.svelte | 487 +++++++++++++++++++++--- src/lib/consts.ts | 3 + src/lib/utils.ts | 27 +- src/lib/utils/event_search.ts | 64 +++- 4 files changed, 523 insertions(+), 58 deletions(-) diff --git a/src/lib/components/CommentViewer.svelte b/src/lib/components/CommentViewer.svelte index fb819e1..3b44665 100644 --- a/src/lib/components/CommentViewer.svelte +++ b/src/lib/components/CommentViewer.svelte @@ -14,6 +14,8 @@ // AI-NOTE: 2025-01-08 - Clean, efficient comment viewer implementation // This component fetches and displays threaded comments with proper hierarchy // Uses simple, reliable profile fetching and efficient state management + // AI-NOTE: 2025-01-24 - Added support for kind 9802 highlights (NIP-84) + // Highlights are displayed with special styling and include source attribution // State management let comments: NDKEvent[] = $state([]); @@ -84,32 +86,78 @@ } try { - // Try multiple filter approaches to find comments + // Build address for NIP-22 search if this is a replaceable event + let eventAddress: string | null = null; + if (event.kind && event.pubkey) { + const dTag = event.getMatchingTags("d")[0]?.[1]; + if (dTag) { + eventAddress = `${event.kind}:${event.pubkey}:${dTag}`; + } + } + + console.log(`[CommentViewer] Event address for NIP-22: ${eventAddress}`); + + // Use more targeted filters to reduce noise const filters = [ - // Standard comment filter + // Primary filter: events that explicitly reference our target via e-tags { - kinds: [1, 1111], + kinds: [1, 1111, 9802], "#e": [event.id], - }, - // Broader search for any events that might reference this event - { - kinds: [1, 1111], - "#e": [event.id], - limit: 100, - }, - // Search for events by the same author that might be replies - { - kinds: [1, 1111], - authors: [event.pubkey], - since: event.created_at ? event.created_at - 86400 : undefined, // Last 24 hours limit: 50, } ]; - console.log(`[CommentViewer] Setting up subscription with filters:`, filters); + // Add NIP-22 filter only if we have a valid event address + if (eventAddress) { + filters.push({ + kinds: [1111, 9802], + "#a": [eventAddress], + limit: 50, + } as any); + } + + console.log(`[CommentViewer] Setting up subscription with ${filters.length} filters:`, filters); + + // Debug: Check if the provided event would match our filters + console.log(`[CommentViewer] Debug: Checking if event b9a15298f2b203d42ba6d0c56c43def87efc887697460c0febb9542515d5a00b would match our filters`); + console.log(`[CommentViewer] Debug: Target event ID: ${event.id}`); + console.log(`[CommentViewer] Debug: Event address: ${eventAddress}`); + + // Get all available relays for a more comprehensive search + // Use the full NDK pool relays instead of just active relays + const ndkPoolRelays = Array.from($ndkInstance.pool.relays.values()).map(relay => relay.url); + console.log(`[CommentViewer] Using ${ndkPoolRelays.length} NDK pool relays for search:`, ndkPoolRelays); + + // Try all filters to find comments with full relay set + activeSub = $ndkInstance.subscribe(filters); - // Try the first filter (standard comment search) - activeSub = $ndkInstance.subscribe(filters[0]); + // Also try a direct search for the specific comment we're looking for + console.log(`[CommentViewer] Also searching for specific comment: 64173a81c2a8e26342d4a75d3def804da8644377bde99cfdfeaf189dff87f942`); + const specificCommentSub = $ndkInstance.subscribe({ + ids: ["64173a81c2a8e26342d4a75d3def804da8644377bde99cfdfeaf189dff87f942"] + }); + + specificCommentSub.on("event", (specificEvent: NDKEvent) => { + console.log(`[CommentViewer] Found specific comment via direct search:`, specificEvent.id); + console.log(`[CommentViewer] Specific comment tags:`, specificEvent.tags); + + // Check if this specific comment references our target + const eTags = specificEvent.getMatchingTags("e"); + const aTags = specificEvent.getMatchingTags("a"); + console.log(`[CommentViewer] Specific comment e-tags:`, eTags.map(t => t[1])); + console.log(`[CommentViewer] Specific comment a-tags:`, aTags.map(t => t[1])); + + const hasETag = eTags.some(tag => tag[1] === event.id); + const hasATag = eventAddress ? aTags.some(tag => tag[1] === eventAddress) : false; + + console.log(`[CommentViewer] Specific comment has matching e-tag: ${hasETag}`); + console.log(`[CommentViewer] Specific comment has matching a-tag: ${hasATag}`); + }); + + specificCommentSub.on("eose", () => { + console.log(`[CommentViewer] Specific comment search EOSE`); + specificCommentSub.stop(); + }); const timeout = setTimeout(() => { console.log(`[CommentViewer] Subscription timeout - no comments found`); @@ -126,16 +174,54 @@ console.log(`[CommentViewer] Comment pubkey: ${commentEvent.pubkey}`); console.log(`[CommentViewer] Comment content preview: ${commentEvent.content?.slice(0, 100)}...`); + // Special debug for the specific comment we're looking for + if (commentEvent.id === "64173a81c2a8e26342d4a75d3def804da8644377bde99cfdfeaf189dff87f942") { + console.log(`[CommentViewer] DEBUG: Found the specific comment we're looking for!`); + console.log(`[CommentViewer] DEBUG: Comment tags:`, commentEvent.tags); + } + // Check if this event actually references our target event + let referencesTarget = false; + let referenceMethod = ""; + + // Check e-tags (standard format) const eTags = commentEvent.getMatchingTags("e"); - const referencesTarget = eTags.some(tag => tag[1] === event.id); + console.log(`[CommentViewer] Checking e-tags:`, eTags.map(t => t[1])); + console.log(`[CommentViewer] Target event ID: ${event.id}`); + const hasETag = eTags.some(tag => tag[1] === event.id); + console.log(`[CommentViewer] Has matching e-tag: ${hasETag}`); + if (hasETag) { + referencesTarget = true; + referenceMethod = "e-tag"; + } + + // Check a-tags (NIP-22 format) if not found via e-tags + if (!referencesTarget && eventAddress) { + const aTags = commentEvent.getMatchingTags("a"); + console.log(`[CommentViewer] Checking a-tags:`, aTags.map(t => t[1])); + console.log(`[CommentViewer] Expected a-tag: ${eventAddress}`); + const hasATag = aTags.some(tag => tag[1] === eventAddress); + console.log(`[CommentViewer] Has matching a-tag: ${hasATag}`); + if (hasATag) { + referencesTarget = true; + referenceMethod = "a-tag"; + } + } if (referencesTarget) { - console.log(`[CommentViewer] Comment references target event - adding to comments`); + console.log(`[CommentViewer] Comment references target event via ${referenceMethod} - adding to comments`); comments = [...comments, commentEvent]; fetchProfile(commentEvent.pubkey); + + // Fetch nested replies for this comment + fetchNestedReplies(commentEvent.id); } else { console.log(`[CommentViewer] Comment does not reference target event - skipping`); + console.log(`[CommentViewer] e-tags:`, eTags.map(t => t[1])); + if (eventAddress) { + console.log(`[CommentViewer] a-tags:`, commentEvent.getMatchingTags("a").map(t => t[1])); + console.log(`[CommentViewer] expected a-tag:`, eventAddress); + } } }); @@ -151,6 +237,11 @@ // Pre-fetch all profiles after comments are loaded preFetchAllProfiles(); + // AI-NOTE: 2025-01-24 - Fetch nested replies for all found comments + comments.forEach(comment => { + fetchNestedReplies(comment.id); + }); + // AI-NOTE: 2025-01-24 - Test for comments if none were found if (comments.length === 0) { testForComments(); @@ -193,25 +284,35 @@ console.log(`[CommentViewer] Pre-fetching complete`); } - // AI-NOTE: 2025-01-24 - Function to manually test for comments - async function testForComments() { - if (!event?.id) return; - - console.log(`[CommentViewer] Testing for comments on event: ${event.id}`); - - try { - // Try a broader search to see if there are any events that might be comments - const testSub = $ndkInstance.subscribe({ - kinds: [1, 1111], - "#e": [event.id], - limit: 10, - }); + // AI-NOTE: 2025-01-24 - Function to manually test for comments + async function testForComments() { + if (!event?.id) return; + + console.log(`[CommentViewer] Testing for comments on event: ${event.id}`); + + try { + // Try a broader search to see if there are any events that might be comments + const testSub = $ndkInstance.subscribe({ + kinds: [1, 1111, 9802], + "#e": [event.id], + limit: 10, + }); let testComments = 0; testSub.on("event", (testEvent: NDKEvent) => { testComments++; - console.log(`[CommentViewer] Test found event: ${testEvent.id}, kind: ${testEvent.kind}`); + console.log(`[CommentViewer] Test found event: ${testEvent.id}, kind: ${testEvent.kind}, content: ${testEvent.content?.slice(0, 50)}...`); + + // Special debug for the specific comment we're looking for + if (testEvent.id === "64173a81c2a8e26342d4a75d3def804da8644377bde99cfdfeaf189dff87f942") { + console.log(`[CommentViewer] DEBUG: Test found the specific comment we're looking for!`); + console.log(`[CommentViewer] DEBUG: Test comment tags:`, testEvent.tags); + } + + // Show the e-tags to help debug + const eTags = testEvent.getMatchingTags("e"); + console.log(`[CommentViewer] Test event e-tags:`, eTags.map(t => t[1])); }); testSub.on("eose", () => { @@ -266,12 +367,36 @@ } } } else if (event.kind === 1111) { - // Kind 1111: Look for lowercase e-tags (immediate parent) - for (const tag of eTags) { - const referencedId = tag[1]; - if (eventMap.has(referencedId) && referencedId !== event.id) { - parentId = referencedId; - break; + // Kind 1111: Use NIP-22 threading format + // First try to find parent using 'a' tags (NIP-22 parent scope) + const aTags = event.getMatchingTags("a"); + for (const tag of aTags) { + const address = tag[1]; + // Extract event ID from address if it's a coordinate + const parts = address.split(":"); + if (parts.length >= 3) { + const [kind, pubkey, dTag] = parts; + // Look for the parent event with this address + for (const [eventId, parentEvent] of eventMap) { + if (parentEvent.kind === parseInt(kind) && + parentEvent.pubkey === pubkey && + parentEvent.getMatchingTags("d")[0]?.[1] === dTag) { + parentId = eventId; + break; + } + } + if (parentId) break; + } + } + + // Fallback to 'e' tags if no parent found via 'a' tags + if (!parentId) { + for (const tag of eTags) { + const referencedId = tag[1]; + if (eventMap.has(referencedId) && referencedId !== event.id) { + parentId = referencedId; + break; + } } } } @@ -310,6 +435,7 @@ // Fetch comments when event changes $effect(() => { if (event?.id) { + console.log(`[CommentViewer] Event changed, fetching comments for:`, event.id, `kind:`, event.kind); if (activeSub) { activeSub.stop(); activeSub = null; @@ -318,6 +444,110 @@ } }); + // AI-NOTE: 2025-01-24 - Add recursive comment fetching for nested replies + let isFetchingNestedReplies = $state(false); + let nestedReplyIds = $state>(new Set()); + + // Function to fetch nested replies for a given event + async function fetchNestedReplies(eventId: string) { + if (isFetchingNestedReplies || nestedReplyIds.has(eventId)) { + console.log(`[CommentViewer] Skipping nested reply fetch for ${eventId} - already fetching or processed`); + return; + } + + console.log(`[CommentViewer] Starting nested reply fetch for event: ${eventId}`); + isFetchingNestedReplies = true; + nestedReplyIds.add(eventId); + + try { + console.log(`[CommentViewer] Fetching nested replies for event:`, eventId); + + // Search for replies to this specific event + const nestedSub = $ndkInstance.subscribe({ + kinds: [1, 1111, 9802], + "#e": [eventId], + limit: 50, + }); + + let nestedCount = 0; + + nestedSub.on("event", (nestedEvent: NDKEvent) => { + console.log(`[CommentViewer] Found nested reply:`, nestedEvent.id, `kind:`, nestedEvent.kind); + + // Check if this event actually references the target event + const eTags = nestedEvent.getMatchingTags("e"); + const referencesTarget = eTags.some(tag => tag[1] === eventId); + + console.log(`[CommentViewer] Nested reply references target:`, referencesTarget, `eTags:`, eTags); + + if (referencesTarget && !comments.some(c => c.id === nestedEvent.id)) { + console.log(`[CommentViewer] Adding nested reply to comments`); + comments = [...comments, nestedEvent]; + fetchProfile(nestedEvent.pubkey); + + // Recursively fetch replies to this nested reply + fetchNestedReplies(nestedEvent.id); + } else if (!referencesTarget) { + console.log(`[CommentViewer] Nested reply does not reference target, skipping`); + } else { + console.log(`[CommentViewer] Nested reply already exists in comments`); + } + }); + + nestedSub.on("eose", () => { + console.log(`[CommentViewer] Nested replies EOSE, found ${nestedCount} replies`); + nestedSub.stop(); + isFetchingNestedReplies = false; + }); + + // Also search for NIP-22 format nested replies + const event = comments.find(c => c.id === eventId); + if (event && event.kind && event.pubkey) { + const dTag = event.getMatchingTags("d")[0]?.[1]; + if (dTag) { + const eventAddress = `${event.kind}:${event.pubkey}:${dTag}`; + + const nip22Sub = $ndkInstance.subscribe({ + kinds: [1111, 9802], + "#a": [eventAddress], + limit: 50, + }); + + nip22Sub.on("event", (nip22Event: NDKEvent) => { + console.log(`[CommentViewer] Found NIP-22 nested reply:`, nip22Event.id, `kind:`, nip22Event.kind); + + const aTags = nip22Event.getMatchingTags("a"); + const referencesTarget = aTags.some(tag => tag[1] === eventAddress); + + console.log(`[CommentViewer] NIP-22 nested reply references target:`, referencesTarget, `aTags:`, aTags, `eventAddress:`, eventAddress); + + if (referencesTarget && !comments.some(c => c.id === nip22Event.id)) { + console.log(`[CommentViewer] Adding NIP-22 nested reply to comments`); + comments = [...comments, nip22Event]; + fetchProfile(nip22Event.pubkey); + + // Recursively fetch replies to this nested reply + fetchNestedReplies(nip22Event.id); + } else if (!referencesTarget) { + console.log(`[CommentViewer] NIP-22 nested reply does not reference target, skipping`); + } else { + console.log(`[CommentViewer] NIP-22 nested reply already exists in comments`); + } + }); + + nip22Sub.on("eose", () => { + console.log(`[CommentViewer] NIP-22 nested replies EOSE`); + nip22Sub.stop(); + }); + } + } + + } catch (err) { + console.error(`[CommentViewer] Error fetching nested replies:`, err); + isFetchingNestedReplies = false; + } + } + // Cleanup on unmount onMount(() => { return () => { @@ -330,15 +560,31 @@ // Navigation functions function getNeventUrl(commentEvent: NDKEvent): string { - return neventEncode(commentEvent, $activeInboxRelays); + try { + console.log(`[CommentViewer] Generating nevent for:`, commentEvent.id, `kind:`, commentEvent.kind); + const nevent = neventEncode(commentEvent, $activeInboxRelays); + console.log(`[CommentViewer] Generated nevent:`, nevent); + return nevent; + } catch (error) { + console.error(`[CommentViewer] Error generating nevent:`, error); + // Fallback to just the event ID + return commentEvent.id; + } } // AI-NOTE: 2025-01-24 - View button functionality is working correctly // This function navigates to the specific event as the main event, allowing // users to view replies as the primary content function navigateToComment(commentEvent: NDKEvent) { - const nevent = getNeventUrl(commentEvent); - goto(`/events?id=${encodeURIComponent(nevent)}`); + try { + const nevent = getNeventUrl(commentEvent); + console.log(`[CommentViewer] Navigating to comment:`, nevent); + goto(`/events?id=${encodeURIComponent(nevent)}`); + } catch (error) { + console.error(`[CommentViewer] Error navigating to comment:`, error); + // Fallback to event ID + goto(`/events?id=${commentEvent.id}`); + } } // Utility functions @@ -414,6 +660,39 @@ return parsedContent; } + + + + // AI-NOTE: 2025-01-24 - Get highlight source information + function getHighlightSource(highlightEvent: NDKEvent): { type: string; value: string; url?: string } | null { + // Check for e-tags (nostr events) + const eTags = highlightEvent.getMatchingTags("e"); + if (eTags.length > 0) { + return { type: "nostr_event", value: eTags[0][1] }; + } + + // Check for r-tags (URLs) + const rTags = highlightEvent.getMatchingTags("r"); + if (rTags.length > 0) { + return { type: "url", value: rTags[0][1], url: rTags[0][1] }; + } + + return null; + } + + // AI-NOTE: 2025-01-24 - Get highlight attribution + function getHighlightAttribution(highlightEvent: NDKEvent): Array<{ pubkey: string; role?: string }> { + const pTags = highlightEvent.getMatchingTags("p"); + return pTags.map(tag => ({ + pubkey: tag[1], + role: tag[3] || undefined + })); + } + + // AI-NOTE: 2025-01-24 - Check if highlight has comment + function hasHighlightComment(highlightEvent: NDKEvent): boolean { + return highlightEvent.getMatchingTags("comment").length > 0; + } @@ -474,11 +753,84 @@
- {#await parseContent(node.event.content || "") then parsedContent} - {@html parsedContent} - {:catch} - {@html node.event.content || ""} - {/await} + {#if node.event.kind === 9802} + +
+ {#if hasHighlightComment(node.event)} + +
+
+ Highlighted content: +
+ {#if node.event.getMatchingTags("context")[0]?.[1]} +
+ {@html node.event.getMatchingTags("context")[0]?.[1]} +
+ {:else} +
+ {node.event.content || ""} +
+ {/if} + {#if getHighlightSource(node.event)} +
+ Source: {getHighlightSource(node.event)?.type === 'nostr_event' ? 'Nostr Event' : 'URL'} +
+ {/if} +
+
+
+ Comment: +
+ {#await parseContent(node.event.getMatchingTags("comment")[0]?.[1] || "") then parsedContent} + {@html parsedContent} + {:catch} + {@html node.event.getMatchingTags("comment")[0]?.[1] || ""} + {/await} +
+ {:else} + + {#if node.event.getMatchingTags("context")[0]?.[1]} +
+ {@html node.event.getMatchingTags("context")[0]?.[1]} +
+ {:else} +
+ {node.event.content || ""} +
+ {/if} + + {#if getHighlightSource(node.event)} +
+ Source: {getHighlightSource(node.event)?.type === 'nostr_event' ? 'Nostr Event' : 'URL'} +
+ {/if} + {/if} + + {#if getHighlightAttribution(node.event).length > 0} +
+ Attribution: + {#each getHighlightAttribution(node.event) as attribution} + + {/each} +
+ {/if} +
+ {:else} + + {#await parseContent(node.event.content || "") then parsedContent} + {@html parsedContent} + {:catch} + {@html node.event.content || ""} + {/await} + {/if}
@@ -494,7 +846,7 @@
- Comments ({threadedComments.length}) + Comments & Highlights ({threadedComments.length}) {#if loading} @@ -507,7 +859,7 @@
{:else if threadedComments.length === 0}
-

No comments yet. Be the first to comment!

+

No comments or highlights yet. Be the first to engage!

{:else}
@@ -516,4 +868,37 @@ {/each}
{/if} - \ No newline at end of file + + + \ No newline at end of file diff --git a/src/lib/consts.ts b/src/lib/consts.ts index 90afa53..29f4502 100644 --- a/src/lib/consts.ts +++ b/src/lib/consts.ts @@ -14,6 +14,9 @@ export const searchRelays = [ "wss://aggr.nostr.land", "wss://relay.noswhere.com", "wss://nostr.wine", + "wss://relay.damus.io", + "wss://relay.nostr.band", + "wss://freelay.sovbit.host" ]; export const secondaryRelays = [ diff --git a/src/lib/utils.ts b/src/lib/utils.ts index ee44929..18fad03 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -19,12 +19,27 @@ export class InvalidKindError extends DecodeError { } export function neventEncode(event: NDKEvent, relays: string[]) { - return nip19.neventEncode({ - id: event.id, - kind: event.kind, - relays, - author: event.pubkey, - }); + try { + console.log(`[neventEncode] Encoding event:`, { + id: event.id, + kind: event.kind, + pubkey: event.pubkey, + relayCount: relays.length + }); + + const nevent = nip19.neventEncode({ + id: event.id, + kind: event.kind, + relays, + author: event.pubkey, + }); + + console.log(`[neventEncode] Generated nevent:`, nevent); + return nevent; + } catch (error) { + console.error(`[neventEncode] Error encoding nevent:`, error); + throw error; + } } export function naddrEncode(event: NDKEvent, relays: string[]) { diff --git a/src/lib/utils/event_search.ts b/src/lib/utils/event_search.ts index 1d5537d..f15b9b3 100644 --- a/src/lib/utils/event_search.ts +++ b/src/lib/utils/event_search.ts @@ -1,5 +1,5 @@ import { ndkInstance } from "../ndk.ts"; -import { fetchEventWithFallback } from "./nostrUtils.ts"; +import { fetchEventWithFallback, NDKRelaySetFromNDK } from "./nostrUtils.ts"; import { nip19 } from "nostr-tools"; import { NDKEvent } from "@nostr-dev-kit/ndk"; import type { Filter } from "./search_types.ts"; @@ -11,6 +11,26 @@ import { TIMEOUTS, VALIDATION } from "./search_constants.ts"; * Search for a single event by ID or filter */ export async function searchEvent(query: string): Promise { + const ndk = get(ndkInstance); + if (!ndk) { + console.warn("[Search] No NDK instance available"); + return null; + } + + // Wait for relays to be available + let attempts = 0; + const maxAttempts = 10; + while (ndk.pool.relays.size === 0 && attempts < maxAttempts) { + console.log(`[Search] Waiting for relays to be available (attempt ${attempts + 1}/${maxAttempts})`); + await new Promise(resolve => setTimeout(resolve, 500)); + attempts++; + } + + if (ndk.pool.relays.size === 0) { + console.warn("[Search] No relays available after waiting"); + return null; + } + // Clean the query and normalize to lowercase const cleanedQuery = query.replace(/^nostr:/, "").toLowerCase(); let filterOrId: Filter | string = cleanedQuery; @@ -51,8 +71,50 @@ export async function searchEvent(query: string): Promise { try { const decoded = nip19.decode(cleanedQuery); if (!decoded) throw new Error("Invalid identifier"); + + console.log(`[Search] Decoded identifier:`, { + type: decoded.type, + data: decoded.data, + query: cleanedQuery + }); + switch (decoded.type) { case "nevent": + console.log(`[Search] Processing nevent:`, { + id: decoded.data.id, + kind: decoded.data.kind, + relays: decoded.data.relays + }); + + // Use the relays from the nevent if available + if (decoded.data.relays && decoded.data.relays.length > 0) { + console.log(`[Search] Using relays from nevent:`, decoded.data.relays); + + // Try to fetch the event using the nevent's relays + try { + // Create a temporary relay set for this search + const neventRelaySet = NDKRelaySetFromNDK.fromRelayUrls(decoded.data.relays, ndk); + + if (neventRelaySet.relays.size > 0) { + console.log(`[Search] Created relay set with ${neventRelaySet.relays.size} relays from nevent`); + + // Try to fetch the event using the nevent's relays + const event = await ndk + .fetchEvent({ ids: [decoded.data.id] }, undefined, neventRelaySet) + .withTimeout(TIMEOUTS.EVENT_FETCH); + + if (event) { + console.log(`[Search] Found event using nevent relays:`, event.id); + return event; + } else { + console.log(`[Search] Event not found on nevent relays, trying default relays`); + } + } + } catch (error) { + console.warn(`[Search] Error fetching from nevent relays:`, error); + } + } + filterOrId = decoded.data.id; break; case "note":