You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
282 lines
10 KiB
282 lines
10 KiB
<script lang="ts"> |
|
import { getNdkContext, activeInboxRelays, activeOutboxRelays } from "$lib/ndk"; |
|
import type { NDKEvent } from "@nostr-dev-kit/ndk"; |
|
import { NDKEvent as NDKEventClass } from "@nostr-dev-kit/ndk"; |
|
import { communityRelays } from "$lib/consts"; |
|
import { WebSocketPool } from "$lib/data_structures/websocket_pool"; |
|
import { generateMockCommentsForSections } from "$lib/utils/mockCommentData"; |
|
|
|
let { |
|
eventId, |
|
eventAddress, |
|
eventIds = [], |
|
eventAddresses = [], |
|
comments = $bindable([]), |
|
useMockComments = false, |
|
}: { |
|
eventId?: string; |
|
eventAddress?: string; |
|
eventIds?: string[]; |
|
eventAddresses?: string[]; |
|
comments?: NDKEvent[]; |
|
useMockComments?: boolean; |
|
} = $props(); |
|
|
|
const ndk = getNdkContext(); |
|
|
|
// State management |
|
let loading = $state(false); |
|
|
|
/** |
|
* Fetch comment events (kind 1111) for the current publication using WebSocketPool |
|
* |
|
* This follows the exact pattern from HighlightLayer.svelte to ensure reliability. |
|
* Uses WebSocketPool with nostr-tools protocol instead of NDK subscriptions. |
|
*/ |
|
async function fetchComments() { |
|
// Prevent concurrent fetches |
|
if (loading) { |
|
console.log("[CommentLayer] Already loading, skipping fetch"); |
|
return; |
|
} |
|
|
|
// Collect all event IDs and addresses |
|
const allEventIds = [...(eventId ? [eventId] : []), ...eventIds].filter(Boolean); |
|
const allAddresses = [...(eventAddress ? [eventAddress] : []), ...eventAddresses].filter(Boolean); |
|
|
|
if (allEventIds.length === 0 && allAddresses.length === 0) { |
|
console.warn("[CommentLayer] No event IDs or addresses provided"); |
|
return; |
|
} |
|
|
|
loading = true; |
|
comments = []; |
|
|
|
// AI-NOTE: Mock mode allows testing comment UI without publishing to relays |
|
// This is useful for development and demonstrating the comment system |
|
if (useMockComments) { |
|
console.log(`[CommentLayer] MOCK MODE - Generating mock comments for ${allAddresses.length} sections`); |
|
|
|
try { |
|
// Generate mock comment data |
|
const mockComments = generateMockCommentsForSections(allAddresses); |
|
|
|
// Convert to NDKEvent instances (same as real events) |
|
comments = mockComments.map(rawEvent => new NDKEventClass(ndk, rawEvent)); |
|
|
|
console.log(`[CommentLayer] Generated ${comments.length} mock comments`); |
|
loading = false; |
|
return; |
|
} catch (err) { |
|
console.error(`[CommentLayer] Error generating mock comments:`, err); |
|
loading = false; |
|
return; |
|
} |
|
} |
|
|
|
console.log(`[CommentLayer] Fetching comments for:`, { |
|
eventIds: allEventIds, |
|
addresses: allAddresses |
|
}); |
|
|
|
try { |
|
// Build filter for kind 1111 comment events |
|
// IMPORTANT: Use only #a tags because filters are AND, not OR |
|
// If we include both #e and #a, relays will only return comments that have BOTH |
|
const filter: any = { |
|
kinds: [1111], |
|
limit: 500, |
|
}; |
|
|
|
// Prefer #a (addressable events) since they're more specific and persistent |
|
if (allAddresses.length > 0) { |
|
filter["#a"] = allAddresses; |
|
} else if (allEventIds.length > 0) { |
|
// Fallback to #e if no addresses available |
|
filter["#e"] = allEventIds; |
|
} |
|
|
|
console.log(`[CommentLayer] Fetching with filter:`, JSON.stringify(filter, null, 2)); |
|
|
|
// Build explicit relay set (same pattern as HighlightLayer) |
|
const relays = [ |
|
...communityRelays, |
|
...$activeOutboxRelays, |
|
...$activeInboxRelays, |
|
]; |
|
const uniqueRelays = Array.from(new Set(relays)); |
|
console.log(`[CommentLayer] Fetching from ${uniqueRelays.length} relays:`, uniqueRelays); |
|
|
|
/** |
|
* Use WebSocketPool with nostr-tools protocol instead of NDK |
|
* |
|
* Reasons for not using NDK: |
|
* 1. NDK subscriptions mysteriously returned 0 events even when websocat confirmed events existed |
|
* 2. Consistency - HighlightLayer, CommentButton, and HighlightSelectionHandler use WebSocketPool |
|
* 3. Better debugging - direct access to WebSocket messages for troubleshooting |
|
* 4. Proven reliability - battle-tested in the codebase for similar use cases |
|
* 5. Performance control - explicit 5s timeout per relay, tunable as needed |
|
* |
|
* This matches the pattern in: |
|
* - src/lib/components/publications/HighlightLayer.svelte:111-212 |
|
* - src/lib/components/publications/CommentButton.svelte:156-220 |
|
* - src/lib/components/publications/HighlightSelectionHandler.svelte:217-280 |
|
*/ |
|
const subscriptionId = `comments-${Date.now()}`; |
|
const receivedEventIds = new Set<string>(); |
|
let eoseCount = 0; |
|
|
|
const fetchPromises = uniqueRelays.map(async (relayUrl) => { |
|
try { |
|
console.log(`[CommentLayer] Connecting to ${relayUrl}`); |
|
const ws = await WebSocketPool.instance.acquire(relayUrl); |
|
|
|
return new Promise<void>((resolve) => { |
|
const messageHandler = (event: MessageEvent) => { |
|
try { |
|
const message = JSON.parse(event.data); |
|
|
|
// Log ALL messages from relay.nostr.band for debugging |
|
if (relayUrl.includes('relay.nostr.band')) { |
|
console.log(`[CommentLayer] RAW message from ${relayUrl}:`, message); |
|
} |
|
|
|
if (message[0] === "EVENT" && message[1] === subscriptionId) { |
|
const rawEvent = message[2]; |
|
console.log(`[CommentLayer] EVENT from ${relayUrl}:`, { |
|
id: rawEvent.id, |
|
kind: rawEvent.kind, |
|
content: rawEvent.content.substring(0, 50), |
|
tags: rawEvent.tags |
|
}); |
|
|
|
// Avoid duplicates |
|
if (!receivedEventIds.has(rawEvent.id)) { |
|
receivedEventIds.add(rawEvent.id); |
|
|
|
// Convert to NDKEvent |
|
const ndkEvent = new NDKEventClass(ndk, rawEvent); |
|
comments = [...comments, ndkEvent]; |
|
console.log(`[CommentLayer] Added comment, total now: ${comments.length}`); |
|
} |
|
} else if (message[0] === "EOSE" && message[1] === subscriptionId) { |
|
eoseCount++; |
|
console.log(`[CommentLayer] EOSE from ${relayUrl} (${eoseCount}/${uniqueRelays.length})`); |
|
|
|
// Close subscription |
|
ws.send(JSON.stringify(["CLOSE", subscriptionId])); |
|
ws.removeEventListener("message", messageHandler); |
|
WebSocketPool.instance.release(ws); |
|
resolve(); |
|
} else if (message[0] === "NOTICE") { |
|
console.warn(`[CommentLayer] NOTICE from ${relayUrl}:`, message[1]); |
|
} |
|
} catch (err) { |
|
console.error(`[CommentLayer] Error processing message from ${relayUrl}:`, err); |
|
} |
|
}; |
|
|
|
ws.addEventListener("message", messageHandler); |
|
|
|
// Send REQ |
|
const req = ["REQ", subscriptionId, filter]; |
|
if (relayUrl.includes('relay.nostr.band')) { |
|
console.log(`[CommentLayer] Sending REQ to ${relayUrl}:`, JSON.stringify(req)); |
|
} else { |
|
console.log(`[CommentLayer] Sending REQ to ${relayUrl}`); |
|
} |
|
ws.send(JSON.stringify(req)); |
|
|
|
// Timeout per relay (5 seconds) |
|
setTimeout(() => { |
|
if (ws.readyState === WebSocket.OPEN) { |
|
ws.send(JSON.stringify(["CLOSE", subscriptionId])); |
|
ws.removeEventListener("message", messageHandler); |
|
WebSocketPool.instance.release(ws); |
|
} |
|
resolve(); |
|
}, 5000); |
|
}); |
|
} catch (err) { |
|
console.error(`[CommentLayer] Error connecting to ${relayUrl}:`, err); |
|
} |
|
}); |
|
|
|
// Wait for all relays to respond or timeout |
|
await Promise.all(fetchPromises); |
|
|
|
console.log(`[CommentLayer] Fetched ${comments.length} comments`); |
|
|
|
if (comments.length > 0) { |
|
console.log(`[CommentLayer] Comments summary:`, comments.map(c => ({ |
|
content: c.content.substring(0, 30) + "...", |
|
address: c.tags.find(t => t[0] === "a")?.[1], |
|
author: c.pubkey.substring(0, 8) |
|
}))); |
|
} |
|
|
|
loading = false; |
|
|
|
} catch (err) { |
|
console.error(`[CommentLayer] Error fetching comments:`, err); |
|
loading = false; |
|
} |
|
} |
|
|
|
// Track the last fetched event count to know when to refetch |
|
let lastFetchedCount = $state(0); |
|
let fetchTimeout: ReturnType<typeof setTimeout> | null = null; |
|
|
|
// Watch for changes to event data - debounce and fetch when data stabilizes |
|
$effect(() => { |
|
const currentCount = eventIds.length + eventAddresses.length; |
|
const hasEventData = currentCount > 0; |
|
|
|
console.log(`[CommentLayer] Event data effect - count: ${currentCount}, lastFetched: ${lastFetchedCount}, loading: ${loading}`); |
|
|
|
// Only fetch if: |
|
// 1. We have event data |
|
// 2. The count has changed since last fetch |
|
// 3. We're not already loading |
|
if (hasEventData && currentCount !== lastFetchedCount && !loading) { |
|
// Clear any existing timeout |
|
if (fetchTimeout) { |
|
clearTimeout(fetchTimeout); |
|
} |
|
|
|
// Debounce: wait 500ms for more events to arrive before fetching |
|
fetchTimeout = setTimeout(() => { |
|
console.log(`[CommentLayer] Event data stabilized at ${currentCount} events, fetching comments...`); |
|
lastFetchedCount = currentCount; |
|
fetchComments(); |
|
}, 500); |
|
} |
|
|
|
// Cleanup timeout on effect cleanup |
|
return () => { |
|
if (fetchTimeout) { |
|
clearTimeout(fetchTimeout); |
|
} |
|
}; |
|
}); |
|
|
|
/** |
|
* Public method to refresh comments (e.g., after creating a new one) |
|
*/ |
|
export function refresh() { |
|
console.log("[CommentLayer] Manual refresh triggered"); |
|
|
|
// Clear existing comments |
|
comments = []; |
|
|
|
// Reset fetch count to force re-fetch |
|
lastFetchedCount = 0; |
|
fetchComments(); |
|
} |
|
</script> |
|
|
|
{#if loading} |
|
<div class="fixed top-40 right-4 z-50 bg-white dark:bg-gray-800 rounded-lg shadow-lg p-3"> |
|
<p class="text-sm text-gray-600 dark:text-gray-300">Loading comments...</p> |
|
</div> |
|
{/if}
|
|
|