|
|
|
|
@ -14,6 +14,8 @@
@@ -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 @@
@@ -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}`); |
|
|
|
|
|
|
|
|
|
// Try the first filter (standard comment search) |
|
|
|
|
activeSub = $ndkInstance.subscribe(filters[0]); |
|
|
|
|
// 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); |
|
|
|
|
|
|
|
|
|
// 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 @@
@@ -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 @@
@@ -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 @@
@@ -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; |
|
|
|
|
// 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}`); |
|
|
|
|
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, |
|
|
|
|
}); |
|
|
|
|
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 @@
@@ -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 @@
@@ -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 @@
@@ -318,6 +444,110 @@
|
|
|
|
|
} |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
// AI-NOTE: 2025-01-24 - Add recursive comment fetching for nested replies |
|
|
|
|
let isFetchingNestedReplies = $state(false); |
|
|
|
|
let nestedReplyIds = $state<Set<string>>(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 @@
@@ -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 @@
@@ -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; |
|
|
|
|
} |
|
|
|
|
</script> |
|
|
|
|
|
|
|
|
|
<!-- Recursive Comment Item Component --> |
|
|
|
|
@ -474,11 +753,84 @@
@@ -474,11 +753,84 @@
|
|
|
|
|
</div> |
|
|
|
|
|
|
|
|
|
<div class="text-gray-800 dark:text-gray-200 whitespace-pre-wrap break-words overflow-hidden"> |
|
|
|
|
{#await parseContent(node.event.content || "") then parsedContent} |
|
|
|
|
{@html parsedContent} |
|
|
|
|
{:catch} |
|
|
|
|
{@html node.event.content || ""} |
|
|
|
|
{/await} |
|
|
|
|
{#if node.event.kind === 9802} |
|
|
|
|
<!-- Highlight rendering --> |
|
|
|
|
<div class="highlight-container bg-yellow-50 dark:bg-yellow-900/20 border-l-4 border-yellow-400 p-3 rounded-r"> |
|
|
|
|
{#if hasHighlightComment(node.event)} |
|
|
|
|
<!-- Quote highlight with comment --> |
|
|
|
|
<div class="highlight-quote bg-gray-50 dark:bg-gray-800 p-3 rounded mb-3 border-l-4 border-blue-400"> |
|
|
|
|
<div class="text-sm text-gray-600 dark:text-gray-400 mb-2"> |
|
|
|
|
<span class="font-medium">Highlighted content:</span> |
|
|
|
|
</div> |
|
|
|
|
{#if node.event.getMatchingTags("context")[0]?.[1]} |
|
|
|
|
<div class="highlight-context"> |
|
|
|
|
{@html node.event.getMatchingTags("context")[0]?.[1]} |
|
|
|
|
</div> |
|
|
|
|
{:else} |
|
|
|
|
<div class="highlight-content text-gray-800 dark:text-gray-200"> |
|
|
|
|
{node.event.content || ""} |
|
|
|
|
</div> |
|
|
|
|
{/if} |
|
|
|
|
{#if getHighlightSource(node.event)} |
|
|
|
|
<div class="text-xs text-gray-500 dark:text-gray-400 mt-2"> |
|
|
|
|
Source: {getHighlightSource(node.event)?.type === 'nostr_event' ? 'Nostr Event' : 'URL'} |
|
|
|
|
</div> |
|
|
|
|
{/if} |
|
|
|
|
</div> |
|
|
|
|
<div class="highlight-comment"> |
|
|
|
|
<div class="text-sm text-gray-600 dark:text-gray-400 mb-2"> |
|
|
|
|
<span class="font-medium">Comment:</span> |
|
|
|
|
</div> |
|
|
|
|
{#await parseContent(node.event.getMatchingTags("comment")[0]?.[1] || "") then parsedContent} |
|
|
|
|
{@html parsedContent} |
|
|
|
|
{:catch} |
|
|
|
|
{@html node.event.getMatchingTags("comment")[0]?.[1] || ""} |
|
|
|
|
{/await} |
|
|
|
|
</div> |
|
|
|
|
{:else} |
|
|
|
|
<!-- Simple highlight --> |
|
|
|
|
{#if node.event.getMatchingTags("context")[0]?.[1]} |
|
|
|
|
<div class="highlight-context"> |
|
|
|
|
{@html node.event.getMatchingTags("context")[0]?.[1]} |
|
|
|
|
</div> |
|
|
|
|
{:else} |
|
|
|
|
<div class="highlight-content"> |
|
|
|
|
{node.event.content || ""} |
|
|
|
|
</div> |
|
|
|
|
{/if} |
|
|
|
|
|
|
|
|
|
{#if getHighlightSource(node.event)} |
|
|
|
|
<div class="text-xs text-gray-500 dark:text-gray-400 mt-2"> |
|
|
|
|
Source: {getHighlightSource(node.event)?.type === 'nostr_event' ? 'Nostr Event' : 'URL'} |
|
|
|
|
</div> |
|
|
|
|
{/if} |
|
|
|
|
{/if} |
|
|
|
|
|
|
|
|
|
{#if getHighlightAttribution(node.event).length > 0} |
|
|
|
|
<div class="text-xs text-gray-500 dark:text-gray-400 mt-2"> |
|
|
|
|
<span class="font-medium">Attribution:</span> |
|
|
|
|
{#each getHighlightAttribution(node.event) as attribution} |
|
|
|
|
<button |
|
|
|
|
class="ml-1 text-blue-600 dark:text-blue-400 hover:underline cursor-pointer" |
|
|
|
|
onclick={() => goto(`/events?n=${toNpub(attribution.pubkey)}`)} |
|
|
|
|
> |
|
|
|
|
{getAuthorName(attribution.pubkey)} |
|
|
|
|
{#if attribution.role} |
|
|
|
|
<span class="text-gray-400">({attribution.role})</span> |
|
|
|
|
{/if} |
|
|
|
|
</button> |
|
|
|
|
{/each} |
|
|
|
|
</div> |
|
|
|
|
{/if} |
|
|
|
|
</div> |
|
|
|
|
{:else} |
|
|
|
|
<!-- Regular comment content --> |
|
|
|
|
{#await parseContent(node.event.content || "") then parsedContent} |
|
|
|
|
{@html parsedContent} |
|
|
|
|
{:catch} |
|
|
|
|
{@html node.event.content || ""} |
|
|
|
|
{/await} |
|
|
|
|
{/if} |
|
|
|
|
</div> |
|
|
|
|
</div> |
|
|
|
|
|
|
|
|
|
@ -494,7 +846,7 @@
@@ -494,7 +846,7 @@
|
|
|
|
|
|
|
|
|
|
<div class="mt-6"> |
|
|
|
|
<Heading tag="h3" class="h-leather mb-4"> |
|
|
|
|
Comments ({threadedComments.length}) |
|
|
|
|
Comments & Highlights ({threadedComments.length}) |
|
|
|
|
</Heading> |
|
|
|
|
|
|
|
|
|
{#if loading} |
|
|
|
|
@ -507,7 +859,7 @@
@@ -507,7 +859,7 @@
|
|
|
|
|
</div> |
|
|
|
|
{:else if threadedComments.length === 0} |
|
|
|
|
<div class="text-center py-4"> |
|
|
|
|
<P class="text-gray-500">No comments yet. Be the first to comment!</P> |
|
|
|
|
<P class="text-gray-500">No comments or highlights yet. Be the first to engage!</P> |
|
|
|
|
</div> |
|
|
|
|
{:else} |
|
|
|
|
<div class="space-y-4"> |
|
|
|
|
@ -517,3 +869,36 @@
@@ -517,3 +869,36 @@
|
|
|
|
|
</div> |
|
|
|
|
{/if} |
|
|
|
|
</div> |
|
|
|
|
|
|
|
|
|
<style> |
|
|
|
|
/* Highlight styles */ |
|
|
|
|
.highlight-container { |
|
|
|
|
position: relative; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
.highlight-content { |
|
|
|
|
font-style: italic; |
|
|
|
|
background: linear-gradient(transparent 0%, transparent 40%, rgba(255, 255, 0, 0.3) 40%, rgba(255, 255, 0, 0.3) 100%); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
.highlight-quote { |
|
|
|
|
position: relative; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
.highlight-quote::before { |
|
|
|
|
content: '"'; |
|
|
|
|
position: absolute; |
|
|
|
|
top: -5px; |
|
|
|
|
left: -10px; |
|
|
|
|
font-size: 2rem; |
|
|
|
|
color: #3b82f6; |
|
|
|
|
opacity: 0.5; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
.highlight-context { |
|
|
|
|
color: #6b7280; |
|
|
|
|
font-size: 0.875rem; |
|
|
|
|
margin-bottom: 0.5rem; |
|
|
|
|
opacity: 0.8; |
|
|
|
|
} |
|
|
|
|
</style> |