clone of repo on github
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

<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}