diff --git a/src/lib/components/CommentBox.svelte b/src/lib/components/CommentBox.svelte index 9c6dd9f..15fab77 100644 --- a/src/lib/components/CommentBox.svelte +++ b/src/lib/components/CommentBox.svelte @@ -11,13 +11,13 @@ import { standardRelays, fallbackRelays } from "$lib/consts"; import { userRelays } from "$lib/stores/relayStore"; import { get } from "svelte/store"; + import { activePubkey } from '$lib/ndk'; import { goto } from "$app/navigation"; import type { NDKEvent } from "$lib/utils/nostrUtils"; import { onMount } from "svelte"; const props = $props<{ event: NDKEvent; - userPubkey: string; userRelayPreference: boolean; }>(); @@ -29,12 +29,26 @@ let showOtherRelays = $state(false); let showFallbackRelays = $state(false); let userProfile = $state(null); + let pubkey = $state(null); + $effect(() => { + pubkey = get(activePubkey); + }); // Fetch user profile on mount - onMount(async () => { - if (props.userPubkey) { - const npub = nip19.npubEncode(props.userPubkey); - userProfile = await getUserMetadata(npub); + onMount(() => { + const trimmedPubkey = pubkey?.trim(); + if (trimmedPubkey && /^[a-fA-F0-9]{64}$/.test(trimmedPubkey)) { + (async () => { + const npub = nip19.npubEncode(trimmedPubkey); + userProfile = await getUserMetadata(npub); + error = null; + })(); + } else if (trimmedPubkey) { + userProfile = null; + error = 'Invalid public key: must be a 64-character hex string.'; + } else { + userProfile = null; + error = null; } }); @@ -102,6 +116,22 @@ updatePreview(); } + // Helper functions to ensure relay and pubkey are always strings + function getRelayString(relay: any): string { + if (!relay) return ''; + if (typeof relay === 'string') return relay; + if (typeof relay.url === 'string') return relay.url; + return ''; + } + + function getPubkeyString(pubkey: any): string { + if (!pubkey) return ''; + if (typeof pubkey === 'string') return pubkey; + if (typeof pubkey.hex === 'function') return pubkey.hex(); + if (typeof pubkey.pubkey === 'string') return pubkey.pubkey; + return ''; + } + async function handleSubmit( useOtherRelays = false, useFallbackRelays = false, @@ -111,53 +141,91 @@ success = null; try { - if (!props.event.kind) { - throw new Error("Invalid event: missing kind"); + if (!pubkey || !/^[a-fA-F0-9]{64}$/.test(pubkey)) { + throw new Error('Invalid public key: must be a 64-character hex string.'); + } + if (props.event.kind === undefined || props.event.kind === null) { + throw new Error('Invalid event: missing kind'); } - const kind = props.event.kind === 1 ? 1 : 1111; - const tags: string[][] = []; - - if (kind === 1) { - // NIP-10 reply - tags.push(["e", props.event.id, "", "reply"]); - tags.push(["p", props.event.pubkey]); - if (props.event.tags) { - const rootTag = props.event.tags.find( - (t: string[]) => t[0] === "e" && t[3] === "root", - ); - if (rootTag) { - tags.push(["e", rootTag[1], "", "root"]); - } - // Add all p tags from the parent event - props.event.tags - .filter((t: string[]) => t[0] === "p") - .forEach((t: string[]) => { - if (!tags.some((pt: string[]) => pt[1] === t[1])) { - tags.push(["p", t[1]]); - } - }); + // Always use kind 1111 for comments + const kind = 1111; + const parent = props.event; + // Try to extract root info from parent tags (NIP-22 threading) + let rootKind = parent.kind; + let rootPubkey = getPubkeyString(parent.pubkey); + let rootRelay = getRelayString(parent.relay); + let rootId = parent.id; + let rootAddress = ''; + let parentRelay = getRelayString(parent.relay); + let parentAddress = ''; + let parentKind = parent.kind; + let parentPubkey = getPubkeyString(parent.pubkey); + // Try to find root event info from tags (E/A/I) + let isRootA = false; + let isRootI = false; + if (parent.tags) { + const rootE = parent.tags.find((t: string[]) => t[0] === 'E'); + const rootA = parent.tags.find((t: string[]) => t[0] === 'A'); + const rootI = parent.tags.find((t: string[]) => t[0] === 'I'); + isRootA = !!rootA; + isRootI = !!rootI; + if (rootE) { + rootId = rootE[1]; + rootRelay = getRelayString(rootE[2]); + rootPubkey = getPubkeyString(rootE[3] || rootPubkey); + rootKind = parent.tags.find((t: string[]) => t[0] === 'K')?.[1] || rootKind; + } else if (rootA) { + rootAddress = rootA[1]; + rootRelay = getRelayString(rootA[2]); + rootPubkey = getPubkeyString(parent.tags.find((t: string[]) => t[0] === 'P')?.[1] || rootPubkey); + rootKind = parent.tags.find((t: string[]) => t[0] === 'K')?.[1] || rootKind; + } else if (rootI) { + rootAddress = rootI[1]; + rootKind = parent.tags.find((t: string[]) => t[0] === 'K')?.[1] || rootKind; } + } + // Compose tags according to NIP-22 + const tags: string[][] = []; + // Root scope (uppercase) + if (rootAddress) { + tags.push([isRootA ? 'A' : isRootI ? 'I' : 'E', rootAddress || rootId, rootRelay, rootPubkey]); + } else { + tags.push(['E', rootId, rootRelay, rootPubkey]); + } + tags.push(['K', String(rootKind), '', '']); + tags.push(['P', rootPubkey, rootRelay, '']); + // Parent (lowercase) + if (parentAddress) { + tags.push([isRootA ? 'a' : isRootI ? 'i' : 'e', parentAddress || parent.id, parentRelay, parentPubkey]); } else { - // NIP-22 comment - tags.push(["E", props.event.id, "", props.event.pubkey]); - tags.push(["K", props.event.kind.toString()]); - tags.push(["P", props.event.pubkey]); - tags.push(["e", props.event.id, "", props.event.pubkey]); - tags.push(["k", props.event.kind.toString()]); - tags.push(["p", props.event.pubkey]); + tags.push(['e', parent.id, parentRelay, parentPubkey]); } + tags.push(['k', String(parentKind), '', '']); + tags.push(['p', parentPubkey, parentRelay, '']); + // Create a completely plain object to avoid proxy cloning issues const eventToSign = { - kind, - created_at: Math.floor(Date.now() / 1000), - tags, - content, - pubkey: props.userPubkey, + kind: Number(kind), + created_at: Number(Math.floor(Date.now() / 1000)), + tags: tags.map(tag => [String(tag[0]), String(tag[1]), String(tag[2] || ''), String(tag[3] || '')]), + content: String(content), + pubkey: String(pubkey), }; - const id = getEventHash(eventToSign); - const sig = await signEvent(eventToSign); + let sig, id; + if (typeof window !== 'undefined' && window.nostr && window.nostr.signEvent) { + const signed = await window.nostr.signEvent(eventToSign); + sig = signed.sig as string; + if ('id' in signed) { + id = signed.id as string; + } else { + id = getEventHash(eventToSign); + } + } else { + id = getEventHash(eventToSign); + sig = await signEvent(eventToSign); + } const signedEvent = { ...eventToSign, @@ -288,10 +356,11 @@ {#if success} - Comment published successfully to {success.relay}! + Comment published successfully to {success.relay}!
+ Event ID: {success.eventId} View your comment @@ -315,16 +384,16 @@ {userProfile.displayName || userProfile.name || - nip19.npubEncode(props.userPubkey).slice(0, 8) + "..."} + nip19.npubEncode(pubkey || '').slice(0, 8) + "..."} {/if} - {#if !props.userPubkey} + {#if !pubkey} Please sign in to post comments. Your comments will be signed with your current account. diff --git a/src/lib/components/EventDetails.svelte b/src/lib/components/EventDetails.svelte index a20ef15..d12a61a 100644 --- a/src/lib/components/EventDetails.svelte +++ b/src/lib/components/EventDetails.svelte @@ -37,7 +37,36 @@ let authorDisplayName = $state(undefined); function getEventTitle(event: NDKEvent): string { - return getMatchingTags(event, "title")[0]?.[1] || "Untitled"; + // First try to get title from title tag + const titleTag = getMatchingTags(event, "title")[0]?.[1]; + if (titleTag) { + return titleTag; + } + + // For kind 30023 events, extract title from markdown content if no title tag + if (event.kind === 30023 && event.content) { + const match = event.content.match(/^#\s+(.+)$/m); + if (match) { + return match[1].trim(); + } + } + + // For kind 30040, 30041, and 30818 events, extract title from AsciiDoc content if no title tag + if ((event.kind === 30040 || event.kind === 30041 || event.kind === 30818) && event.content) { + // First try to find a document header (= ) + const docMatch = event.content.match(/^=\s+(.+)$/m); + if (docMatch) { + return docMatch[1].trim(); + } + + // If no document header, try to find the first section header (== ) + const sectionMatch = event.content.match(/^==\s+(.+)$/m); + if (sectionMatch) { + return sectionMatch[1].trim(); + } + } + + return "Untitled"; } function getEventSummary(event: NDKEvent): string { diff --git a/src/lib/components/EventInput.svelte b/src/lib/components/EventInput.svelte new file mode 100644 index 0000000..c7f49d5 --- /dev/null +++ b/src/lib/components/EventInput.svelte @@ -0,0 +1,363 @@ + + +{#if pubkey} +
+

Publish Nostr Event

+
+
+ + + {#if !isValidKind(kind)} +
+ Kind must be an integer between 0 and 65535 (NIP-01). +
+ {/if} +
+
+ +
+ {#each tags as [key, value], i} +
+ updateTag(i, (e.target as HTMLInputElement).value, tags[i][1])} /> + updateTag(i, tags[i][0], (e.target as HTMLInputElement).value)} /> + +
+ {/each} + +
+
+
+ + +
+
+ + +
+
+ + + {#if dTagError} +
{dTagError}
+ {/if} +
+ + {#if loading} + Publishing... + {/if} + {#if error} +
{error}
+ {/if} + {#if success} +
{success}
+
Relays: {publishedRelays.join(', ')}
+ {#if lastPublishedEventId} +
+ Event ID: {lastPublishedEventId} + + View your event + +
+ {/if} + {/if} +
+
+{/if} \ No newline at end of file diff --git a/src/lib/components/RelayActions.svelte b/src/lib/components/RelayActions.svelte index 0f21891..b854d77 100644 --- a/src/lib/components/RelayActions.svelte +++ b/src/lib/components/RelayActions.svelte @@ -19,9 +19,6 @@ let searchingRelays = $state(false); let foundRelays = $state([]); - let broadcasting = $state(false); - let broadcastSuccess = $state(false); - let broadcastError = $state(null); let showRelayModal = $state(false); let relaySearchResults = $state< Record @@ -33,43 +30,6 @@ `; - // Broadcast icon SVG - const broadcastIcon = ``; - - async function broadcastEvent() { - if (!event || !$ndkInstance?.activeUser) return; - broadcasting = true; - broadcastSuccess = false; - broadcastError = null; - - try { - const connectedRelays = getConnectedRelays(); - if (connectedRelays.length === 0) { - throw new Error("No connected relays available"); - } - - // Create a new event with the same content - const newEvent = createNDKEvent($ndkInstance, { - ...event.rawEvent(), - pubkey: $ndkInstance.activeUser.pubkey, - created_at: Math.floor(Date.now() / 1000), - sig: "", - }); - - // Publish to all relays - await newEvent.publish(); - broadcastSuccess = true; - } catch (err) { - console.error("Error broadcasting event:", err); - broadcastError = - err instanceof Error ? err.message : "Failed to broadcast event"; - } finally { - broadcasting = false; - } - } - function openRelayModal() { showRelayModal = true; relaySearchResults = {}; @@ -117,17 +77,6 @@ {@html searchIcon} Where can I find this event? - - {#if $ndkInstance?.activeUser} - - {/if} {#if foundRelays.length > 0} @@ -141,23 +90,6 @@ {/if} -{#if broadcastSuccess} -
- Event broadcast successfully to: -
- {#each getConnectedRelays() as relay} - - {/each} -
-
-{/if} - -{#if broadcastError} -
- {broadcastError} -
-{/if} -
Found on:
diff --git a/src/lib/components/RelayStatus.svelte b/src/lib/components/RelayStatus.svelte index 92c5028..fa9f51c 100644 --- a/src/lib/components/RelayStatus.svelte +++ b/src/lib/components/RelayStatus.svelte @@ -99,6 +99,8 @@ onMount(() => { checkWebSocketSupport(); checkEnvironmentForWebSocketDowngrade(); + // Run initial relay tests + void runRelayTests(); }); function getStatusColor(status: RelayStatus): string { diff --git a/src/lib/consts.ts b/src/lib/consts.ts index c661399..ac908fd 100644 --- a/src/lib/consts.ts +++ b/src/lib/consts.ts @@ -6,7 +6,7 @@ export const standardRelays = [ "wss://thecitadel.nostr1.com", "wss://theforest.nostr1.com", "wss://profiles.nostr1.com", - "wss://gitcitadel.nostr1.com", + // Removed gitcitadel.nostr1.com as it's causing connection issues //'wss://thecitadel.gitcitadel.eu', //'wss://theforest.gitcitadel.eu', ]; diff --git a/src/lib/ndk.ts b/src/lib/ndk.ts index cbdf546..196ee03 100644 --- a/src/lib/ndk.ts +++ b/src/lib/ndk.ts @@ -397,21 +397,40 @@ function createRelayWithAuth(url: string, ndk: NDK): NDKRelay { // Ensure the URL is using wss:// protocol const secureUrl = ensureSecureWebSocket(url); + // Add connection timeout and error handling const relay = new NDKRelay( secureUrl, NDKRelayAuthPolicies.signIn({ ndk }), ndk, ); + // Set up connection timeout + const connectionTimeout = setTimeout(() => { + console.warn(`[NDK.ts] Connection timeout for ${secureUrl}`); + relay.disconnect(); + }, 10000); // 10 second timeout + // Set up custom authentication handling only if user is signed in if (ndk.signer && ndk.activeUser) { const authPolicy = new CustomRelayAuthPolicy(ndk); relay.on("connect", () => { console.debug(`[NDK.ts] Relay connected: ${secureUrl}`); + clearTimeout(connectionTimeout); authPolicy.authenticate(relay); }); + } else { + relay.on("connect", () => { + console.debug(`[NDK.ts] Relay connected: ${secureUrl}`); + clearTimeout(connectionTimeout); + }); } + // Add error handling + relay.on("disconnect", () => { + console.debug(`[NDK.ts] Relay disconnected: ${secureUrl}`); + clearTimeout(connectionTimeout); + }); + return relay; } @@ -462,7 +481,23 @@ export function initNdk(): NDK { // Set up custom authentication policy ndk.relayAuthDefaultPolicy = NDKRelayAuthPolicies.signIn({ ndk }); - ndk.connect().then(() => console.debug("[NDK.ts] NDK connected")); + + // Connect with better error handling + ndk.connect() + .then(() => { + console.debug("[NDK.ts] NDK connected successfully"); + }) + .catch((error) => { + console.error("[NDK.ts] Failed to connect NDK:", error); + // Try to reconnect after a delay + setTimeout(() => { + console.debug("[NDK.ts] Attempting to reconnect..."); + ndk.connect().catch((retryError) => { + console.error("[NDK.ts] Reconnection failed:", retryError); + }); + }, 5000); + }); + return ndk; } diff --git a/src/lib/stores/authStore.ts b/src/lib/stores/authStore.ts new file mode 100644 index 0000000..9337100 --- /dev/null +++ b/src/lib/stores/authStore.ts @@ -0,0 +1,11 @@ +import { writable, derived } from 'svelte/store'; + +/** + * Stores the user's public key if logged in, or null otherwise. + */ +export const userPubkey = writable(null); + +/** + * Derived store indicating if the user is logged in. + */ +export const isLoggedIn = derived(userPubkey, ($userPubkey) => !!$userPubkey); \ No newline at end of file diff --git a/src/lib/utils/event_input_utils.ts b/src/lib/utils/event_input_utils.ts new file mode 100644 index 0000000..4a35c5e --- /dev/null +++ b/src/lib/utils/event_input_utils.ts @@ -0,0 +1,219 @@ +import type { NDKEvent } from './nostrUtils'; +import { get } from 'svelte/store'; +import { ndkInstance } from '$lib/ndk'; +import { NDKEvent as NDKEventClass } from '@nostr-dev-kit/ndk'; + +// ========================= +// Validation +// ========================= + +/** + * Returns true if the event kind requires a d-tag (kinds 30000-39999). + */ +export function requiresDTag(kind: number): boolean { + return kind >= 30000 && kind <= 39999; +} + +/** + * Returns true if the tags array contains at least one d-tag with a non-empty value. + */ +export function hasDTag(tags: [string, string][]): boolean { + return tags.some(([k, v]) => k === 'd' && v && v.trim() !== ''); +} + +/** + * Returns true if the content contains AsciiDoc headers (lines starting with '=' or '=='). + */ +function containsAsciiDocHeaders(content: string): boolean { + return /^={1,}\s+/m.test(content); +} + +/** + * Validates that content does NOT contain AsciiDoc headers (for kind 30023). + * Returns { valid, reason }. + */ +export function validateNotAsciidoc(content: string): { valid: boolean; reason?: string } { + if (containsAsciiDocHeaders(content)) { + return { + valid: false, + reason: 'Kind 30023 must not contain AsciiDoc headers (lines starting with = or ==).', + }; + } + return { valid: true }; +} + +/** + * Validates AsciiDoc content. Must start with '=' and contain at least one '==' section header. + * Returns { valid, reason }. + */ +export function validateAsciiDoc(content: string): { valid: boolean; reason?: string } { + if (!content.trim().startsWith('=')) { + return { valid: false, reason: 'AsciiDoc must start with a document title ("=").' }; + } + if (!/^==\s+/m.test(content)) { + return { valid: false, reason: 'AsciiDoc must contain at least one section header ("==").' }; + } + return { valid: true }; +} + +// ========================= +// Extraction & Normalization +// ========================= + +/** + * Normalize a string for use as a d-tag: lowercase, hyphens, alphanumeric only. + */ +function normalizeDTagValue(header: string): string { + return header + .toLowerCase() + .replace(/[^\p{L}\p{N}]+/gu, '-') + .replace(/^-+|-+$/g, ''); +} + +/** + * Converts a title string to a valid d-tag (lowercase, hyphens, no punctuation). + */ +export function titleToDTag(title: string): string { + return title + .toLowerCase() + .replace(/[^a-z0-9]+/g, '-') // Replace non-alphanumeric with hyphens + .replace(/^-+|-+$/g, ''); // Trim leading/trailing hyphens +} + +/** + * Extracts the first AsciiDoc document header (line starting with '= '). + */ +function extractAsciiDocDocumentHeader(content: string): string | null { + const match = content.match(/^=\s+(.+)$/m); + return match ? match[1].trim() : null; +} + +/** + * Extracts all section headers (lines starting with '== '). + */ +function extractAsciiDocSectionHeaders(content: string): string[] { + return Array.from(content.matchAll(/^==\s+(.+)$/gm)).map(m => m[1].trim()); +} + +/** + * Extracts the topmost Markdown # header (line starting with '# '). + */ +function extractMarkdownTopHeader(content: string): string | null { + const match = content.match(/^#\s+(.+)$/m); + return match ? match[1].trim() : null; +} + +/** + * Splits AsciiDoc content into sections at each '==' header. Returns array of section strings. + */ +function splitAsciiDocSections(content: string): string[] { + const lines = content.split(/\r?\n/); + const sections: string[] = []; + let current: string[] = []; + for (const line of lines) { + if (/^==\s+/.test(line) && current.length > 0) { + sections.push(current.join('\n').trim()); + current = []; + } + current.push(line); + } + if (current.length > 0) { + sections.push(current.join('\n').trim()); + } + return sections; +} + +// ========================= +// Event Construction +// ========================= + +/** + * Returns the current NDK instance from the store. + */ +function getNdk() { + return get(ndkInstance); +} + +/** + * Builds a set of events for a 30040 publication: one 30040 index event and one 30041 event per section. + * Each 30041 gets a d-tag (normalized section header) and a title tag (raw section header). + * The 30040 index event references all 30041s by their d-tag. + */ +export function build30040EventSet( + content: string, + tags: [string, string][], + baseEvent: Partial & { pubkey: string; created_at: number } +): { indexEvent: NDKEvent; sectionEvents: NDKEvent[] } { + const ndk = getNdk(); + const sections = splitAsciiDocSections(content); + const sectionHeaders = extractAsciiDocSectionHeaders(content); + const dTags = sectionHeaders.length === sections.length + ? sectionHeaders.map(normalizeDTagValue) + : sections.map((_, i) => `section${i}`); + const sectionEvents: NDKEvent[] = sections.map((section, i) => { + const header = sectionHeaders[i] || `Section ${i + 1}`; + const dTag = dTags[i]; + return new NDKEventClass(ndk, { + kind: 30041, + content: section, + tags: [ + ...tags, + ['d', dTag], + ['title', header], + ], + pubkey: baseEvent.pubkey, + created_at: baseEvent.created_at, + }); + }); + const indexTags = [ + ...tags, + ...dTags.map(d => ['a', d] as [string, string]), + ]; + const indexEvent: NDKEvent = new NDKEventClass(ndk, { + kind: 30040, + content: '', + tags: indexTags, + pubkey: baseEvent.pubkey, + created_at: baseEvent.created_at, + }); + return { indexEvent, sectionEvents }; +} + +/** + * Returns the appropriate title tag for a given event kind and content. + * - 30041, 30818: AsciiDoc document header (first '= ' line) + * - 30023: Markdown topmost '# ' header + */ +export function getTitleTagForEvent(kind: number, content: string): string | null { + if (kind === 30041 || kind === 30818) { + return extractAsciiDocDocumentHeader(content); + } + if (kind === 30023) { + return extractMarkdownTopHeader(content); + } + return null; +} + +/** + * Returns the appropriate d-tag value for a given event kind and content. + * - 30023: Normalized markdown header + * - 30041, 30818: Normalized AsciiDoc document header + * - 30040: Uses existing d-tag or generates from content + */ +export function getDTagForEvent(kind: number, content: string, existingDTag?: string): string | null { + if (existingDTag && existingDTag.trim() !== '') { + return existingDTag.trim(); + } + + if (kind === 30023) { + const title = extractMarkdownTopHeader(content); + return title ? normalizeDTagValue(title) : null; + } + + if (kind === 30041 || kind === 30818) { + const title = extractAsciiDocDocumentHeader(content); + return title ? normalizeDTagValue(title) : null; + } + + return null; +} \ No newline at end of file diff --git a/src/lib/utils/relayDiagnostics.ts b/src/lib/utils/relayDiagnostics.ts new file mode 100644 index 0000000..71a5d99 --- /dev/null +++ b/src/lib/utils/relayDiagnostics.ts @@ -0,0 +1,140 @@ +import { standardRelays, anonymousRelays, fallbackRelays } from '$lib/consts'; +import NDK from '@nostr-dev-kit/ndk'; + +export interface RelayDiagnostic { + url: string; + connected: boolean; + requiresAuth: boolean; + error?: string; + responseTime?: number; +} + +/** + * Tests connection to a single relay + */ +export async function testRelay(url: string): Promise { + const startTime = Date.now(); + + return new Promise((resolve) => { + const ws = new WebSocket(url); + let resolved = false; + + const timeout = setTimeout(() => { + if (!resolved) { + resolved = true; + ws.close(); + resolve({ + url, + connected: false, + requiresAuth: false, + error: 'Connection timeout', + responseTime: Date.now() - startTime, + }); + } + }, 5000); + + ws.onopen = () => { + if (!resolved) { + resolved = true; + clearTimeout(timeout); + ws.close(); + resolve({ + url, + connected: true, + requiresAuth: false, + responseTime: Date.now() - startTime, + }); + } + }; + + ws.onerror = () => { + if (!resolved) { + resolved = true; + clearTimeout(timeout); + resolve({ + url, + connected: false, + requiresAuth: false, + error: 'WebSocket error', + responseTime: Date.now() - startTime, + }); + } + }; + + ws.onmessage = (event) => { + const data = JSON.parse(event.data); + if (data[0] === 'NOTICE' && data[1]?.includes('auth-required')) { + if (!resolved) { + resolved = true; + clearTimeout(timeout); + ws.close(); + resolve({ + url, + connected: true, + requiresAuth: true, + responseTime: Date.now() - startTime, + }); + } + } + }; + }); +} + +/** + * Tests all relays and returns diagnostic information + */ +export async function testAllRelays(): Promise { + const allRelays = [...new Set([...standardRelays, ...anonymousRelays, ...fallbackRelays])]; + + console.log('[RelayDiagnostics] Testing', allRelays.length, 'relays...'); + + const results = await Promise.allSettled( + allRelays.map(url => testRelay(url)) + ); + + return results.map((result, index) => { + if (result.status === 'fulfilled') { + return result.value; + } else { + return { + url: allRelays[index], + connected: false, + requiresAuth: false, + error: 'Test failed', + }; + } + }); +} + +/** + * Gets working relays from diagnostic results + */ +export function getWorkingRelays(diagnostics: RelayDiagnostic[]): string[] { + return diagnostics + .filter(d => d.connected) + .map(d => d.url); +} + +/** + * Logs relay diagnostic results to console + */ +export function logRelayDiagnostics(diagnostics: RelayDiagnostic[]): void { + console.group('[RelayDiagnostics] Results'); + + const working = diagnostics.filter(d => d.connected); + const failed = diagnostics.filter(d => !d.connected); + + console.log(`✅ Working relays (${working.length}):`); + working.forEach(d => { + console.log(` - ${d.url}${d.requiresAuth ? ' (requires auth)' : ''}${d.responseTime ? ` (${d.responseTime}ms)` : ''}`); + }); + + if (failed.length > 0) { + console.log(`❌ Failed relays (${failed.length}):`); + failed.forEach(d => { + console.log(` - ${d.url}: ${d.error || 'Unknown error'}`); + }); + } + + console.groupEnd(); +} \ No newline at end of file diff --git a/src/routes/events/+page.svelte b/src/routes/events/+page.svelte index 7e426e5..fd1236b 100644 --- a/src/routes/events/+page.svelte +++ b/src/routes/events/+page.svelte @@ -9,6 +9,10 @@ import CommentBox from "$lib/components/CommentBox.svelte"; import { userBadge } from "$lib/snippets/UserSnippets.svelte"; import { getMatchingTags, toNpub } from "$lib/utils/nostrUtils"; + import EventInput from '$lib/components/EventInput.svelte'; + import { userPubkey, isLoggedIn } from '$lib/stores/authStore'; + import RelayStatus from '$lib/components/RelayStatus.svelte'; + import { testAllRelays, logRelayDiagnostics } from '$lib/utils/relayDiagnostics'; let loading = $state(false); let error = $state(null); @@ -26,7 +30,6 @@ lud16?: string; nip05?: string; } | null>(null); - let userPubkey = $state(null); let userRelayPreference = $state(false); function handleEventFound(newEvent: NDKEvent) { @@ -74,10 +77,14 @@ } }); - onMount(async () => { - // Get user's pubkey and relay preference from localStorage - userPubkey = localStorage.getItem("userPubkey"); - userRelayPreference = localStorage.getItem("useUserRelays") === "true"; + onMount(() => { + // Initialize userPubkey from localStorage if available + const pubkey = localStorage.getItem('userPubkey'); + userPubkey.set(pubkey); + userRelayPreference = localStorage.getItem('useUserRelays') === 'true'; + + // Run relay diagnostics to help identify connection issues + testAllRelays().then(logRelayDiagnostics).catch(console.error); }); @@ -103,13 +110,17 @@ onSearchResults={handleSearchResults} /> + {#if $isLoggedIn && !event && searchResults.length === 0} + + {/if} + {#if event} - {#if userPubkey} + {#if $isLoggedIn && $userPubkey}
Add Comment - +
{:else}