From 078cf05cc1ead2536d7ab1ba15581f8b56c44188 Mon Sep 17 00:00:00 2001 From: silberengel Date: Wed, 6 Aug 2025 06:34:28 +0200 Subject: [PATCH 01/27] comment viewer added --- src/lib/components/CommentViewer.svelte | 316 ++++++++++++++++++++++++ src/routes/events/+page.svelte | 3 + 2 files changed, 319 insertions(+) create mode 100644 src/lib/components/CommentViewer.svelte diff --git a/src/lib/components/CommentViewer.svelte b/src/lib/components/CommentViewer.svelte new file mode 100644 index 0000000..b240aad --- /dev/null +++ b/src/lib/components/CommentViewer.svelte @@ -0,0 +1,316 @@ + + +
+ + Comments ({threadedComments.length}) + + + {#if loading} +
+

Loading comments...

+
+ {:else if error} +
+

{error}

+
+ {:else if threadedComments.length === 0} +
+

No comments yet. Be the first to comment!

+
+ {:else} +
+ {#each threadedComments as node (node.event.id)} +
+
+
+
+ + {getAuthorName(node.event.pubkey)} + + + {formatDate(node.event.created_at || 0)} Kind: {node.event.kind} + +
+
+ + {shortenNevent(getNeventUrl(node.event))} + + +
+
+ +
+ {@html node.event.content || ""} +
+
+ + {#if node.children.length > 0} + {#each node.children as childNode (childNode.event.id)} +
+
+
+
+ + {getAuthorName(childNode.event.pubkey)} + + + {formatDate(childNode.event.created_at || 0)} Kind: {childNode.event.kind} + +
+
+ + {shortenNevent(getNeventUrl(childNode.event))} + + +
+
+ +
+ {@html childNode.event.content || ""} +
+
+
+ {/each} + {/if} +
+ {/each} +
+ {/if} +
\ No newline at end of file diff --git a/src/routes/events/+page.svelte b/src/routes/events/+page.svelte index 15c469c..65e0c43 100644 --- a/src/routes/events/+page.svelte +++ b/src/routes/events/+page.svelte @@ -8,6 +8,7 @@ import EventDetails from "$lib/components/EventDetails.svelte"; import RelayActions from "$lib/components/RelayActions.svelte"; import CommentBox from "$lib/components/CommentBox.svelte"; +import CommentViewer from "$lib/components/CommentViewer.svelte"; import { userStore } from "$lib/stores/userStore"; import { userBadge } from "$lib/snippets/UserSnippets.svelte"; import { getMatchingTags, toNpub } from "$lib/utils/nostrUtils"; @@ -810,6 +811,8 @@ + + {#if isLoggedIn && userPubkey}
Add Comment From 976771fc655b74b8d23c2e6d84d494cf7c29e61b Mon Sep 17 00:00:00 2001 From: silberengel Date: Wed, 6 Aug 2025 07:12:42 +0200 Subject: [PATCH 02/27] working kind 1 thread --- src/lib/components/CommentViewer.svelte | 375 +++++++++++++++--------- 1 file changed, 229 insertions(+), 146 deletions(-) diff --git a/src/lib/components/CommentViewer.svelte b/src/lib/components/CommentViewer.svelte index b240aad..078341c 100644 --- a/src/lib/components/CommentViewer.svelte +++ b/src/lib/components/CommentViewer.svelte @@ -1,96 +1,129 @@ + +{#snippet CommentItem(node: CommentNode)} +
+
+
+
+ {#if getAuthorPicture(node.event.pubkey)} + {getAuthorName(node.event.pubkey)} (e.target as HTMLImageElement).style.display = 'none'} + /> + {:else} +
+ + {getAuthorName(node.event.pubkey).charAt(0).toUpperCase()} + +
+ {/if} +
+ + {getAuthorName(node.event.pubkey)} + + + {formatRelativeDate(node.event.created_at || 0)} • Kind: {node.event.kind} + +
+
+
+ + {shortenNevent(getNeventUrl(node.event))} + + +
+
+ +
+ {#await parseContent(node.event.content || "") then parsedContent} + {@html parsedContent} + {:catch} + {@html node.event.content || ""} + {/await} +
+
+ + {#if node.children.length > 0} +
+ {#each node.children as childNode (childNode.event.id)} + {@render CommentItem(childNode)} + {/each} +
+ {/if} +
+{/snippet} +
Comments ({threadedComments.length}) @@ -239,77 +392,7 @@ {:else}
{#each threadedComments as node (node.event.id)} -
-
-
-
- - {getAuthorName(node.event.pubkey)} - - - {formatDate(node.event.created_at || 0)} Kind: {node.event.kind} - -
-
- - {shortenNevent(getNeventUrl(node.event))} - - -
-
- -
- {@html node.event.content || ""} -
-
- - {#if node.children.length > 0} - {#each node.children as childNode (childNode.event.id)} -
-
-
-
- - {getAuthorName(childNode.event.pubkey)} - - - {formatDate(childNode.event.created_at || 0)} Kind: {childNode.event.kind} - -
-
- - {shortenNevent(getNeventUrl(childNode.event))} - - -
-
- -
- {@html childNode.event.content || ""} -
-
-
- {/each} - {/if} -
+ {@render CommentItem(node)} {/each}
{/if} From 550ae84a9f81db09c82624dd6f99f90cb6910c59 Mon Sep 17 00:00:00 2001 From: silberengel Date: Wed, 6 Aug 2025 07:17:22 +0200 Subject: [PATCH 03/27] fixed commentbox userbadge display --- src/lib/components/CommentBox.svelte | 31 ++++++++++++++-------------- 1 file changed, 15 insertions(+), 16 deletions(-) diff --git a/src/lib/components/CommentBox.svelte b/src/lib/components/CommentBox.svelte index fbff0f3..6952279 100644 --- a/src/lib/components/CommentBox.svelte +++ b/src/lib/components/CommentBox.svelte @@ -10,6 +10,7 @@ } from "$lib/utils/search_utility"; import { userPubkey } from "$lib/stores/authStore.Svelte"; + import { userStore } from "$lib/stores/userStore"; import type { NDKEvent } from "$lib/utils/nostrUtils"; import { extractRootEventInfo, @@ -67,17 +68,12 @@ } }); + // Get user profile from userStore $effect(() => { - const trimmedPubkey = $userPubkey?.trim(); - const npub = toNpub(trimmedPubkey); - if (npub) { - // Call an async function, but don't make the effect itself async - getUserMetadata(npub).then((metadata) => { - userProfile = metadata; - }); - } else if (trimmedPubkey) { - userProfile = null; - error = "Invalid public key: must be a 64-character hex string."; + const currentUser = $userStore; + if (currentUser?.signedIn && currentUser.profile) { + userProfile = currentUser.profile; + error = null; } else { userProfile = null; error = null; @@ -590,17 +586,20 @@ {userProfile.name { - const img = e.target as HTMLImageElement; - img.src = `https://api.dicebear.com/7.x/avataaars/svg?seed=${encodeURIComponent(img.alt)}`; - }} + class="w-8 h-8 rounded-full object-cover" + onerror={(e) => (e.target as HTMLImageElement).style.display = 'none'} /> + {:else} +
+ + {(userProfile.displayName || userProfile.name || "U").charAt(0).toUpperCase()} + +
{/if} {userProfile.displayName || userProfile.name || - nip19.npubEncode($userPubkey || "").slice(0, 8) + "..."} + `${$userPubkey?.slice(0, 8)}...${$userPubkey?.slice(-4)}`}
{/if} From 939759e5ceea3b4f21b2966ac9f60130b79054d5 Mon Sep 17 00:00:00 2001 From: silberengel Date: Wed, 6 Aug 2025 07:41:23 +0200 Subject: [PATCH 04/27] made the cache more persistent --- src/lib/utils/npubCache.ts | 44 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/src/lib/utils/npubCache.ts b/src/lib/utils/npubCache.ts index 4fc4405..8c1c36f 100644 --- a/src/lib/utils/npubCache.ts +++ b/src/lib/utils/npubCache.ts @@ -4,6 +4,47 @@ export type NpubMetadata = NostrProfile; class NpubCache { private cache: Record = {}; + private readonly storageKey = 'alexandria_npub_cache'; + private readonly maxAge = 24 * 60 * 60 * 1000; // 24 hours in milliseconds + + constructor() { + this.loadFromStorage(); + } + + private loadFromStorage(): void { + try { + if (typeof window !== 'undefined') { + const stored = localStorage.getItem(this.storageKey); + if (stored) { + const data = JSON.parse(stored) as Record; + const now = Date.now(); + + // Filter out expired entries + for (const [key, entry] of Object.entries(data)) { + if (entry.timestamp && (now - entry.timestamp) < this.maxAge) { + this.cache[key] = entry.profile; + } + } + } + } + } catch (error) { + console.warn('Failed to load npub cache from storage:', error); + } + } + + private saveToStorage(): void { + try { + if (typeof window !== 'undefined') { + const data: Record = {}; + for (const [key, profile] of Object.entries(this.cache)) { + data[key] = { profile, timestamp: Date.now() }; + } + localStorage.setItem(this.storageKey, JSON.stringify(data)); + } + } catch (error) { + console.warn('Failed to save npub cache to storage:', error); + } + } get(key: string): NpubMetadata | undefined { return this.cache[key]; @@ -11,6 +52,7 @@ class NpubCache { set(key: string, value: NpubMetadata): void { this.cache[key] = value; + this.saveToStorage(); } has(key: string): boolean { @@ -20,6 +62,7 @@ class NpubCache { delete(key: string): boolean { if (key in this.cache) { delete this.cache[key]; + this.saveToStorage(); return true; } return false; @@ -37,6 +80,7 @@ class NpubCache { clear(): void { this.cache = {}; + this.saveToStorage(); } size(): number { From 485601a67b1ac08aa5c20405209c60f447e168ea Mon Sep 17 00:00:00 2001 From: silberengel Date: Wed, 6 Aug 2025 07:41:58 +0200 Subject: [PATCH 05/27] fixed image display for markup content fields --- src/lib/utils/markup/basicMarkupParser.ts | 45 ++++++++++++++--------- 1 file changed, 27 insertions(+), 18 deletions(-) diff --git a/src/lib/utils/markup/basicMarkupParser.ts b/src/lib/utils/markup/basicMarkupParser.ts index fd7fd14..2d70c41 100644 --- a/src/lib/utils/markup/basicMarkupParser.ts +++ b/src/lib/utils/markup/basicMarkupParser.ts @@ -78,20 +78,7 @@ function replaceAlexandriaNostrLinks(text: string): string { return `nostr:${bech32Match[0]}`; } } - // For non-Alexandria/localhost URLs, append (View here: nostr:) if a Nostr identifier is present - const hexMatch = url.match(hexPattern); - if (hexMatch) { - try { - const nevent = nip19.neventEncode({ id: hexMatch[0] }); - return `${url} (View here: nostr:${nevent})`; - } catch { - return url; - } - } - const bech32Match = url.match(bech32Pattern); - if (bech32Match) { - return `${url} (View here: nostr:${bech32Match[0]})`; - } + // For non-Alexandria/localhost URLs, just return the URL as-is return url; }); @@ -253,7 +240,18 @@ function processBasicFormatting(content: string): string { } // Only render if the url ends with a direct image extension if (IMAGE_EXTENSIONS.test(url.split("?")[0])) { - return `${alt}`; + return `
+
+
+
🖼️
+
Image
+
+
+ ${alt} + +
`; } // Otherwise, render as a clickable link return `${alt || url}`; @@ -290,7 +288,18 @@ function processBasicFormatting(content: string): string { } // Only render if the url ends with a direct image extension if (IMAGE_EXTENSIONS.test(clean.split("?")[0])) { - return `Embedded media`; + return `
+
+
+
🖼️
+
Image
+
+
+ Embedded media + +
`; } // Otherwise, render as a clickable link return `${clean}`; @@ -310,10 +319,10 @@ function processBasicFormatting(content: string): string { }, ); - // Process hashtags + // Process hashtags as clickable buttons processedText = processedText.replace( HASHTAG_REGEX, - '#$1', + '', ); // --- Improved List Grouping and Parsing --- From 577d8c832a7b33c9a7100585abc009db5d69b876 Mon Sep 17 00:00:00 2001 From: silberengel Date: Wed, 6 Aug 2025 08:31:46 +0200 Subject: [PATCH 06/27] fix relays and subscription search --- src/lib/components/CommentViewer.svelte | 46 ++--- src/lib/ndk.ts | 88 +++++++++- src/lib/utils/subscription_search.ts | 217 ++++++++++++++++++++---- src/routes/+layout.svelte | 24 ++- src/routes/events/+page.svelte | 27 ++- 5 files changed, 333 insertions(+), 69 deletions(-) diff --git a/src/lib/components/CommentViewer.svelte b/src/lib/components/CommentViewer.svelte index 078341c..20ace2c 100644 --- a/src/lib/components/CommentViewer.svelte +++ b/src/lib/components/CommentViewer.svelte @@ -294,12 +294,6 @@ let parsedContent = await parseBasicmarkup(content); - // Make images blurry until clicked - parsedContent = parsedContent.replace( - /]+)>/g, - '' - ); - return parsedContent; } @@ -313,24 +307,32 @@ >
- {#if getAuthorPicture(node.event.pubkey)} - {getAuthorName(node.event.pubkey)} (e.target as HTMLImageElement).style.display = 'none'} - /> - {:else} -
- - {getAuthorName(node.event.pubkey).charAt(0).toUpperCase()} - -
- {/if} +
- + ([]); export const activeInboxRelays = writable([]); export const activeOutboxRelays = writable([]); +// AI-NOTE: 2025-01-08 - Persistent relay storage to avoid recalculation +let persistentRelaySet: { inboxRelays: string[]; outboxRelays: string[] } | null = null; +let relaySetLastUpdated: number = 0; +const RELAY_SET_CACHE_DURATION = 5 * 60 * 1000; // 5 minutes +const RELAY_SET_STORAGE_KEY = 'alexandria/relay_set_cache'; + +/** + * Load persistent relay set from localStorage + */ +function loadPersistentRelaySet(): { relaySet: { inboxRelays: string[]; outboxRelays: string[] } | null; lastUpdated: number } { + try { + const stored = localStorage.getItem(RELAY_SET_STORAGE_KEY); + if (!stored) return { relaySet: null, lastUpdated: 0 }; + + const data = JSON.parse(stored); + const now = Date.now(); + + // Check if cache is expired + if (now - data.timestamp > RELAY_SET_CACHE_DURATION) { + localStorage.removeItem(RELAY_SET_STORAGE_KEY); + return { relaySet: null, lastUpdated: 0 }; + } + + return { relaySet: data.relaySet, lastUpdated: data.timestamp }; + } catch (error) { + console.warn('[NDK.ts] Failed to load persistent relay set:', error); + localStorage.removeItem(RELAY_SET_STORAGE_KEY); + return { relaySet: null, lastUpdated: 0 }; + } +} + +/** + * Save persistent relay set to localStorage + */ +function savePersistentRelaySet(relaySet: { inboxRelays: string[]; outboxRelays: string[] }): void { + try { + const data = { + relaySet, + timestamp: Date.now() + }; + localStorage.setItem(RELAY_SET_STORAGE_KEY, JSON.stringify(data)); + } catch (error) { + console.warn('[NDK.ts] Failed to save persistent relay set:', error); + } +} + +/** + * Clear persistent relay set from localStorage + */ +function clearPersistentRelaySet(): void { + try { + localStorage.removeItem(RELAY_SET_STORAGE_KEY); + } catch (error) { + console.warn('[NDK.ts] Failed to clear persistent relay set:', error); + } +} + // Subscribe to userStore changes and update ndkSignedIn accordingly userStore.subscribe((userState) => { ndkSignedIn.set(userState.signedIn); @@ -351,15 +408,39 @@ export async function getActiveRelaySet(ndk: NDK): Promise<{ inboxRelays: string /** * Updates the active relay stores and NDK pool with new relay URLs * @param ndk NDK instance + * @param forceUpdate Force update even if cached (default: false) */ -export async function updateActiveRelayStores(ndk: NDK): Promise { +export async function updateActiveRelayStores(ndk: NDK, forceUpdate: boolean = false): Promise { try { + // AI-NOTE: 2025-01-08 - Use persistent relay set to avoid recalculation + const now = Date.now(); + const cacheExpired = now - relaySetLastUpdated > RELAY_SET_CACHE_DURATION; + + // Load from persistent storage if not already loaded + if (!persistentRelaySet) { + const loaded = loadPersistentRelaySet(); + persistentRelaySet = loaded.relaySet; + relaySetLastUpdated = loaded.lastUpdated; + } + + if (!forceUpdate && persistentRelaySet && !cacheExpired) { + console.debug('[NDK.ts] updateActiveRelayStores: Using cached relay set'); + activeInboxRelays.set(persistentRelaySet.inboxRelays); + activeOutboxRelays.set(persistentRelaySet.outboxRelays); + return; + } + console.debug('[NDK.ts] updateActiveRelayStores: Starting relay store update'); // Get the active relay set from the relay management system const relaySet = await getActiveRelaySet(ndk); console.debug('[NDK.ts] updateActiveRelayStores: Got relay set:', relaySet); + // Cache the relay set + persistentRelaySet = relaySet; + relaySetLastUpdated = now; + savePersistentRelaySet(relaySet); // Save to persistent storage + // Update the stores with the new relay configuration activeInboxRelays.set(relaySet.inboxRelays); activeOutboxRelays.set(relaySet.outboxRelays); @@ -560,6 +641,11 @@ export function logout(user: NDKUser): void { activeInboxRelays.set([]); activeOutboxRelays.set([]); + // AI-NOTE: 2025-01-08 - Clear persistent relay set on logout + persistentRelaySet = null; + relaySetLastUpdated = 0; + clearPersistentRelaySet(); // Clear persistent storage + // Stop network monitoring stopNetworkStatusMonitoring(); diff --git a/src/lib/utils/subscription_search.ts b/src/lib/utils/subscription_search.ts index 17fa093..d992f5b 100644 --- a/src/lib/utils/subscription_search.ts +++ b/src/lib/utils/subscription_search.ts @@ -26,6 +26,17 @@ const normalizeUrl = (url: string): string => { return url.replace(/\/$/, ''); // Remove trailing slash }; +/** + * Filter out unwanted events from search results + * @param events Array of NDKEvent to filter + * @returns Filtered array of NDKEvent + */ +function filterUnwantedEvents(events: NDKEvent[]): NDKEvent[] { + return events.filter( + (event) => !isEmojiReaction(event) && event.kind !== 3 && event.kind !== 5, + ); +} + /** * Search for events by subscription type (d, t, n) */ @@ -35,6 +46,7 @@ export async function searchBySubscription( callbacks?: SearchCallbacks, abortSignal?: AbortSignal, ): Promise { + const startTime = Date.now(); // AI-NOTE: 2025-01-08 - Track search performance const normalizedSearchTerm = searchTerm.toLowerCase().trim(); console.log("subscription_search: Starting search:", { @@ -47,7 +59,22 @@ export async function searchBySubscription( const cachedResult = searchCache.get(searchType, normalizedSearchTerm); if (cachedResult) { console.log("subscription_search: Found cached result:", cachedResult); - return cachedResult; + // AI-NOTE: 2025-01-08 - For profile searches, clear cache if it's empty to force fresh search + if (searchType === "n" && cachedResult.events.length === 0) { + console.log("subscription_search: Clearing empty cached profile result to force fresh search"); + searchCache.clear(); // Clear all cache to force fresh search + } else if (searchType === "n" && cachedResult.events.length > 0 && cachedResult.secondOrder.length === 0) { + // AI-NOTE: 2025-01-08 - Clear cache if we have profile results but no second-order events + // This forces a fresh search that includes second-order searching + console.log("subscription_search: Clearing cached profile result with no second-order events to force fresh search"); + searchCache.clear(); + } else if (searchType === "n") { + // AI-NOTE: 2025-01-08 - For profile searches, always clear cache to ensure fresh second-order search + console.log("subscription_search: Clearing cache for profile search to ensure fresh second-order search"); + searchCache.clear(); + } else { + return cachedResult; + } } const ndk = get(ndkInstance); @@ -64,7 +91,7 @@ export async function searchBySubscription( searchState.timeoutId = setTimeout(() => { console.log("subscription_search: Search timeout reached"); cleanup(); - }, TIMEOUTS.SUBSCRIPTION_SEARCH); + }, searchType === "n" ? 5000 : TIMEOUTS.SUBSCRIPTION_SEARCH); // AI-NOTE: 2025-01-08 - Shorter timeout for profile searches // Check for abort signal if (abortSignal?.aborted) { @@ -125,7 +152,26 @@ export async function searchBySubscription( ); searchCache.set(searchType, normalizedSearchTerm, immediateResult); - // Start Phase 2 in background for additional results + // AI-NOTE: 2025-01-08 - For profile searches, return immediately when found + // but still start background search for second-order results + if (searchType === "n") { + console.log("subscription_search: Profile found, returning immediately but starting background second-order search"); + + // Start Phase 2 in background for second-order results + searchOtherRelaysInBackground( + searchType, + searchFilter, + searchState, + callbacks, + cleanup, + ); + + const elapsed = Date.now() - startTime; + console.log(`subscription_search: Profile search completed in ${elapsed}ms`); + return immediateResult; + } + + // Start Phase 2 in background for additional results (only for non-profile searches) searchOtherRelaysInBackground( searchType, searchFilter, @@ -137,8 +183,72 @@ export async function searchBySubscription( return immediateResult; } else { console.log( - "subscription_search: No results from primary relay, continuing to Phase 2", + "subscription_search: No results from primary relay", ); + + // AI-NOTE: 2025-01-08 - For profile searches, if no results found in search relays, + // try all relays as fallback + if (searchType === "n") { + console.log( + "subscription_search: No profile found in search relays, trying all relays", + ); + // Try with all relays as fallback + const allRelaySet = new NDKRelaySet(new Set(Array.from(ndk.pool.relays.values())) as any, ndk); + try { + const fallbackEvents = await ndk.fetchEvents( + searchFilter.filter, + { closeOnEose: true }, + allRelaySet, + ); + + console.log( + "subscription_search: Fallback search returned", + fallbackEvents.size, + "events", + ); + + processPrimaryRelayResults( + fallbackEvents, + searchType, + searchFilter.subscriptionType, + normalizedSearchTerm, + searchState, + abortSignal, + cleanup, + ); + + if (hasResults(searchState, searchType)) { + console.log( + "subscription_search: Found profile in fallback search, returning immediately", + ); + const fallbackResult = createSearchResult( + searchState, + searchType, + normalizedSearchTerm, + ); + searchCache.set(searchType, normalizedSearchTerm, fallbackResult); + const elapsed = Date.now() - startTime; + console.log(`subscription_search: Profile search completed in ${elapsed}ms (fallback)`); + return fallbackResult; + } + } catch (fallbackError) { + console.error("subscription_search: Fallback search failed:", fallbackError); + } + + console.log( + "subscription_search: Profile not found in any relays, returning empty result", + ); + const emptyResult = createEmptySearchResult(searchType, normalizedSearchTerm); + // AI-NOTE: 2025-01-08 - Don't cache empty profile results as they may be due to search issues + // rather than the profile not existing + const elapsed = Date.now() - startTime; + console.log(`subscription_search: Profile search completed in ${elapsed}ms (not found)`); + return emptyResult; + } else { + console.log( + "subscription_search: No results from primary relay, continuing to Phase 2", + ); + } } } catch (error) { console.error( @@ -153,13 +263,21 @@ export async function searchBySubscription( } // Always do Phase 2: Search all other relays in parallel - return searchOtherRelaysInBackground( + const result = await searchOtherRelaysInBackground( searchType, searchFilter, searchState, callbacks, cleanup, ); + + // AI-NOTE: 2025-01-08 - Log performance for non-profile searches + if (searchType !== "n") { + const elapsed = Date.now() - startTime; + console.log(`subscription_search: ${searchType} search completed in ${elapsed}ms`); + } + + return result; } /** @@ -253,7 +371,7 @@ async function createProfileSearchFilter( filter: { kinds: [0], authors: [decoded.data], - limit: SEARCH_LIMITS.SPECIFIC_PROFILE, + limit: 1, // AI-NOTE: 2025-01-08 - Only need 1 result for specific npub search }, subscriptionType: "npub-specific", }; @@ -273,7 +391,7 @@ async function createProfileSearchFilter( filter: { kinds: [0], authors: [npub], - limit: SEARCH_LIMITS.SPECIFIC_PROFILE, + limit: 1, // AI-NOTE: 2025-01-08 - Only need 1 result for specific npub search }, subscriptionType: "nip05-found", }; @@ -299,31 +417,38 @@ function createPrimaryRelaySet( searchType: SearchSubscriptionType, ndk: any, ): NDKRelaySet { - // Use the new relay management system - const searchRelays = [...get(activeInboxRelays), ...get(activeOutboxRelays)]; - console.debug('subscription_search: Active relay stores:', { - inboxRelays: get(activeInboxRelays), - outboxRelays: get(activeOutboxRelays), - searchRelays - }); - // Debug: Log all relays in NDK pool const poolRelays = Array.from(ndk.pool.relays.values()); console.debug('subscription_search: NDK pool relays:', poolRelays.map((r: any) => r.url)); if (searchType === "n") { - // For profile searches, use search relays first - const profileRelaySet = poolRelays.filter( + // AI-NOTE: 2025-01-08 - For profile searches, prioritize search relays for speed + // Use search relays first, then fall back to all relays if needed + const searchRelaySet = poolRelays.filter( (relay: any) => searchRelays.some( (searchRelay: string) => normalizeUrl(relay.url) === normalizeUrl(searchRelay), ), ); - console.debug('subscription_search: Profile relay set:', profileRelaySet.map((r: any) => r.url)); - return new NDKRelaySet(new Set(profileRelaySet) as any, ndk); + + if (searchRelaySet.length > 0) { + console.debug('subscription_search: Profile search - using search relays for speed:', searchRelaySet.map((r: any) => r.url)); + return new NDKRelaySet(new Set(searchRelaySet) as any, ndk); + } else { + // Fallback to all relays if search relays not available + console.debug('subscription_search: Profile search - fallback to all relays:', poolRelays.map((r: any) => r.url)); + return new NDKRelaySet(new Set(poolRelays) as any, ndk); + } } else { // For other searches, use active relays first + const searchRelays = [...get(activeInboxRelays), ...get(activeOutboxRelays)]; + console.debug('subscription_search: Active relay stores:', { + inboxRelays: get(activeInboxRelays), + outboxRelays: get(activeOutboxRelays), + searchRelays + }); + const activeRelaySet = poolRelays.filter( (relay: any) => searchRelays.some( @@ -534,11 +659,9 @@ function searchOtherRelaysInBackground( new Set( Array.from(ndk.pool.relays.values()).filter((relay: any) => { if (searchType === "n") { - // For profile searches, exclude search relays from fallback search - return !searchRelays.some( - (searchRelay: string) => - normalizeUrl(relay.url) === normalizeUrl(searchRelay), - ); + // AI-NOTE: 2025-01-08 - For profile searches, use ALL available relays + // Don't exclude any relays since we want maximum coverage + return true; } else { // For other searches, exclude community relays from fallback search return !communityRelays.some( @@ -652,6 +775,7 @@ function processProfileEoseResults( ) { const targetPubkey = dedupedProfiles[0]?.pubkey; if (targetPubkey) { + console.log("subscription_search: Triggering second-order search for npub-specific profile:", targetPubkey); performSecondOrderSearchInBackground( "n", dedupedProfiles, @@ -660,11 +784,14 @@ function processProfileEoseResults( targetPubkey, callbacks, ); + } else { + console.log("subscription_search: No targetPubkey found for second-order search"); } } else if (searchFilter.subscriptionType === "profile") { // For general profile searches, perform second-order search for each found profile for (const profile of dedupedProfiles) { if (profile.pubkey) { + console.log("subscription_search: Triggering second-order search for general profile:", profile.pubkey); performSecondOrderSearchInBackground( "n", dedupedProfiles, @@ -675,6 +802,8 @@ function processProfileEoseResults( ); } } + } else { + console.log("subscription_search: No second-order search triggered for subscription type:", searchFilter.subscriptionType); } return { @@ -784,6 +913,7 @@ async function performSecondOrderSearchInBackground( callbacks?: SearchCallbacks, ) { try { + console.log("subscription_search: Starting second-order search for", searchType, "with targetPubkey:", targetPubkey); const ndk = get(ndkInstance); let allSecondOrderEvents: NDKEvent[] = []; @@ -797,6 +927,8 @@ async function performSecondOrderSearchInBackground( const searchPromise = (async () => { if (searchType === "n" && targetPubkey) { + console.log("subscription_search: Searching for events mentioning pubkey:", targetPubkey); + // Search for events that mention this pubkey via p-tags const pTagFilter = { "#p": [targetPubkey] }; const pTagEvents = await ndk.fetchEvents( @@ -804,11 +936,25 @@ async function performSecondOrderSearchInBackground( { closeOnEose: true }, new NDKRelaySet(new Set(Array.from(ndk.pool.relays.values())), ndk), ); - // Filter out emoji reactions - const filteredEvents = Array.from(pTagEvents).filter( - (event) => !isEmojiReaction(event), + console.log("subscription_search: Found", pTagEvents.size, "events with p-tag for", targetPubkey); + + // AI-NOTE: 2025-01-08 - Also search for events written by this pubkey + const authorFilter = { authors: [targetPubkey] }; + const authorEvents = await ndk.fetchEvents( + authorFilter, + { closeOnEose: true }, + new NDKRelaySet(new Set(Array.from(ndk.pool.relays.values())), ndk), ); - allSecondOrderEvents = [...allSecondOrderEvents, ...filteredEvents]; + console.log("subscription_search: Found", authorEvents.size, "events written by", targetPubkey); + + // Filter out unwanted events from both sets + const filteredPTagEvents = filterUnwantedEvents(Array.from(pTagEvents)); + const filteredAuthorEvents = filterUnwantedEvents(Array.from(authorEvents)); + + console.log("subscription_search: After filtering unwanted events:", filteredPTagEvents.length, "p-tag events,", filteredAuthorEvents.length, "author events"); + + // Combine both sets of events + allSecondOrderEvents = [...filteredPTagEvents, ...filteredAuthorEvents]; } else if (searchType === "d") { // Parallel fetch for #e and #a tag events const relaySet = new NDKRelaySet( @@ -831,13 +977,9 @@ async function performSecondOrderSearchInBackground( ) : Promise.resolve([]), ]); - // Filter out emoji reactions - const filteredETagEvents = Array.from(eTagEvents).filter( - (event) => !isEmojiReaction(event), - ); - const filteredATagEvents = Array.from(aTagEvents).filter( - (event) => !isEmojiReaction(event), - ); + // Filter out unwanted events + const filteredETagEvents = filterUnwantedEvents(Array.from(eTagEvents)); + const filteredATagEvents = filterUnwantedEvents(Array.from(aTagEvents)); allSecondOrderEvents = [ ...allSecondOrderEvents, ...filteredETagEvents, @@ -866,6 +1008,8 @@ async function performSecondOrderSearchInBackground( .sort((a, b) => (b.created_at || 0) - (a.created_at || 0)) .slice(0, SEARCH_LIMITS.SECOND_ORDER_RESULTS); + console.log("subscription_search: Second-order search completed with", sortedSecondOrder.length, "results"); + // Update the search results with second-order events const result: SearchResult = { events: firstOrderEvents, @@ -882,7 +1026,10 @@ async function performSecondOrderSearchInBackground( // Notify UI of updated results if (callbacks?.onSecondOrderUpdate) { + console.log("subscription_search: Calling onSecondOrderUpdate callback with", sortedSecondOrder.length, "second-order events"); callbacks.onSecondOrderUpdate(result); + } else { + console.log("subscription_search: No onSecondOrderUpdate callback available"); } })(); diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index 2fff8a9..1ae83af 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -5,7 +5,10 @@ import { page } from "$app/stores"; import { Alert } from "flowbite-svelte"; import { HammerSolid } from "flowbite-svelte-icons"; - import { logCurrentRelayConfiguration } from "$lib/ndk"; + import { logCurrentRelayConfiguration, activeInboxRelays, activeOutboxRelays } from "$lib/ndk"; + + // Define children prop for Svelte 5 + let { children } = $props(); // Get standard metadata for OpenGraph tags let title = "Library of Alexandria"; @@ -16,12 +19,23 @@ let summary = "Alexandria is a digital library, utilizing Nostr events for curated publications and wiki pages."; + // Reactive effect to log relay configuration when stores change + $effect(() => { + const inboxRelays = $activeInboxRelays; + const outboxRelays = $activeOutboxRelays; + + // Only log if we have relays (not empty arrays) + if (inboxRelays.length > 0 || outboxRelays.length > 0) { + console.log('🔌 Relay Configuration Updated:'); + console.log('📥 Inbox Relays:', inboxRelays); + console.log('📤 Outbox Relays:', outboxRelays); + console.log(`📊 Total: ${inboxRelays.length} inbox, ${outboxRelays.length} outbox`); + } + }); + onMount(() => { const rect = document.body.getBoundingClientRect(); // document.body.style.height = `${rect.height}px`; - - // Log relay configuration when layout mounts - logCurrentRelayConfiguration(); }); @@ -47,5 +61,5 @@
- + {@render children()}
diff --git a/src/routes/events/+page.svelte b/src/routes/events/+page.svelte index 65e0c43..b06ea4e 100644 --- a/src/routes/events/+page.svelte +++ b/src/routes/events/+page.svelte @@ -151,21 +151,27 @@ import CommentViewer from "$lib/components/CommentViewer.svelte"; searchInProgress = loading || (results.length > 0 && secondOrder.length === 0); - // Show second-order search message when we have first-order results but no second-order yet + // AI-NOTE: 2025-01-08 - Only show second-order search message if we're actually searching + // Don't show it for cached results that have no second-order events if ( results.length > 0 && secondOrder.length === 0 && - searchTypeParam === "n" + searchTypeParam === "n" && + !loading // Only show message if we're actively searching, not for cached results ) { secondOrderSearchMessage = `Found ${results.length} profile(s). Starting second-order search for events mentioning these profiles...`; } else if ( results.length > 0 && secondOrder.length === 0 && - searchTypeParam === "d" + searchTypeParam === "d" && + !loading // Only show message if we're actively searching, not for cached results ) { secondOrderSearchMessage = `Found ${results.length} event(s). Starting second-order search for events referencing these events...`; } else if (secondOrder.length > 0) { secondOrderSearchMessage = null; + } else { + // Clear message if we have results but no second-order search is happening + secondOrderSearchMessage = null; } // Check community status for all search results @@ -347,9 +353,18 @@ import CommentViewer from "$lib/components/CommentViewer.svelte"; - // Log relay configuration when page mounts - onMount(() => { - logCurrentRelayConfiguration(); + // Reactive effect to log relay configuration when stores change + $effect(() => { + const inboxRelays = $activeInboxRelays; + const outboxRelays = $activeOutboxRelays; + + // Only log if we have relays (not empty arrays) + if (inboxRelays.length > 0 || outboxRelays.length > 0) { + console.log('🔌 Events Page - Relay Configuration Updated:'); + console.log('📥 Inbox Relays:', inboxRelays); + console.log('📤 Outbox Relays:', outboxRelays); + console.log(`📊 Total: ${inboxRelays.length} inbox, ${outboxRelays.length} outbox`); + } }); From 3eafdf0282f7a7bcf0380b45dce4bb43268f4d8e Mon Sep 17 00:00:00 2001 From: silberengel Date: Wed, 6 Aug 2025 08:51:49 +0200 Subject: [PATCH 07/27] fixed profile display --- src/lib/components/CommentViewer.svelte | 134 ++++++- src/lib/components/EventDetails.svelte | 28 +- src/routes/events/+page.svelte | 491 +++++++++++++++--------- 3 files changed, 457 insertions(+), 196 deletions(-) diff --git a/src/lib/components/CommentViewer.svelte b/src/lib/components/CommentViewer.svelte index 20ace2c..fb819e1 100644 --- a/src/lib/components/CommentViewer.svelte +++ b/src/lib/components/CommentViewer.svelte @@ -36,12 +36,24 @@ const npub = toNpub(pubkey); if (!npub) return; - const profile = await getUserMetadata(npub); + // Force fetch to ensure we get the latest profile data + const profile = await getUserMetadata(npub, true); const newProfiles = new Map(profiles); newProfiles.set(pubkey, profile); profiles = newProfiles; + + console.log(`[CommentViewer] Fetched profile for ${pubkey}:`, profile); } catch (err) { console.warn(`Failed to fetch profile for ${pubkey}:`, err); + // Set a fallback profile to avoid repeated failed requests + const fallbackProfile = { + name: `${pubkey.slice(0, 8)}...${pubkey.slice(-4)}`, + displayName: `${pubkey.slice(0, 8)}...${pubkey.slice(-4)}`, + picture: null + }; + const newProfiles = new Map(profiles); + newProfiles.set(pubkey, fallbackProfile); + profiles = newProfiles; } } @@ -54,6 +66,9 @@ comments = []; console.log(`[CommentViewer] Fetching comments for event: ${event.id}`); + console.log(`[CommentViewer] Event kind: ${event.kind}`); + console.log(`[CommentViewer] Event pubkey: ${event.pubkey}`); + console.log(`[CommentViewer] Available relays: ${$activeInboxRelays.length}`); // Wait for relays to be available let attempts = 0; @@ -69,12 +84,35 @@ } try { - activeSub = $ndkInstance.subscribe({ - kinds: [1, 1111], - "#e": [event.id], - }); + // Try multiple filter approaches to find comments + const filters = [ + // Standard comment filter + { + kinds: [1, 1111], + "#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); + + // Try the first filter (standard comment search) + activeSub = $ndkInstance.subscribe(filters[0]); const timeout = setTimeout(() => { + console.log(`[CommentViewer] Subscription timeout - no comments found`); if (activeSub) { activeSub.stop(); activeSub = null; @@ -84,8 +122,21 @@ activeSub.on("event", (commentEvent: NDKEvent) => { console.log(`[CommentViewer] Received comment: ${commentEvent.id}`); - comments = [...comments, commentEvent]; - fetchProfile(commentEvent.pubkey); + console.log(`[CommentViewer] Comment kind: ${commentEvent.kind}`); + console.log(`[CommentViewer] Comment pubkey: ${commentEvent.pubkey}`); + console.log(`[CommentViewer] Comment content preview: ${commentEvent.content?.slice(0, 100)}...`); + + // Check if this event actually references our target event + const eTags = commentEvent.getMatchingTags("e"); + const referencesTarget = eTags.some(tag => tag[1] === event.id); + + if (referencesTarget) { + console.log(`[CommentViewer] Comment references target event - adding to comments`); + comments = [...comments, commentEvent]; + fetchProfile(commentEvent.pubkey); + } else { + console.log(`[CommentViewer] Comment does not reference target event - skipping`); + } }); activeSub.on("eose", () => { @@ -96,6 +147,14 @@ activeSub = null; } loading = false; + + // Pre-fetch all profiles after comments are loaded + preFetchAllProfiles(); + + // AI-NOTE: 2025-01-24 - Test for comments if none were found + if (comments.length === 0) { + testForComments(); + } }); activeSub.on("error", (err: any) => { @@ -116,6 +175,60 @@ } } + // Pre-fetch all profiles for comments + async function preFetchAllProfiles() { + const uniquePubkeys = new Set(); + comments.forEach(comment => { + if (comment.pubkey && !profiles.has(comment.pubkey)) { + uniquePubkeys.add(comment.pubkey); + } + }); + + console.log(`[CommentViewer] Pre-fetching ${uniquePubkeys.size} profiles`); + + // Fetch profiles in parallel + const profilePromises = Array.from(uniquePubkeys).map(pubkey => fetchProfile(pubkey)); + await Promise.allSettled(profilePromises); + + 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, + }); + + let testComments = 0; + + testSub.on("event", (testEvent: NDKEvent) => { + testComments++; + console.log(`[CommentViewer] Test found event: ${testEvent.id}, kind: ${testEvent.kind}`); + }); + + testSub.on("eose", () => { + console.log(`[CommentViewer] Test search found ${testComments} potential comments`); + testSub.stop(); + }); + + // Stop the test after 5 seconds + setTimeout(() => { + testSub.stop(); + }, 5000); + + } catch (err) { + console.error(`[CommentViewer] Test search error:`, err); + } + } + // Build threaded comment structure function buildCommentThread(events: NDKEvent[]): CommentNode[] { if (events.length === 0) return []; @@ -220,6 +333,9 @@ return neventEncode(commentEvent, $activeInboxRelays); } + // 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)}`); @@ -275,7 +391,9 @@ function getAuthorName(pubkey: string): string { const profile = profiles.get(pubkey); - return profile?.displayName || profile?.name || `${pubkey.slice(0, 8)}...${pubkey.slice(-4)}`; + if (profile?.displayName) return profile.displayName; + if (profile?.name) return profile.name; + return `${pubkey.slice(0, 8)}...${pubkey.slice(-4)}`; } function getAuthorPicture(pubkey: string): string | null { diff --git a/src/lib/components/EventDetails.svelte b/src/lib/components/EventDetails.svelte index 4bd78e4..c14d7e6 100644 --- a/src/lib/components/EventDetails.svelte +++ b/src/lib/components/EventDetails.svelte @@ -449,20 +449,22 @@ -
- {#if event.kind !== 0} - Content: -
- {@html showFullContent ? parsedContent : contentPreview} - {#if !showFullContent && parsedContent.length > 250} - - {/if} + {#if event.kind !== 0} +
+
+ Content: +
+ {@html showFullContent ? parsedContent : contentPreview} + {#if !showFullContent && parsedContent.length > 250} + + {/if} +
- {/if} -
+
+ {/if} {#if event.kind === 0} diff --git a/src/routes/events/+page.svelte b/src/routes/events/+page.svelte index b06ea4e..389b8bd 100644 --- a/src/routes/events/+page.svelte +++ b/src/routes/events/+page.svelte @@ -11,7 +11,7 @@ import CommentViewer from "$lib/components/CommentViewer.svelte"; import { userStore } from "$lib/stores/userStore"; import { userBadge } from "$lib/snippets/UserSnippets.svelte"; - import { getMatchingTags, toNpub } from "$lib/utils/nostrUtils"; + import { getMatchingTags, toNpub, getUserMetadata } from "$lib/utils/nostrUtils"; import EventInput from "$lib/components/EventInput.svelte"; import { userPubkey, isLoggedIn } from "$lib/stores/authStore.Svelte"; import CopyToClipboard from "$lib/components/util/CopyToClipboard.svelte"; @@ -75,6 +75,25 @@ import CommentViewer from "$lib/components/CommentViewer.svelte"; } else { profile = null; } + + // AI-NOTE: 2025-01-24 - Ensure profile is cached for the event author + if (newEvent.pubkey) { + cacheProfileForPubkey(newEvent.pubkey); + } + } + + // AI-NOTE: 2025-01-24 - Function to ensure profile is cached for a pubkey + async function cacheProfileForPubkey(pubkey: string) { + try { + const npub = toNpub(pubkey); + if (npub) { + // Force fetch to ensure profile is cached + await getUserMetadata(npub, true); + console.log(`[Events Page] Cached profile for pubkey: ${pubkey}`); + } + } catch (error) { + console.warn(`[Events Page] Failed to cache profile for ${pubkey}:`, error); + } } // Use Svelte 5 idiomatic effect to update searchValue when $page.url.searchParams.get('id') changes @@ -185,11 +204,32 @@ import CommentViewer from "$lib/components/CommentViewer.svelte"; checkCommunityStatusForResults(tTagEvents); } + // AI-NOTE: 2025-01-24 - Cache profiles for all search results + cacheProfilesForEvents([...results, ...secondOrder, ...tTagEvents]); + // Don't clear the current event - let the user continue viewing it // event = null; // profile = null; } + // AI-NOTE: 2025-01-24 - Function to cache profiles for multiple events + async function cacheProfilesForEvents(events: NDKEvent[]) { + const uniquePubkeys = new Set(); + events.forEach(event => { + if (event.pubkey) { + uniquePubkeys.add(event.pubkey); + } + }); + + console.log(`[Events Page] Caching profiles for ${uniquePubkeys.size} unique pubkeys`); + + // Cache profiles in parallel + const cachePromises = Array.from(uniquePubkeys).map(pubkey => cacheProfileForPubkey(pubkey)); + await Promise.allSettled(cachePromises); + + console.log(`[Events Page] Profile caching complete`); + } + function handleClear() { searchType = null; searchTerm = null; @@ -233,48 +273,47 @@ import CommentViewer from "$lib/components/CommentViewer.svelte"; originalEventIds: Set, originalAddresses: Set, ): string { - // Check if this event has e-tags referencing original events - const eTags = getMatchingTags(event, "e"); - for (const tag of eTags) { - if (originalEventIds.has(tag[1])) { - return "Reply/Reference (e-tag)"; - } - } + const eTags = event.getMatchingTags("e"); + const aTags = event.getMatchingTags("a"); - // Check if this event has a-tags or e-tags referencing original events - let tags = getMatchingTags(event, "a"); - if (tags.length === 0) { - tags = getMatchingTags(event, "e"); + if (eTags.length > 0) { + const referencedEventId = eTags[eTags.length - 1][1]; + if (originalEventIds.has(referencedEventId)) { + return "Reply"; + } } - for (const tag of tags) { - if (originalAddresses.has(tag[1])) { - return "Reply/Reference (a-tag)"; + if (aTags.length > 0) { + const referencedAddress = aTags[aTags.length - 1][1]; + if (originalAddresses.has(referencedAddress)) { + return "Quote"; } } - // Check if this event has content references - if (event.content) { - for (const id of originalEventIds) { - const neventPattern = new RegExp(`nevent1[a-z0-9]{50,}`, "i"); - const notePattern = new RegExp(`note1[a-z0-9]{50,}`, "i"); - if ( - neventPattern.test(event.content) || - notePattern.test(event.content) - ) { - return "Content Reference"; - } - } + return "Reference"; + } - for (const address of originalAddresses) { - const naddrPattern = new RegExp(`naddr1[a-z0-9]{50,}`, "i"); - if (naddrPattern.test(event.content)) { - return "Content Reference"; - } - } + // AI-NOTE: 2025-01-24 - Function to parse profile content from kind 0 events + function parseProfileContent(event: NDKEvent): { + name?: string; + display_name?: string; + about?: string; + picture?: string; + banner?: string; + website?: string; + lud16?: string; + nip05?: string; + } | null { + if (event.kind !== 0 || !event.content) { + return null; } - return "Reference"; + try { + return JSON.parse(event.content); + } catch (error) { + console.warn("Failed to parse profile content:", error); + return null; + } } function getNeventUrl(event: NDKEvent): string { @@ -427,6 +466,7 @@ import CommentViewer from "$lib/components/CommentViewer.svelte";
{#each searchResults as result, index} + {@const profileData = parseProfileContent(result)}
- {#if getSummary(result)} -
- {getSummary(result)} + {#if result.kind === 0 && profileData} +
+ {#if profileData.picture} + Profile { + (e.target as HTMLImageElement).style.display = 'none'; + }} + /> + {:else} +
+ + {(profileData.display_name || profileData.name || result.pubkey.slice(0, 1)).toUpperCase()} + +
+ {/if} +
+ {#if profileData.display_name || profileData.name} + + {profileData.display_name || profileData.name} + + {/if} + {#if profileData.about} + + {profileData.about} + + {/if} +
- {/if} - {#if getDeferralNaddr(result)} -
- Read - { - e.stopPropagation(); - navigateToPublication( - getDeferralNaddr(result) || "", - ); - }} - onkeydown={(e) => { - if (e.key === "Enter" || e.key === " ") { - e.preventDefault(); + {:else} + {#if getSummary(result)} +
+ {getSummary(result)} +
+ {/if} + {#if getDeferralNaddr(result)} +
+ Read + { e.stopPropagation(); navigateToPublication( getDeferralNaddr(result) || "", ); - } - }} - tabindex="0" - role="button" + }} + onkeydown={(e) => { + if (e.key === "Enter" || e.key === " ") { + e.preventDefault(); + e.stopPropagation(); + navigateToPublication( + getDeferralNaddr(result) || "", + ); + } + }} + tabindex="0" + role="button" + > + {getDeferralNaddr(result)} + +
+ {/if} + {#if isAddressableEvent(result)} +
- {getDeferralNaddr(result)} - -
- {/if} - {#if isAddressableEvent(result)} -
- -
- {/if} - {#if result.content} -
- {result.content.slice(0, 200)}{result.content.length > - 200 - ? "..." - : ""} -
+ +
+ {/if} + {#if result.content} +
+ {result.content.slice(0, 200)}{result.content.length > + 200 + ? "..." + : ""} +
+ {/if} {/if}
@@ -551,6 +624,7 @@ import CommentViewer from "$lib/components/CommentViewer.svelte";

{#each secondOrderResults as result, index} + {@const profileData = parseProfileContent(result)} @@ -675,6 +782,7 @@ import CommentViewer from "$lib/components/CommentViewer.svelte";

{#each tTagResults as result, index} + {@const profileData = parseProfileContent(result)}
- {#if getSummary(result)} -
- {getSummary(result)} + {#if result.kind === 0 && profileData} +
+ {#if profileData.picture} + Profile { + (e.target as HTMLImageElement).style.display = 'none'; + }} + /> + {:else} +
+ + {(profileData.display_name || profileData.name || result.pubkey.slice(0, 1)).toUpperCase()} + +
+ {/if} +
+ {#if profileData.display_name || profileData.name} + + {profileData.display_name || profileData.name} + + {/if} + {#if profileData.about} + + {profileData.about} + + {/if} +
- {/if} - {#if getDeferralNaddr(result)} -
- Read - { - e.stopPropagation(); - navigateToPublication( - getDeferralNaddr(result) || "", - ); - }} - onkeydown={(e) => { - if (e.key === "Enter" || e.key === " ") { - e.preventDefault(); + {:else} + {#if getSummary(result)} +
+ {getSummary(result)} +
+ {/if} + {#if getDeferralNaddr(result)} +
+ Read + { e.stopPropagation(); navigateToPublication( getDeferralNaddr(result) || "", ); - } - }} - tabindex="0" - role="button" + }} + onkeydown={(e) => { + if (e.key === "Enter" || e.key === " ") { + e.preventDefault(); + e.stopPropagation(); + navigateToPublication( + getDeferralNaddr(result) || "", + ); + } + }} + tabindex="0" + role="button" + > + {getDeferralNaddr(result)} + +
+ {/if} + {#if isAddressableEvent(result)} +
- {getDeferralNaddr(result)} - -
- {/if} - {#if isAddressableEvent(result)} -
- -
- {/if} - {#if result.content} -
- {result.content.slice(0, 200)}{result.content.length > - 200 - ? "..." - : ""} -
+ +
+ {/if} + {#if result.content} +
+ {result.content.slice(0, 200)}{result.content.length > + 200 + ? "..." + : ""} +
+ {/if} {/if}
From 11c605b5eff23870e41aea99d4e28e0e757bd5c2 Mon Sep 17 00:00:00 2001 From: silberengel Date: Wed, 6 Aug 2025 09:21:57 +0200 Subject: [PATCH 08/27] improved search some more --- src/lib/components/EventSearch.svelte | 132 ++++++++++++++++++++++++++ src/lib/utils/search_constants.ts | 7 +- src/lib/utils/subscription_search.ts | 50 +++++----- src/routes/events/+page.svelte | 12 ++- 4 files changed, 171 insertions(+), 30 deletions(-) diff --git a/src/lib/components/EventSearch.svelte b/src/lib/components/EventSearch.svelte index 10f888b..67dece6 100644 --- a/src/lib/components/EventSearch.svelte +++ b/src/lib/components/EventSearch.svelte @@ -488,6 +488,61 @@ searchType, searchTerm, }); + + // AI-NOTE: 2025-01-24 - Check cache first for profile searches to provide immediate response + if (searchType === "n") { + try { + const { getUserMetadata } = await import("$lib/utils/nostrUtils"); + const cachedProfile = await getUserMetadata(searchTerm, false); + if (cachedProfile && cachedProfile.name) { + console.log("EventSearch: Found cached profile, displaying immediately:", cachedProfile); + + // Create a mock NDKEvent for the cached profile + const { NDKEvent } = await import("@nostr-dev-kit/ndk"); + const { nip19 } = await import("$lib/utils/nostrUtils"); + + // Decode the npub to get the actual pubkey + let pubkey = searchTerm; + try { + const decoded = nip19.decode(searchTerm); + if (decoded && decoded.type === "npub") { + pubkey = decoded.data; + } + } catch (error) { + console.warn("EventSearch: Failed to decode npub for mock event:", error); + } + + const mockEvent = new NDKEvent(undefined, { + kind: 0, + pubkey: pubkey, + content: JSON.stringify(cachedProfile), + tags: [], + created_at: Math.floor(Date.now() / 1000), + id: "", // Will be computed by NDK + sig: "", // Will be computed by NDK + }); + + // Display the cached profile immediately + handleFoundEvent(mockEvent); + updateSearchState(false, true, 1, "profile-cached"); + + // AI-NOTE: 2025-01-24 - Still perform background search for second-order events + // but with better timeout handling to prevent hanging + setTimeout(async () => { + try { + await performBackgroundProfileSearch(searchType, searchTerm); + } catch (error) { + console.warn("EventSearch: Background profile search failed:", error); + } + }, 100); + + return; + } + } catch (error) { + console.warn("EventSearch: Cache check failed, proceeding with subscription search:", error); + } + } + isResetting = false; // Allow effects to run for new searches localError = null; updateSearchState(true); @@ -663,6 +718,83 @@ } } + // AI-NOTE: 2025-01-24 - Function to perform background profile search without blocking UI + async function performBackgroundProfileSearch( + searchType: "d" | "t" | "n", + searchTerm: string, + ) { + console.log("EventSearch: Performing background profile search:", { + searchType, + searchTerm, + }); + + try { + // Cancel existing search + if (currentAbortController) { + currentAbortController.abort(); + } + currentAbortController = new AbortController(); + + // AI-NOTE: 2025-01-24 - Add timeout to prevent hanging background searches + const timeoutPromise = new Promise((_, reject) => { + setTimeout(() => { + reject(new Error("Background search timeout")); + }, 10000); // 10 second timeout for background searches + }); + + const searchPromise = searchBySubscription( + searchType, + searchTerm, + { + onSecondOrderUpdate: (updatedResult) => { + console.log("EventSearch: Background second order update:", updatedResult); + // Only update if we have new results + if (updatedResult.events.length > 0) { + onSearchResults( + updatedResult.events, + updatedResult.secondOrder, + updatedResult.tTagEvents, + updatedResult.eventIds, + updatedResult.addresses, + updatedResult.searchType, + updatedResult.searchTerm, + ); + } + }, + onSubscriptionCreated: (sub) => { + console.log("EventSearch: Background subscription created:", sub); + if (activeSub) { + activeSub.stop(); + } + activeSub = sub; + }, + }, + currentAbortController.signal, + ); + + // Race between search and timeout + const result = await Promise.race([searchPromise, timeoutPromise]) as any; + + console.log("EventSearch: Background search completed:", result); + + // Only update results if we have new data + if (result.events.length > 0) { + onSearchResults( + result.events, + result.secondOrder, + result.tTagEvents, + result.eventIds, + result.addresses, + result.searchType, + result.searchTerm, + ); + } + } catch (error) { + console.warn("EventSearch: Background profile search failed:", error); + } + } + + // Search utility functions function handleClear() { isResetting = true; searchQuery = ""; diff --git a/src/lib/utils/search_constants.ts b/src/lib/utils/search_constants.ts index 663e985..cc6f677 100644 --- a/src/lib/utils/search_constants.ts +++ b/src/lib/utils/search_constants.ts @@ -17,7 +17,7 @@ export const TIMEOUTS = { SUBSCRIPTION_SEARCH: 10000, /** Timeout for second-order search operations */ - SECOND_ORDER_SEARCH: 5000, + SECOND_ORDER_SEARCH: 3000, // AI-NOTE: 2025-01-24 - Reduced timeout since we limit scope /** Timeout for relay diagnostics */ RELAY_DIAGNOSTICS: 5000, @@ -44,7 +44,10 @@ export const SEARCH_LIMITS = { SPECIFIC_PROFILE: 10, /** Limit for general profile searches */ - GENERAL_PROFILE: 500, + GENERAL_PROFILE: 100, // AI-NOTE: 2025-01-24 - Reduced from 500 to prevent wild searches + + /** Limit for general content searches (t-tag, d-tag, etc.) */ + GENERAL_CONTENT: 100, // AI-NOTE: 2025-01-24 - Added limit for all content searches /** Limit for community relay checks */ COMMUNITY_CHECK: 1, diff --git a/src/lib/utils/subscription_search.ts b/src/lib/utils/subscription_search.ts index d992f5b..d07067e 100644 --- a/src/lib/utils/subscription_search.ts +++ b/src/lib/utils/subscription_search.ts @@ -59,19 +59,11 @@ export async function searchBySubscription( const cachedResult = searchCache.get(searchType, normalizedSearchTerm); if (cachedResult) { console.log("subscription_search: Found cached result:", cachedResult); - // AI-NOTE: 2025-01-08 - For profile searches, clear cache if it's empty to force fresh search - if (searchType === "n" && cachedResult.events.length === 0) { - console.log("subscription_search: Clearing empty cached profile result to force fresh search"); - searchCache.clear(); // Clear all cache to force fresh search - } else if (searchType === "n" && cachedResult.events.length > 0 && cachedResult.secondOrder.length === 0) { - // AI-NOTE: 2025-01-08 - Clear cache if we have profile results but no second-order events - // This forces a fresh search that includes second-order searching - console.log("subscription_search: Clearing cached profile result with no second-order events to force fresh search"); - searchCache.clear(); - } else if (searchType === "n") { - // AI-NOTE: 2025-01-08 - For profile searches, always clear cache to ensure fresh second-order search - console.log("subscription_search: Clearing cache for profile search to ensure fresh second-order search"); - searchCache.clear(); + // AI-NOTE: 2025-01-24 - For profile searches, return cached results immediately + // The EventSearch component now handles cache checking before calling this function + if (searchType === "n") { + console.log("subscription_search: Returning cached profile result immediately"); + return cachedResult; } else { return cachedResult; } @@ -91,7 +83,7 @@ export async function searchBySubscription( searchState.timeoutId = setTimeout(() => { console.log("subscription_search: Search timeout reached"); cleanup(); - }, searchType === "n" ? 5000 : TIMEOUTS.SUBSCRIPTION_SEARCH); // AI-NOTE: 2025-01-08 - Shorter timeout for profile searches + }, TIMEOUTS.SUBSCRIPTION_SEARCH); // AI-NOTE: 2025-01-24 - Use standard timeout since cache is checked first // Check for abort signal if (abortSignal?.aborted) { @@ -332,7 +324,7 @@ async function createSearchFilter( switch (searchType) { case "d": { const dFilter = { - filter: { "#d": [normalizedSearchTerm] }, + filter: { "#d": [normalizedSearchTerm], limit: SEARCH_LIMITS.GENERAL_CONTENT }, subscriptionType: "d-tag", }; console.log("subscription_search: Created d-tag filter:", dFilter); @@ -340,7 +332,7 @@ async function createSearchFilter( } case "t": { const tFilter = { - filter: { "#t": [normalizedSearchTerm] }, + filter: { "#t": [normalizedSearchTerm], limit: SEARCH_LIMITS.GENERAL_CONTENT }, subscriptionType: "t-tag", }; console.log("subscription_search: Created t-tag filter:", tFilter); @@ -929,21 +921,33 @@ async function performSecondOrderSearchInBackground( if (searchType === "n" && targetPubkey) { console.log("subscription_search: Searching for events mentioning pubkey:", targetPubkey); + // AI-NOTE: 2025-01-24 - Use only active relays for second-order profile search to prevent hanging + const activeRelays = [...get(activeInboxRelays), ...get(activeOutboxRelays)]; + const availableRelays = activeRelays + .map(url => ndk.pool.relays.get(url)) + .filter((relay): relay is any => relay !== undefined); + const relaySet = new NDKRelaySet( + new Set(availableRelays), + ndk + ); + + console.log("subscription_search: Using", activeRelays.length, "active relays for second-order search"); + // Search for events that mention this pubkey via p-tags - const pTagFilter = { "#p": [targetPubkey] }; + const pTagFilter = { "#p": [targetPubkey], limit: 50 }; // AI-NOTE: 2025-01-24 - Limit results to prevent hanging const pTagEvents = await ndk.fetchEvents( pTagFilter, { closeOnEose: true }, - new NDKRelaySet(new Set(Array.from(ndk.pool.relays.values())), ndk), + relaySet, ); console.log("subscription_search: Found", pTagEvents.size, "events with p-tag for", targetPubkey); - // AI-NOTE: 2025-01-08 - Also search for events written by this pubkey - const authorFilter = { authors: [targetPubkey] }; + // AI-NOTE: 2025-01-24 - Also search for events written by this pubkey with limit + const authorFilter = { authors: [targetPubkey], limit: 50 }; // AI-NOTE: 2025-01-24 - Limit results to prevent hanging const authorEvents = await ndk.fetchEvents( authorFilter, { closeOnEose: true }, - new NDKRelaySet(new Set(Array.from(ndk.pool.relays.values())), ndk), + relaySet, ); console.log("subscription_search: Found", authorEvents.size, "events written by", targetPubkey); @@ -964,14 +968,14 @@ async function performSecondOrderSearchInBackground( const [eTagEvents, aTagEvents] = await Promise.all([ eventIds.size > 0 ? ndk.fetchEvents( - { "#e": Array.from(eventIds) }, + { "#e": Array.from(eventIds), limit: SEARCH_LIMITS.SECOND_ORDER_RESULTS }, { closeOnEose: true }, relaySet, ) : Promise.resolve([]), addresses.size > 0 ? ndk.fetchEvents( - { "#a": Array.from(addresses) }, + { "#a": Array.from(addresses), limit: SEARCH_LIMITS.SECOND_ORDER_RESULTS }, { closeOnEose: true }, relaySet, ) diff --git a/src/routes/events/+page.svelte b/src/routes/events/+page.svelte index 389b8bd..f37d5a8 100644 --- a/src/routes/events/+page.svelte +++ b/src/routes/events/+page.svelte @@ -453,15 +453,17 @@ import CommentViewer from "$lib/components/CommentViewer.svelte"; {#if searchResults.length > 0}
- + {#if searchType === "n"} - Search Results for name: "{searchTerm}" ({searchResults.length} profiles) + Search Results for name: "{searchTerm && searchTerm.length > 50 ? searchTerm.slice(0, 50) + '...' : searchTerm || ''}" ({searchResults.length} profiles) {:else if searchType === "t"} - Search Results for t-tag: "{searchTerm}" ({searchResults.length} + Search Results for t-tag: "{searchTerm && searchTerm.length > 50 ? searchTerm.slice(0, 50) + '...' : searchTerm || ''}" ({searchResults.length} events) {:else} - Search Results for d-tag: "{searchTerm || - dTagValue?.toLowerCase()}" ({searchResults.length} events) + Search Results for d-tag: "{(() => { + const term = searchTerm || dTagValue?.toLowerCase() || ''; + return term.length > 50 ? term.slice(0, 50) + '...' : term; + })()}" ({searchResults.length} events) {/if}
From 715efad96e6323d41a274810dfd839c9cd766202 Mon Sep 17 00:00:00 2001 From: silberengel Date: Wed, 6 Aug 2025 17:12:27 +0200 Subject: [PATCH 09/27] 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": From 0b45c3e97c3ab70c095df0f1304f7dd2a665da37 Mon Sep 17 00:00:00 2001 From: silberengel Date: Wed, 6 Aug 2025 18:05:04 +0200 Subject: [PATCH 10/27] Personal notifications added to "View Profile" --- src/lib/components/EventDetails.svelte | 4 + src/lib/components/Notifications.svelte | 451 ++++++++++++++++++++++++ 2 files changed, 455 insertions(+) create mode 100644 src/lib/components/Notifications.svelte diff --git a/src/lib/components/EventDetails.svelte b/src/lib/components/EventDetails.svelte index c14d7e6..3a8801e 100644 --- a/src/lib/components/EventDetails.svelte +++ b/src/lib/components/EventDetails.svelte @@ -14,6 +14,7 @@ import CopyToClipboard from "$lib/components/util/CopyToClipboard.svelte"; import { navigateToEvent } from "$lib/utils/nostrEventService"; import ContainingIndexes from "$lib/components/util/ContainingIndexes.svelte"; + import Notifications from "$lib/components/Notifications.svelte"; const { event, @@ -473,6 +474,9 @@ {profile} identifiers={getIdentifiers(event, profile)} /> + + + {/if} diff --git a/src/lib/components/Notifications.svelte b/src/lib/components/Notifications.svelte new file mode 100644 index 0000000..9566bcb --- /dev/null +++ b/src/lib/components/Notifications.svelte @@ -0,0 +1,451 @@ + + +{#if isOwnProfile && $userStore.signedIn} +
+
+ Notifications + + +
+ + +
+
+ + {#if loading} +
+
+ Loading notifications... +
+ {:else if error} +
+

Error loading notifications: {error}

+
+ {:else if notifications.length === 0} +
+

No notifications {notificationMode === "to-me" ? "received" : "sent"} found.

+
+ {:else} +
+
+ {#each notifications.slice(0, 10) as notification} + {@const authorProfile = authorProfiles.get(notification.pubkey)} +
+
+ +
+ {#if authorProfile?.picture} + Author avatar { + (e.target as HTMLImageElement).style.display = 'none'; + }} + /> + {:else} +
+ + {(authorProfile?.displayName || authorProfile?.name || notification.pubkey.slice(0, 1)).toUpperCase()} + +
+ {/if} +
+ + +
+
+ + {getNotificationType(notification)} + + + {notification.created_at ? formatDate(notification.created_at) : "Unknown date"} + +
+ + +
+ + {authorProfile?.displayName || authorProfile?.name || `${notification.pubkey.slice(0, 8)}...${notification.pubkey.slice(-4)}`} + + {#if authorProfile?.name && authorProfile?.displayName && authorProfile.name !== authorProfile.displayName} + + (@{authorProfile.name}) + + {/if} +
+ + {#if notification.content} +
+ {truncateContent(notification.content)} +
+ {/if} + +
+ + + {getNeventUrl(notification).slice(0, 16)}... + +
+
+
+
+ {/each} +
+ + {#if notifications.length > 10} +
+ Showing 10 of {notifications.length} notifications {notificationMode === "to-me" ? "received" : "sent"}. Scroll to see more. +
+ {/if} +
+ {/if} +
+{/if} \ No newline at end of file From 1da3f6ba0d95b50276ff868ccc887171816265d8 Mon Sep 17 00:00:00 2001 From: silberengel Date: Wed, 6 Aug 2025 23:50:09 +0200 Subject: [PATCH 11/27] Worked on the notifications some more --- src/lib/components/EventDetails.svelte | 2 +- src/lib/components/Notifications.svelte | 687 ++++++++++++++---------- 2 files changed, 397 insertions(+), 292 deletions(-) diff --git a/src/lib/components/EventDetails.svelte b/src/lib/components/EventDetails.svelte index 3a8801e..dcdd0eb 100644 --- a/src/lib/components/EventDetails.svelte +++ b/src/lib/components/EventDetails.svelte @@ -454,7 +454,7 @@
Content: -
+
{@html showFullContent ? parsedContent : contentPreview} {#if !showFullContent && parsedContent.length > 250} - + +
+ {#each ["to-me", "from-me", "public-messages"] as mode} + {@const modeLabel = mode === "to-me" ? "To Me" : mode === "from-me" ? "From Me" : "Public Messages"} + + {/each}
{#if loading}
- Loading notifications... + + Loading {notificationMode === "public-messages" ? "public messages" : "notifications"}... +
{:else if error}
-

Error loading notifications: {error}

-
- {:else if notifications.length === 0} -
-

No notifications {notificationMode === "to-me" ? "received" : "sent"} found.

+

Error loading {notificationMode === "public-messages" ? "public messages" : "notifications"}: {error}

- {:else} -
-
- {#each notifications.slice(0, 10) as notification} - {@const authorProfile = authorProfiles.get(notification.pubkey)} -
-
- -
- {#if authorProfile?.picture} - Author avatar { - (e.target as HTMLImageElement).style.display = 'none'; - }} - /> - {:else} -
- - {(authorProfile?.displayName || authorProfile?.name || notification.pubkey.slice(0, 1)).toUpperCase()} + {:else if notificationMode === "public-messages"} + {#if publicMessages.length === 0} +
+

No public messages found.

+
+ {:else} +
+ {#if filteredByUser} +
+
+ + Filtered by user: {authorProfiles.get(filteredByUser)?.displayName || authorProfiles.get(filteredByUser)?.name || `${filteredByUser.slice(0, 8)}...${filteredByUser.slice(-4)}`} + + +
+
+ {/if} +
+ {#each filteredMessages.slice(0, 20) as message} + {@const authorProfile = authorProfiles.get(message.pubkey)} + {@const isFromUser = message.pubkey === $userStore.pubkey} +
+
+ +
+ {#if authorProfile?.picture} + Author avatar (e.target as HTMLImageElement).style.display = 'none'} + /> + {:else} +
+ + {(authorProfile?.displayName || authorProfile?.name || message.pubkey.slice(0, 1)).toUpperCase()} + +
+ {/if} + + + {#if !isFromUser} +
+ +
+ {/if} +
+ + +
+
+ + {isFromUser ? 'Your Message' : 'Public Message'} + + + {message.created_at ? formatDate(message.created_at) : "Unknown date"} + +
+ + +
+ + {authorProfile?.displayName || authorProfile?.name || `${message.pubkey.slice(0, 8)}...${message.pubkey.slice(-4)}`} + {#if authorProfile?.name && authorProfile?.displayName && authorProfile.name !== authorProfile.displayName} + + (@{authorProfile.name}) + + {/if}
- {/if} + + {#if message.content} +
+ {truncateContent(message.content)} +
+ {/if} + +
+ + + {getNeventUrl(message).slice(0, 16)}... + +
+
- - -
-
- - {getNotificationType(notification)} - - - {notification.created_at ? formatDate(notification.created_at) : "Unknown date"} - +
+ {/each} +
+ + {#if filteredMessages.length > 20} +
+ Showing 20 of {filteredMessages.length} messages {filteredByUser ? `(filtered)` : ''}. Scroll to see more. +
+ {/if} +
+ {/if} + {:else} + {#if notifications.length === 0} +
+

No notifications {notificationMode === "to-me" ? "received" : "sent"} found.

+
+ {:else} +
+
+ {#each notifications.slice(0, 10) as notification} + {@const authorProfile = authorProfiles.get(notification.pubkey)} +
+
+ +
+ {#if authorProfile?.picture} + Author avatar (e.target as HTMLImageElement).style.display = 'none'} + /> + {:else} +
+ + {(authorProfile?.displayName || authorProfile?.name || notification.pubkey.slice(0, 1)).toUpperCase()} + +
+ {/if}
- -
- - {authorProfile?.displayName || authorProfile?.name || `${notification.pubkey.slice(0, 8)}...${notification.pubkey.slice(-4)}`} - - {#if authorProfile?.name && authorProfile?.displayName && authorProfile.name !== authorProfile.displayName} - - (@{authorProfile.name}) + +
+
+ + {getNotificationType(notification)} + + {notification.created_at ? formatDate(notification.created_at) : "Unknown date"} + +
+ + +
+ + {authorProfile?.displayName || authorProfile?.name || `${notification.pubkey.slice(0, 8)}...${notification.pubkey.slice(-4)}`} + + {#if authorProfile?.name && authorProfile?.displayName && authorProfile.name !== authorProfile.displayName} + + (@{authorProfile.name}) + + {/if} +
+ + {#if notification.content} +
+ {truncateContent(notification.content)} +
{/if} -
- - {#if notification.content} -
- {truncateContent(notification.content)} + +
+ + + {getNeventUrl(notification).slice(0, 16)}... +
- {/if} - -
- - - {getNeventUrl(notification).slice(0, 16)}... -
+ {/each} +
+ + {#if notifications.length > 10} +
+ Showing 10 of {notifications.length} notifications {notificationMode === "to-me" ? "received" : "sent"}. Scroll to see more.
- {/each} + {/if}
- - {#if notifications.length > 10} -
- Showing 10 of {notifications.length} notifications {notificationMode === "to-me" ? "received" : "sent"}. Scroll to see more. -
- {/if} -
+ {/if} {/if}
{/if} \ No newline at end of file From 49de089e9a4e3c66eafdec29a2594a46e90fb453 Mon Sep 17 00:00:00 2001 From: silberengel Date: Fri, 8 Aug 2025 23:21:56 +0200 Subject: [PATCH 12/27] relay info display in public message replies --- src/lib/components/Notifications.svelte | 240 +++++++++++++++++++- src/lib/components/RelayInfoDisplay.svelte | 92 ++++++++ src/lib/components/RelayInfoList.svelte | 169 ++++++++++++++ src/lib/utils/kind24_utils.ts | 252 +++++++++++++++++++++ src/lib/utils/relay_info_service.ts | 166 ++++++++++++++ 5 files changed, 917 insertions(+), 2 deletions(-) create mode 100644 src/lib/components/RelayInfoDisplay.svelte create mode 100644 src/lib/components/RelayInfoList.svelte create mode 100644 src/lib/utils/kind24_utils.ts create mode 100644 src/lib/utils/relay_info_service.ts diff --git a/src/lib/components/Notifications.svelte b/src/lib/components/Notifications.svelte index 867b881..062ecd4 100644 --- a/src/lib/components/Notifications.svelte +++ b/src/lib/components/Notifications.svelte @@ -11,6 +11,9 @@ import { get } from "svelte/store"; import { nip19 } from "nostr-tools"; import { communityRelays, localRelays } from "$lib/consts"; + import { createKind24Reply, getKind24RelaySet } from "$lib/utils/kind24_utils"; + import RelayDisplay from "$lib/components/RelayDisplay.svelte"; + import RelayInfoList from "$lib/components/RelayInfoList.svelte"; const { event } = $props<{ event: NDKEvent }>(); @@ -23,6 +26,14 @@ let notificationMode = $state<"to-me" | "from-me" | "public-messages">("to-me"); let authorProfiles = $state>(new Map()); let filteredByUser = $state(null); + let replyContent = $state(""); + let replyingTo = $state(null); + let isReplying = $state(false); + let originalMessage = $state(null); + let replyingToMessageId = $state(null); + let replyRelays = $state([]); + let senderOutboxRelays = $state([]); + let recipientInboxRelays = $state([]); // Derived state for filtered messages let filteredMessages = $derived.by(() => { @@ -85,6 +96,19 @@ return content.slice(0, maxLength) + "..."; } + function renderContentWithLinks(content: string): string { + console.log("[Notifications] Rendering content:", content); + + // Parse markdown links [text](url) and convert to HTML + let rendered = content.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '$1'); + + // Also handle the new quote format: "> LINK: nevent://..." and convert to button + rendered = rendered.replace(/> LINK: (nevent:\/\/[^\s\n]+)/g, '> '); + + console.log("[Notifications] Rendered content:", rendered); + return rendered; + } + function getNotificationType(event: NDKEvent): string { switch (event.kind) { case 1: return "Reply"; @@ -109,6 +133,141 @@ filteredByUser = null; } + // AI-NOTE: Reply functionality for kind 24 messages + async function startReply(pubkey: string, messageEvent?: NDKEvent) { + replyingTo = pubkey; + isReplying = true; + replyContent = ""; + replyingToMessageId = messageEvent?.id || null; + // Store the original message for q tag + originalMessage = messageEvent || null; + // Clear previous relay information + replyRelays = []; + senderOutboxRelays = []; + recipientInboxRelays = []; + + // Immediately fetch relay information for this recipient + await getReplyRelays(); + } + + function cancelReply() { + replyingTo = null; + isReplying = false; + replyContent = ""; + replyingToMessageId = null; + replyRelays = []; + senderOutboxRelays = []; + recipientInboxRelays = []; + } + + async function sendReply() { + if (!replyingTo || !replyContent.trim()) return; + + try { + // Find the original message being replied to + const originalMessage = publicMessages.find(msg => msg.id === replyingToMessageId); + const result = await createKind24Reply(replyContent, replyingTo, originalMessage); + + if (result.success) { + // Store relay information for display + replyRelays = result.relays || []; + + // Update the inbox/outbox arrays to match the actual relays being used + // Keep only the top 3 that are actually in the reply relay set + const replyRelaySet = new Set(replyRelays); + senderOutboxRelays = senderOutboxRelays + .filter(relay => replyRelaySet.has(relay)) + .slice(0, 3); + recipientInboxRelays = recipientInboxRelays + .filter(relay => replyRelaySet.has(relay)) + .slice(0, 3); + + // Clear reply state + replyingTo = null; + isReplying = false; + replyContent = ""; + replyingToMessageId = null; + // Optionally refresh messages + await fetchPublicMessages(); + } else { + console.error("Failed to send reply:", result.error); + // You could show an error message to the user here + } + } catch (error) { + console.error("Error sending reply:", error); + } + } + + // Function to get relay information before sending + async function getReplyRelays() { + if (!replyingTo) return; + + try { + const originalMessage = publicMessages.find(msg => msg.id === replyingToMessageId); + + // Get sender's outbox relays and recipient's inbox relays + const ndk = get(ndkInstance); + if (ndk?.activeUser) { + // Get sender's outbox relays + const senderUser = ndk.activeUser; + const senderRelayList = await ndk.fetchEvent({ + kinds: [10002], + authors: [senderUser.pubkey], + }); + + if (senderRelayList) { + senderOutboxRelays = senderRelayList.tags + .filter(tag => tag[0] === 'r' && tag[1]) + .map(tag => tag[1]) + .slice(0, 3); // Limit to top 3 outbox relays + } + + // Get recipient's inbox relays + const recipientUser = ndk.getUser({ pubkey: replyingTo }); + const recipientRelayList = await ndk.fetchEvent({ + kinds: [10002], + authors: [replyingTo], + }); + + if (recipientRelayList) { + recipientInboxRelays = recipientRelayList.tags + .filter(tag => tag[0] === 'r' && tag[1]) + .map(tag => tag[1]) + .slice(0, 3); // Limit to top 3 inbox relays + } + } + + // If we have content, use the actual reply function + if (replyContent.trim()) { + const result = await createKind24Reply(replyContent, replyingTo, originalMessage); + replyRelays = result.relays || []; + } else { + // If no content yet, just get the relay set for this recipient + const result = await getKind24RelaySet($userStore.pubkey || '', replyingTo); + replyRelays = result || []; + + // Update the inbox/outbox arrays to match the actual relays being used + // Keep only the top 3 that are actually in the reply relay set + const replyRelaySet = new Set(replyRelays); + senderOutboxRelays = senderOutboxRelays + .filter(relay => replyRelaySet.has(relay)) + .slice(0, 3); + recipientInboxRelays = recipientInboxRelays + .filter(relay => replyRelaySet.has(relay)) + .slice(0, 3); + + console.log('[Notifications] Got relay set:', result); + console.log('[Notifications] Filtered sender outbox relays:', senderOutboxRelays); + console.log('[Notifications] Filtered recipient inbox relays:', recipientInboxRelays); + } + } catch (error) { + console.error("Error getting relay information:", error); + replyRelays = []; + senderOutboxRelays = []; + recipientInboxRelays = []; + } + } + // AI-NOTE: Simplified profile fetching with better error handling async function fetchAuthorProfiles(events: NDKEvent[]) { const uniquePubkeys = new Set(); @@ -321,6 +480,13 @@ authorProfiles.clear(); } }); + + // Fetch relay information when reply content changes (for updates) + $effect(() => { + if (isReplying && replyingTo && replyContent.trim() && replyRelays.length === 0) { + getReplyRelays(); + } + }); {#if isOwnProfile && $userStore.signedIn} @@ -400,7 +566,19 @@ {#if !isFromUser} -
+
+ + +
+ + + {#if isReplying && replyingToMessageId === message.id} + {@const recipientProfile = authorProfiles.get(message.pubkey)} +
+
+ + Replying to: {recipientProfile?.displayName || recipientProfile?.name || `${message.pubkey.slice(0, 8)}...${message.pubkey.slice(-4)}`} + + +
+
+ + +
+ + +
+ {#if replyRelays.length > 0} + {@const debugInfo = console.log('[Notifications] Rendering RelayInfoList with:', { replyRelays, recipientInboxRelays, senderOutboxRelays })} + + {:else} +
+
+ Loading relay information... +
+ {/if} +
+
+ {/if}
{/each}
diff --git a/src/lib/components/RelayInfoDisplay.svelte b/src/lib/components/RelayInfoDisplay.svelte new file mode 100644 index 0000000..c72dc24 --- /dev/null +++ b/src/lib/components/RelayInfoDisplay.svelte @@ -0,0 +1,92 @@ + + +
+ {#if showIcon && relayIcon} + Relay icon (e.target as HTMLImageElement).style.display = 'none'} + /> + {:else if showIcon} + +
+ + + +
+ {/if} + +
+ {#if showName} + + {isLoading ? 'Loading...' : displayName} + + {/if} + + {#if showType} + + {relayType} + + {/if} +
+ + {#if error} + + ⚠️ + + {/if} +
diff --git a/src/lib/components/RelayInfoList.svelte b/src/lib/components/RelayInfoList.svelte new file mode 100644 index 0000000..0443453 --- /dev/null +++ b/src/lib/components/RelayInfoList.svelte @@ -0,0 +1,169 @@ + + +
+ {#if showLabels && !compact} + {@const categorizedCount = categorizedRelays().length} + {@const debugCategorized = console.log('[RelayInfoList] Debug - categorized relays:', categorizedRelays())} +
+ Publishing to {categorizedCount} relay(s): +
+ {/if} + + {#if isLoading} +
+
+ Loading relay info... +
+ {:else} + {@const categorized = categorizedRelays()} + {@const debugCategorized = console.log('[RelayInfoList] Debug - categorized relays:', categorized)} + +
+ {#each categorized as { relay, category, label }} +
+
+ + {relay} + + {#if category === 'both'} + + common relay + + {/if} +
+
+ {/each} +
+ {/if} +
diff --git a/src/lib/utils/kind24_utils.ts b/src/lib/utils/kind24_utils.ts new file mode 100644 index 0000000..e3b6e14 --- /dev/null +++ b/src/lib/utils/kind24_utils.ts @@ -0,0 +1,252 @@ +import { get } from "svelte/store"; +import { ndkInstance } from "../ndk"; +import { userStore } from "../stores/userStore"; +import { NDKEvent, NDKRelaySet, NDKUser } from "@nostr-dev-kit/ndk"; +import type NDK from "@nostr-dev-kit/ndk"; +import { nip19 } from "nostr-tools"; + +/** + * Fetches user's outbox relays from NIP-65 relay list + * @param ndk NDK instance + * @param user User to fetch outbox relays for + * @returns Promise that resolves to array of outbox relay URLs + */ +async function getUseroutboxRelays(ndk: NDK, user: NDKUser): Promise { + try { + console.debug('[kind24_utils] Fetching outbox relays for user:', user.pubkey); + const relayList = await ndk.fetchEvent( + { + kinds: [10002], + authors: [user.pubkey], + } + ); + + if (!relayList) { + console.debug('[kind24_utils] No relay list found for user'); + return []; + } + + console.debug('[kind24_utils] Found relay list event:', relayList.id); + console.debug('[kind24_utils] Relay list tags:', relayList.tags); + + const outboxRelays: string[] = []; + relayList.tags.forEach((tag) => { + console.debug('[kind24_utils] Processing tag:', tag); + if (tag[0] === 'r' && tag[1]) { + // NIP-65: r tags with optional inbox/outbox markers + const marker = tag[2]; + if (!marker || marker === 'outbox' || marker === 'inbox') { + // If no marker or marker is 'outbox', it's a outbox relay + // If marker is 'inbox', it's also a outbox relay (NIP-65 allows both) + outboxRelays.push(tag[1]); + console.debug('[kind24_utils] Added outbox relay:', tag[1]); + } + } + }); + + console.debug('[kind24_utils] Final outbox relays:', outboxRelays); + return outboxRelays; + } catch (error) { + console.info('[kind24_utils] Error fetching user outbox relays:', error); + return []; + } +} + +/** + * Fetches user's inbox relays from NIP-65 relay list + * @param ndk NDK instance + * @param user User to fetch inbox relays for + * @returns Promise that resolves to array of inbox relay URLs + */ +async function getUserinboxRelays(ndk: NDK, user: NDKUser): Promise { + try { + console.debug('[kind24_utils] Fetching inbox relays for user:', user.pubkey); + const relayList = await ndk.fetchEvent( + { + kinds: [10002], + authors: [user.pubkey], + } + ); + + if (!relayList) { + console.debug('[kind24_utils] No relay list found for user'); + return []; + } + + console.debug('[kind24_utils] Found relay list event:', relayList.id); + console.debug('[kind24_utils] Relay list tags:', relayList.tags); + + const inboxRelays: string[] = []; + relayList.tags.forEach((tag) => { + console.debug('[kind24_utils] Processing tag:', tag); + if (tag[0] === 'r' && tag[1]) { + // NIP-65: r tags with optional inbox/outbox markers + const marker = tag[2]; + if (!marker || marker === 'inbox' || marker === 'outbox') { + // If no marker or marker is 'inbox', it's a inbox relay + // If marker is 'outbox', it's also a inbox relay (NIP-65 allows both) + inboxRelays.push(tag[1]); + console.debug('[kind24_utils] Added inbox relay:', tag[1]); + } + } + }); + + console.debug('[kind24_utils] Final inbox relays:', inboxRelays); + return inboxRelays; + } catch (error) { + console.info('[kind24_utils] Error fetching user inbox relays:', error); + return []; + } +} + +/** + * Creates a kind 24 public message reply according to NIP-A4 + * @param content The message content + * @param recipientPubkey The recipient's pubkey + * @param originalEvent The original event being replied to (optional) + * @returns Promise resolving to publish result with relay information + */ +export async function createKind24Reply( + content: string, + recipientPubkey: string, + originalEvent?: NDKEvent +): Promise<{ success: boolean; eventId?: string; error?: string; relays?: string[] }> { + const ndk = get(ndkInstance); + if (!ndk?.activeUser) { + return { success: false, error: "Not logged in" }; + } + + if (!content.trim()) { + return { success: false, error: "Message content cannot be empty" }; + } + + try { + // Get sender's outbox relays (NIP-65) + const senderoutboxRelays = await getUseroutboxRelays(ndk, ndk.activeUser); + + // Get recipient's inbox relays (NIP-65) + const recipientUser = ndk.getUser({ pubkey: recipientPubkey }); + const recipientinboxRelays = await getUserinboxRelays(ndk, recipientUser); + + // According to NIP-A4: Messages MUST be sent to the NIP-65 inbox relays of each receiver + // and the outbox relay of the sender + const targetRelays = [...new Set([...senderoutboxRelays, ...recipientinboxRelays])]; + + // Prioritize common relays between sender and recipient for better privacy + const commonRelays = senderoutboxRelays.filter(relay => + recipientinboxRelays.includes(relay) + ); + const senderOnlyRelays = senderoutboxRelays.filter(relay => + !recipientinboxRelays.includes(relay) + ); + const recipientOnlyRelays = recipientinboxRelays.filter(relay => + !senderoutboxRelays.includes(relay) + ); + + // Prioritize: common relays first, then sender outbox, then recipient inbox + const prioritizedRelays = [...commonRelays, ...senderOnlyRelays, ...recipientOnlyRelays]; + + if (prioritizedRelays.length === 0) { + return { success: false, error: "No relays available for publishing" }; + } + + // Create the kind 24 event + const event = new NDKEvent(ndk); + event.kind = 24; + + // Build content with quoted message if replying + let finalContent = content; + if (originalEvent) { + // Use multiple relays for better discoverability + const nevent = nip19.neventEncode({ + id: originalEvent.id, + relays: prioritizedRelays.slice(0, 3) // Use first 3 relays + }); + const quotedContent = originalEvent.content ? originalEvent.content.slice(0, 200) : "No content"; + // Use a more visible quote format with a clickable link + finalContent = `> QUOTED: ${quotedContent}\n> LINK: ${nevent}\n\n${content}`; + console.log("[kind24_utils] Reply content:", finalContent); + } + + event.content = finalContent; + event.created_at = Math.floor(Date.now() / 1000); + + // Add p tag for recipient with relay URL + const tags: string[][] = [ + ["p", recipientPubkey, prioritizedRelays[0]] // Use first relay as primary + ]; + + // Add q tag if replying to an original event + if (originalEvent) { + const nevent = nip19.neventEncode({ + id: originalEvent.id, + relays: prioritizedRelays.slice(0, 3) // Use first 3 relays + }); + tags.push(["q", nevent, prioritizedRelays[0]]); + } + + event.tags = tags; + event.pubkey = ndk.activeUser.pubkey; + + // Sign the event + await event.sign(); + + // Publish to relays + const relaySet = NDKRelaySet.fromRelayUrls(prioritizedRelays, ndk); + const publishedToRelays = await event.publish(relaySet); + + if (publishedToRelays.size > 0) { + return { success: true, eventId: event.id, relays: prioritizedRelays }; + } else { + return { success: false, error: "Failed to publish to any relays", relays: prioritizedRelays }; + } + } catch (error) { + console.error("[kind24_utils] Error creating kind 24 reply:", error); + return { + success: false, + error: error instanceof Error ? error.message : "Unknown error" + }; + } +} + +/** + * Gets optimal relay set for kind 24 messages between two users + * @param senderPubkey The sender's pubkey + * @param recipientPubkey The recipient's pubkey + * @returns Promise resolving to relay URLs prioritized by commonality + */ +export async function getKind24RelaySet( + senderPubkey: string, + recipientPubkey: string +): Promise { + const ndk = get(ndkInstance); + if (!ndk) { + throw new Error("NDK not available"); + } + + // Get sender's outbox relays (NIP-65) + const senderUser = ndk.getUser({ pubkey: senderPubkey }); + const senderoutboxRelays = await getUseroutboxRelays(ndk, senderUser); + + // Get recipient's inbox relays (NIP-65) + const recipientUser = ndk.getUser({ pubkey: recipientPubkey }); + const recipientinboxRelays = await getUserinboxRelays(ndk, recipientUser); + + // According to NIP-A4: Messages MUST be sent to the NIP-65 inbox relays of each receiver + // and the outbox relay of the sender + const targetRelays = [...new Set([...senderoutboxRelays, ...recipientinboxRelays])]; + + // Prioritize common relays between sender and recipient for better privacy + const commonRelays = senderoutboxRelays.filter((relay: string) => + recipientinboxRelays.includes(relay) + ); + const senderOnlyRelays = senderoutboxRelays.filter((relay: string) => + !recipientinboxRelays.includes(relay) + ); + const recipientOnlyRelays = recipientinboxRelays.filter((relay: string) => + !senderoutboxRelays.includes(relay) + ); + + // Prioritize: common relays first, then sender outbox, then recipient inbox + return [...commonRelays, ...senderOnlyRelays, ...recipientOnlyRelays]; +} diff --git a/src/lib/utils/relay_info_service.ts b/src/lib/utils/relay_info_service.ts new file mode 100644 index 0000000..8b978a0 --- /dev/null +++ b/src/lib/utils/relay_info_service.ts @@ -0,0 +1,166 @@ +/** + * Simplifies a URL by removing protocol and common prefixes + * @param url The URL to simplify + * @returns Simplified URL string + */ +function simplifyUrl(url: string): string { + try { + const urlObj = new URL(url); + return urlObj.hostname + (urlObj.port ? `:${urlObj.port}` : ''); + } catch { + // If URL parsing fails, return the original string + return url; + } +} + +export interface RelayInfo { + name?: string; + description?: string; + icon?: string; + pubkey?: string; + contact?: string; + supported_nips?: number[]; + software?: string; + version?: string; + tags?: string[]; + payments_url?: string; + limitation?: { + auth_required?: boolean; + payment_required?: boolean; + }; +} + +export interface RelayInfoWithMetadata extends RelayInfo { + url: string; + shortUrl: string; + hasNip11: boolean; + triedNip11: boolean; +} + +/** + * Fetches relay information using NIP-11 + * @param url The relay URL to fetch info for + * @returns Promise resolving to relay info or undefined if failed + */ +export async function fetchRelayInfo(url: string): Promise { + try { + // Convert WebSocket URL to HTTP URL for NIP-11 + const httpUrl = url.replace('ws://', 'http://').replace('wss://', 'https://'); + + const response = await fetch(httpUrl, { + headers: { + 'Accept': 'application/nostr+json', + 'User-Agent': 'Alexandria/1.0' + }, + // Add timeout to prevent hanging + signal: AbortSignal.timeout(5000) + }); + + if (!response.ok) { + console.warn(`[RelayInfo] HTTP ${response.status} for ${url}`); + return { + url, + shortUrl: simplifyUrl(url), + hasNip11: false, + triedNip11: true + }; + } + + const relayInfo = await response.json() as RelayInfo; + + return { + ...relayInfo, + url, + shortUrl: simplifyUrl(url), + hasNip11: Object.keys(relayInfo).length > 0, + triedNip11: true + }; + } catch (error) { + console.warn(`[RelayInfo] Failed to fetch info for ${url}:`, error); + return { + url, + shortUrl: simplifyUrl(url), + hasNip11: false, + triedNip11: true + }; + } +} + +/** + * Fetches relay information for multiple relays in parallel + * @param urls Array of relay URLs to fetch info for + * @returns Promise resolving to array of relay info objects + */ +export async function fetchRelayInfos(urls: string[]): Promise { + if (urls.length === 0) { + return []; + } + + const promises = urls.map(url => fetchRelayInfo(url)); + const results = await Promise.allSettled(promises); + + return results + .map(result => result.status === 'fulfilled' ? result.value : undefined) + .filter((info): info is RelayInfoWithMetadata => info !== undefined); +} + +/** + * Gets relay type label based on relay URL and info + * @param relayUrl The relay URL + * @param relayInfo Optional relay info + * @returns String describing the relay type + */ +export function getRelayTypeLabel(relayUrl: string, relayInfo?: RelayInfoWithMetadata): string { + // Check if it's a local relay + if (relayUrl.includes('localhost') || relayUrl.includes('127.0.0.1')) { + return 'Local'; + } + + // Check if it's a community relay + if (relayUrl.includes('nostr.band') || relayUrl.includes('noswhere.com') || + relayUrl.includes('damus.io') || relayUrl.includes('nostr.wine')) { + return 'Community'; + } + + // Check if it's a user's relay (likely inbox/outbox) + if (relayUrl.includes('relay.nsec.app') || relayUrl.includes('relay.snort.social')) { + return 'User'; + } + + // Use relay name if available + if (relayInfo?.name) { + return relayInfo.name; + } + + // Fallback to domain + try { + const domain = new URL(relayUrl).hostname; + return domain.replace('www.', ''); + } catch { + return 'Unknown'; + } +} + +/** + * Gets relay icon URL or fallback + * @param relayInfo Relay info object + * @param relayUrl Relay URL as fallback + * @returns Icon URL or undefined + */ +export function getRelayIcon(relayInfo?: RelayInfoWithMetadata, relayUrl?: string): string | undefined { + if (relayInfo?.icon) { + return relayInfo.icon; + } + + // Generate favicon URL from relay URL + if (relayUrl) { + try { + const url = new URL(relayUrl); + return `${url.protocol}//${url.hostname}/favicon.ico`; + } catch { + // Invalid URL, return undefined + } + } + + return undefined; +} From b6c30352e215fdd212e138d3259cd27e57ab1d77 Mon Sep 17 00:00:00 2001 From: silberengel Date: Sat, 9 Aug 2025 00:08:22 +0200 Subject: [PATCH 13/27] gray bar navigation --- src/lib/components/Notifications.svelte | 123 +++++++++++++++++------- src/lib/components/RelayInfoList.svelte | 34 +------ src/lib/utils/kind24_utils.ts | 28 ++---- 3 files changed, 103 insertions(+), 82 deletions(-) diff --git a/src/lib/components/Notifications.svelte b/src/lib/components/Notifications.svelte index 062ecd4..1604532 100644 --- a/src/lib/components/Notifications.svelte +++ b/src/lib/components/Notifications.svelte @@ -17,6 +17,22 @@ const { event } = $props<{ event: NDKEvent }>(); + // Handle navigation events from quoted messages + $effect(() => { + if (typeof window !== 'undefined') { + const handleJumpToMessage = (e: Event) => { + const customEvent = e as CustomEvent; + jumpToMessageInFeed(customEvent.detail); + }; + + window.addEventListener('jump-to-message', handleJumpToMessage); + + return () => { + window.removeEventListener('jump-to-message', handleJumpToMessage); + }; + } + }); + // Component state let notifications = $state([]); let publicMessages = $state([]); @@ -76,7 +92,9 @@ function getNeventUrl(event: NDKEvent): string { const relays = getAvailableRelays(); - return neventEncode(event, relays); + const nevent = neventEncode(event, relays); + console.log('Generated nevent for event:', event.id, '→', nevent); + return nevent; } function formatDate(timestamp: number): string { @@ -97,15 +115,28 @@ } function renderContentWithLinks(content: string): string { - console.log("[Notifications] Rendering content:", content); - // Parse markdown links [text](url) and convert to HTML let rendered = content.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '$1'); - // Also handle the new quote format: "> LINK: nevent://..." and convert to button - rendered = rendered.replace(/> LINK: (nevent:\/\/[^\s\n]+)/g, '> '); + // Handle quote format and convert to small gray bars like Jumble + const patterns = [ + /> QUOTED: ([^•]*?) • LINK:\s*\n(nevent[^\s]*)/g, + /> QUOTED: ([^\n]*?)\n> LINK: (nevent[^\s]*)/g, + /> QUOTED: ([^•]*?) • LINK:\s*(nevent[^\s]*)/g, + ]; + + for (const pattern of patterns) { + const beforeReplace = rendered; + rendered = rendered.replace(pattern, (match, quotedText, neventUrl) => { + const encodedUrl = neventUrl.replace(/'/g, '''); + const cleanQuotedText = quotedText.trim(); + return `
${cleanQuotedText}
`; + }); + if (beforeReplace !== rendered) { + break; + } + } - console.log("[Notifications] Rendered content:", rendered); return rendered; } @@ -122,9 +153,43 @@ } function navigateToEvent(nevent: string) { + // Navigate to the events search page with this specific event goto(`/events?id=${nevent}`); } + function jumpToMessageInFeed(nevent: string) { + // Switch to public messages tab and scroll to the specific message + notificationMode = "public-messages"; + + // Try to find and scroll to the specific message + setTimeout(() => { + try { + // Decode the nevent to get the event ID + const decoded = nip19.decode(nevent); + if (decoded.type === 'nevent' && decoded.data.id) { + const eventId = decoded.data.id; + + // Find the message in our public messages + const targetMessage = publicMessages.find(msg => msg.id === eventId); + if (targetMessage) { + // Try to scroll to the element if it exists in the DOM + const element = document.querySelector(`[data-event-id="${eventId}"]`); + if (element) { + element.scrollIntoView({ behavior: 'smooth', block: 'center' }); + // Briefly highlight the message + element.classList.add('ring-2', 'ring-blue-500'); + setTimeout(() => { + element.classList.remove('ring-2', 'ring-blue-500'); + }, 2000); + } + } + } + } catch (error) { + console.warn('Failed to jump to message:', error); + } + }, 100); + } + function filterByUser(pubkey: string) { filteredByUser = filteredByUser === pubkey ? null : pubkey; } @@ -256,9 +321,7 @@ .filter(relay => replyRelaySet.has(relay)) .slice(0, 3); - console.log('[Notifications] Got relay set:', result); - console.log('[Notifications] Filtered sender outbox relays:', senderOutboxRelays); - console.log('[Notifications] Filtered recipient inbox relays:', recipientInboxRelays); + } } catch (error) { console.error("Error getting relay information:", error); @@ -545,7 +608,7 @@ {#each filteredMessages.slice(0, 20) as message} {@const authorProfile = authorProfiles.get(message.pubkey)} {@const isFromUser = message.pubkey === $userStore.pubkey} -
+
@@ -602,6 +665,13 @@ {message.created_at ? formatDate(message.created_at) : "Unknown date"} +
@@ -622,17 +692,7 @@
{/if} -
- - - {getNeventUrl(message).slice(0, 16)}... - -
+
@@ -676,7 +736,7 @@
{#if replyRelays.length > 0} - {@const debugInfo = console.log('[Notifications] Rendering RelayInfoList with:', { replyRelays, recipientInboxRelays, senderOutboxRelays })} + {notification.created_at ? formatDate(notification.created_at) : "Unknown date"} +
@@ -763,17 +830,7 @@
{/if} -
- - - {getNeventUrl(notification).slice(0, 16)}... - -
+
diff --git a/src/lib/components/RelayInfoList.svelte b/src/lib/components/RelayInfoList.svelte index 0443453..62d6b8b 100644 --- a/src/lib/components/RelayInfoList.svelte +++ b/src/lib/components/RelayInfoList.svelte @@ -25,18 +25,10 @@ label: string; }; - // AI-NOTE: Updated to show only top-3 inboxes and top-3 outboxes as intended + // Categorize relays by their function (inbox/outbox/both) const categorizedRelays = $derived(() => { const inbox = new Set(inboxRelays); const outbox = new Set(outboxRelays); - - console.log('[RelayInfoList] Categorizing relays:', { - relays: relays.length, - inboxRelays: inboxRelays.length, - outboxRelays: outboxRelays.length - }); - - // Create a map of all relays with their categories const relayCategories = new Map(); // Process inbox relays (up to top 3) @@ -58,29 +50,19 @@ } }); - // Only include relays that are actually in the top-3 lists - // This ensures we only show the intended top-3 inboxes and top-3 outboxes - const categorized = Array.from(relayCategories.values()); - console.log('[RelayInfoList] Categorized relays count:', categorized.length); - return categorized; + return Array.from(relayCategories.values()); }); - // Group by category + // Group by category for display const groupedRelays = $derived(() => { const categorized = categorizedRelays(); - console.log('[RelayInfoList] Grouping categorized relays'); - const groups = { + return { both: categorized.filter((r: CategorizedRelay) => r.category === 'both'), inbox: categorized.filter((r: CategorizedRelay) => r.category === 'inbox'), outbox: categorized.filter((r: CategorizedRelay) => r.category === 'outbox'), other: categorized.filter((r: CategorizedRelay) => r.category === 'other') }; - - console.log('[RelayInfoList] Grouped relays:', Object.fromEntries( - Object.entries(groups).map(([key, relays]) => [key, relays.length]) - )); - return groups; }); async function loadRelayInfos() { @@ -99,12 +81,6 @@ // Load relay info when categorized relays change $effect(() => { const categorized = categorizedRelays(); - console.log('[RelayInfoList] Categorized relays changed:', { - total: categorized.length, - byCategory: Object.fromEntries( - Object.entries(groupedRelays()).map(([key, relays]) => [key, relays.length]) - ) - }); if (categorized.length > 0) { loadRelayInfos(); } @@ -134,7 +110,6 @@
{#if showLabels && !compact} {@const categorizedCount = categorizedRelays().length} - {@const debugCategorized = console.log('[RelayInfoList] Debug - categorized relays:', categorizedRelays())}
Publishing to {categorizedCount} relay(s):
@@ -147,7 +122,6 @@
{:else} {@const categorized = categorizedRelays()} - {@const debugCategorized = console.log('[RelayInfoList] Debug - categorized relays:', categorized)}
{#each categorized as { relay, category, label }} diff --git a/src/lib/utils/kind24_utils.ts b/src/lib/utils/kind24_utils.ts index e3b6e14..62a27d0 100644 --- a/src/lib/utils/kind24_utils.ts +++ b/src/lib/utils/kind24_utils.ts @@ -13,7 +13,7 @@ import { nip19 } from "nostr-tools"; */ async function getUseroutboxRelays(ndk: NDK, user: NDKUser): Promise { try { - console.debug('[kind24_utils] Fetching outbox relays for user:', user.pubkey); + const relayList = await ndk.fetchEvent( { kinds: [10002], @@ -22,16 +22,11 @@ async function getUseroutboxRelays(ndk: NDK, user: NDKUser): Promise { ); if (!relayList) { - console.debug('[kind24_utils] No relay list found for user'); return []; } - console.debug('[kind24_utils] Found relay list event:', relayList.id); - console.debug('[kind24_utils] Relay list tags:', relayList.tags); - const outboxRelays: string[] = []; relayList.tags.forEach((tag) => { - console.debug('[kind24_utils] Processing tag:', tag); if (tag[0] === 'r' && tag[1]) { // NIP-65: r tags with optional inbox/outbox markers const marker = tag[2]; @@ -39,15 +34,15 @@ async function getUseroutboxRelays(ndk: NDK, user: NDKUser): Promise { // If no marker or marker is 'outbox', it's a outbox relay // If marker is 'inbox', it's also a outbox relay (NIP-65 allows both) outboxRelays.push(tag[1]); - console.debug('[kind24_utils] Added outbox relay:', tag[1]); + } } }); - console.debug('[kind24_utils] Final outbox relays:', outboxRelays); + return outboxRelays; } catch (error) { - console.info('[kind24_utils] Error fetching user outbox relays:', error); + return []; } } @@ -60,7 +55,7 @@ async function getUseroutboxRelays(ndk: NDK, user: NDKUser): Promise { */ async function getUserinboxRelays(ndk: NDK, user: NDKUser): Promise { try { - console.debug('[kind24_utils] Fetching inbox relays for user:', user.pubkey); + const relayList = await ndk.fetchEvent( { kinds: [10002], @@ -69,16 +64,11 @@ async function getUserinboxRelays(ndk: NDK, user: NDKUser): Promise { ); if (!relayList) { - console.debug('[kind24_utils] No relay list found for user'); return []; } - console.debug('[kind24_utils] Found relay list event:', relayList.id); - console.debug('[kind24_utils] Relay list tags:', relayList.tags); - const inboxRelays: string[] = []; relayList.tags.forEach((tag) => { - console.debug('[kind24_utils] Processing tag:', tag); if (tag[0] === 'r' && tag[1]) { // NIP-65: r tags with optional inbox/outbox markers const marker = tag[2]; @@ -86,15 +76,15 @@ async function getUserinboxRelays(ndk: NDK, user: NDKUser): Promise { // If no marker or marker is 'inbox', it's a inbox relay // If marker is 'outbox', it's also a inbox relay (NIP-65 allows both) inboxRelays.push(tag[1]); - console.debug('[kind24_utils] Added inbox relay:', tag[1]); + } } }); - console.debug('[kind24_utils] Final inbox relays:', inboxRelays); + return inboxRelays; } catch (error) { - console.info('[kind24_utils] Error fetching user inbox relays:', error); + return []; } } @@ -165,7 +155,7 @@ export async function createKind24Reply( const quotedContent = originalEvent.content ? originalEvent.content.slice(0, 200) : "No content"; // Use a more visible quote format with a clickable link finalContent = `> QUOTED: ${quotedContent}\n> LINK: ${nevent}\n\n${content}`; - console.log("[kind24_utils] Reply content:", finalContent); + } event.content = finalContent; From dab4995dcf717c58478e9f7d738d886721f19ddc Mon Sep 17 00:00:00 2001 From: silberengel Date: Sat, 9 Aug 2025 00:21:24 +0200 Subject: [PATCH 14/27] moved notifications to the top --- src/lib/components/EventDetails.svelte | 8 +++++--- src/lib/components/Notifications.svelte | 12 +++++------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/lib/components/EventDetails.svelte b/src/lib/components/EventDetails.svelte index dcdd0eb..6e6712a 100644 --- a/src/lib/components/EventDetails.svelte +++ b/src/lib/components/EventDetails.svelte @@ -401,6 +401,11 @@ {/if} + + {#if event.kind === 0} + + {/if} +
{#if toNpub(event.pubkey)} - - - {/if} diff --git a/src/lib/components/Notifications.svelte b/src/lib/components/Notifications.svelte index 1604532..37cf746 100644 --- a/src/lib/components/Notifications.svelte +++ b/src/lib/components/Notifications.svelte @@ -92,9 +92,7 @@ function getNeventUrl(event: NDKEvent): string { const relays = getAvailableRelays(); - const nevent = neventEncode(event, relays); - console.log('Generated nevent for event:', event.id, '→', nevent); - return nevent; + return neventEncode(event, relays); } function formatDate(timestamp: number): string { @@ -605,7 +603,7 @@
{/if}
- {#each filteredMessages.slice(0, 20) as message} + {#each filteredMessages.slice(0, 100) as message} {@const authorProfile = authorProfiles.get(message.pubkey)} {@const isFromUser = message.pubkey === $userStore.pubkey}
@@ -772,7 +770,7 @@ {:else}
- {#each notifications.slice(0, 10) as notification} + {#each notifications.slice(0, 100) as notification} {@const authorProfile = authorProfiles.get(notification.pubkey)}
@@ -837,9 +835,9 @@ {/each}
- {#if notifications.length > 10} + {#if notifications.length > 100}
- Showing 10 of {notifications.length} notifications {notificationMode === "to-me" ? "received" : "sent"}. Scroll to see more. + Showing 100 of {notifications.length} notifications {notificationMode === "to-me" ? "received" : "sent"}. Scroll to see more.
{/if}
From eb4c19a9d720171fe9cf1cb8975dafc87c11e58b Mon Sep 17 00:00:00 2001 From: silberengel Date: Sat, 9 Aug 2025 01:55:19 +0200 Subject: [PATCH 15/27] fixed multi-recipients --- src/lib/components/Notifications.svelte | 33 +++++++++++++------------ 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/src/lib/components/Notifications.svelte b/src/lib/components/Notifications.svelte index 37cf746..4488952 100644 --- a/src/lib/components/Notifications.svelte +++ b/src/lib/components/Notifications.svelte @@ -14,6 +14,9 @@ import { createKind24Reply, getKind24RelaySet } from "$lib/utils/kind24_utils"; import RelayDisplay from "$lib/components/RelayDisplay.svelte"; import RelayInfoList from "$lib/components/RelayInfoList.svelte"; + import { Modal, Button } from "flowbite-svelte"; + import { searchProfiles } from "$lib/utils/search_utility"; + import type { NostrProfile } from "$lib/utils/search_types"; const { event } = $props<{ event: NDKEvent }>(); @@ -570,7 +573,7 @@
{#if loading} -
+
Loading {notificationMode === "public-messages" ? "public messages" : "notifications"}... @@ -586,9 +589,9 @@

No public messages found.

{:else} -
+
{#if filteredByUser} -
+
Filtered by user: {authorProfiles.get(filteredByUser)?.displayName || authorProfiles.get(filteredByUser)?.name || `${filteredByUser.slice(0, 8)}...${filteredByUser.slice(-4)}`} @@ -602,11 +605,11 @@
{/if} -
+
{#each filteredMessages.slice(0, 100) as message} {@const authorProfile = authorProfiles.get(message.pubkey)} {@const isFromUser = message.pubkey === $userStore.pubkey} -
+
@@ -752,11 +755,11 @@
{/if}
- {/each} + {/each}
{#if filteredMessages.length > 20} -
+
Showing 20 of {filteredMessages.length} messages {filteredByUser ? `(filtered)` : ''}. Scroll to see more.
{/if} @@ -768,11 +771,10 @@

No notifications {notificationMode === "to-me" ? "received" : "sent"} found.

{:else} -
-
- {#each notifications.slice(0, 100) as notification} - {@const authorProfile = authorProfiles.get(notification.pubkey)} -
+
+ {#each notifications.slice(0, 100) as notification} + {@const authorProfile = authorProfiles.get(notification.pubkey)} +
@@ -831,12 +833,11 @@
-
- {/each} -
+
+ {/each} {#if notifications.length > 100} -
+
Showing 100 of {notifications.length} notifications {notificationMode === "to-me" ? "received" : "sent"}. Scroll to see more.
{/if} From 49752931173ec91000dc1846f24a08be7c3c22a3 Mon Sep 17 00:00:00 2001 From: silberengel Date: Sat, 9 Aug 2025 07:49:55 +0200 Subject: [PATCH 16/27] made the kind 24 public messages ephemeral for 4 weeks --- src/lib/consts.ts | 2 ++ src/lib/utils/nostrEventService.ts | 10 +++++++++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/src/lib/consts.ts b/src/lib/consts.ts index 29f4502..f141e7b 100644 --- a/src/lib/consts.ts +++ b/src/lib/consts.ts @@ -51,5 +51,7 @@ export enum FeedType { UserRelays = "user", } +export const EXPIRATION_DURATION = 28 * 24 * 60 * 60; // 4 weeks in seconds + export const loginStorageKey = "alexandria/login/pubkey"; export const feedTypeStorageKey = "alexandria/feed/type"; diff --git a/src/lib/utils/nostrEventService.ts b/src/lib/utils/nostrEventService.ts index cdea5e1..459275c 100644 --- a/src/lib/utils/nostrEventService.ts +++ b/src/lib/utils/nostrEventService.ts @@ -3,6 +3,7 @@ import { getEventHash, signEvent, prefixNostrAddresses } from "./nostrUtils.ts"; import { get } from "svelte/store"; import { goto } from "$app/navigation"; import { EVENT_KINDS, TIME_CONSTANTS } from "./search_constants.ts"; +import { EXPIRATION_DURATION } from "../consts.ts"; import { ndkInstance } from "../ndk.ts"; import { NDKRelaySet, NDKEvent } from "@nostr-dev-kit/ndk"; @@ -320,12 +321,19 @@ export async function createSignedEvent( ): Promise<{ id: string; sig: string; event: any }> { const prefixedContent = prefixNostrAddresses(content); + // Add expiration tag for kind 24 events (NIP-40) + const finalTags = [...tags]; + if (kind === 24) { + const expirationTimestamp = Math.floor(Date.now() / TIME_CONSTANTS.UNIX_TIMESTAMP_FACTOR) + EXPIRATION_DURATION; + finalTags.push(["expiration", String(expirationTimestamp)]); + } + const eventToSign = { kind: Number(kind), created_at: Number( Math.floor(Date.now() / TIME_CONSTANTS.UNIX_TIMESTAMP_FACTOR), ), - tags: tags.map((tag) => [ + tags: finalTags.map((tag) => [ String(tag[0]), String(tag[1]), String(tag[2] || ""), From 49e8293fbeae40f61b789abefefa5731fc94d122 Mon Sep 17 00:00:00 2001 From: silberengel Date: Sat, 9 Aug 2025 08:01:56 +0200 Subject: [PATCH 17/27] unified the kind24 publishing --- src/lib/utils/kind24_utils.ts | 26 ++++++++++++-------------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/src/lib/utils/kind24_utils.ts b/src/lib/utils/kind24_utils.ts index 62a27d0..edf362f 100644 --- a/src/lib/utils/kind24_utils.ts +++ b/src/lib/utils/kind24_utils.ts @@ -4,6 +4,7 @@ import { userStore } from "../stores/userStore"; import { NDKEvent, NDKRelaySet, NDKUser } from "@nostr-dev-kit/ndk"; import type NDK from "@nostr-dev-kit/ndk"; import { nip19 } from "nostr-tools"; +import { createSignedEvent } from "./nostrEventService.ts"; /** * Fetches user's outbox relays from NIP-65 relay list @@ -140,10 +141,6 @@ export async function createKind24Reply( return { success: false, error: "No relays available for publishing" }; } - // Create the kind 24 event - const event = new NDKEvent(ndk); - event.kind = 24; - // Build content with quoted message if replying let finalContent = content; if (originalEvent) { @@ -155,13 +152,9 @@ export async function createKind24Reply( const quotedContent = originalEvent.content ? originalEvent.content.slice(0, 200) : "No content"; // Use a more visible quote format with a clickable link finalContent = `> QUOTED: ${quotedContent}\n> LINK: ${nevent}\n\n${content}`; - } - event.content = finalContent; - event.created_at = Math.floor(Date.now() / 1000); - - // Add p tag for recipient with relay URL + // Build tags for the kind 24 event const tags: string[][] = [ ["p", recipientPubkey, prioritizedRelays[0]] // Use first relay as primary ]; @@ -175,11 +168,16 @@ export async function createKind24Reply( tags.push(["q", nevent, prioritizedRelays[0]]); } - event.tags = tags; - event.pubkey = ndk.activeUser.pubkey; - - // Sign the event - await event.sign(); + // Create and sign the event using the unified function (includes expiration tag) + const { event: signedEventData } = await createSignedEvent( + finalContent, + ndk.activeUser.pubkey, + 24, + tags + ); + + // Create NDKEvent from the signed event data + const event = new NDKEvent(ndk, signedEventData); // Publish to relays const relaySet = NDKRelaySet.fromRelayUrls(prioritizedRelays, ndk); From de183ed6ffd7cfe9af3f305eec52b3b19c09cf19 Mon Sep 17 00:00:00 2001 From: silberengel Date: Sat, 9 Aug 2025 08:48:02 +0200 Subject: [PATCH 18/27] reinstate missing New Message button --- src/lib/components/Notifications.svelte | 754 ++++++++++++++++++------ src/lib/utils/profile_search.ts | 27 +- 2 files changed, 589 insertions(+), 192 deletions(-) diff --git a/src/lib/components/Notifications.svelte b/src/lib/components/Notifications.svelte index 4488952..45ee83e 100644 --- a/src/lib/components/Notifications.svelte +++ b/src/lib/components/Notifications.svelte @@ -17,6 +17,7 @@ import { Modal, Button } from "flowbite-svelte"; import { searchProfiles } from "$lib/utils/search_utility"; import type { NostrProfile } from "$lib/utils/search_types"; + import { PlusOutline } from "flowbite-svelte-icons"; const { event } = $props<{ event: NDKEvent }>(); @@ -45,14 +46,26 @@ let notificationMode = $state<"to-me" | "from-me" | "public-messages">("to-me"); let authorProfiles = $state>(new Map()); let filteredByUser = $state(null); - let replyContent = $state(""); - let replyingTo = $state(null); - let isReplying = $state(false); - let originalMessage = $state(null); - let replyingToMessageId = $state(null); - let replyRelays = $state([]); - let senderOutboxRelays = $state([]); - let recipientInboxRelays = $state([]); + + + // New Message Modal state + let showNewMessageModal = $state(false); + let newMessageContent = $state(""); + let selectedRecipients = $state([]); + let newMessageRelays = $state([]); + let isComposingMessage = $state(false); + let replyToMessage = $state(null); + let quotedContent = $state(""); + + // Recipient Selection Modal state + let showRecipientModal = $state(false); + let recipientSearch = $state(""); + let recipientResults = $state([]); + let recipientLoading = $state(false); + let recipientSearchInput = $state(); + let recipientSearchTimeout: ReturnType | null = null; + let recipientCommunityStatus: Record = $state({}); + let isRecipientSearching = $state(false); // Derived state for filtered messages let filteredMessages = $derived.by(() => { @@ -121,9 +134,10 @@ // Handle quote format and convert to small gray bars like Jumble const patterns = [ - /> QUOTED: ([^•]*?) • LINK:\s*\n(nevent[^\s]*)/g, - /> QUOTED: ([^\n]*?)\n> LINK: (nevent[^\s]*)/g, - /> QUOTED: ([^•]*?) • LINK:\s*(nevent[^\s]*)/g, + /> QUOTED: ([^•]*?) • LINK:\s*\n((?:nostr:)?nevent[^\s]*)/g, + /> QUOTED: ([^\n]*?)\n> LINK: ((?:nostr:)?nevent[^\s]*)/g, + /> QUOTED: ([^•]*?) • LINK:\s*((?:nostr:)?nevent[^\s]*)/g, + /> QUOTED: ([^•]*?) • LINK: ((?:nostr:)?nevent[^\s]*)/g, // Without optional whitespace ]; for (const pattern of patterns) { @@ -199,139 +213,301 @@ filteredByUser = null; } - // AI-NOTE: Reply functionality for kind 24 messages - async function startReply(pubkey: string, messageEvent?: NDKEvent) { - replyingTo = pubkey; - isReplying = true; - replyContent = ""; - replyingToMessageId = messageEvent?.id || null; - // Store the original message for q tag - originalMessage = messageEvent || null; - // Clear previous relay information - replyRelays = []; - senderOutboxRelays = []; - recipientInboxRelays = []; + + + // AI-NOTE: New Message Modal Functions + function openNewMessageModal(messageToReplyTo?: NDKEvent) { + showNewMessageModal = true; + newMessageContent = ""; + selectedRecipients = []; + newMessageRelays = []; + isComposingMessage = false; + replyToMessage = messageToReplyTo || null; - // Immediately fetch relay information for this recipient - await getReplyRelays(); + // If replying, set up the quote and pre-select all original recipients plus sender + if (messageToReplyTo) { + // Store clean content for UI display (no markdown formatting) + quotedContent = messageToReplyTo.content.length > 200 + ? messageToReplyTo.content.slice(0, 200) + "..." + : messageToReplyTo.content; + + // Collect all recipients: original sender + all p-tag recipients + const recipientPubkeys = new Set(); + + // Add the original sender + recipientPubkeys.add(messageToReplyTo.pubkey); + + // Add all p-tag recipients from the original message + const pTags = messageToReplyTo.getMatchingTags("p"); + pTags.forEach(tag => { + if (tag[1]) { + recipientPubkeys.add(tag[1]); + } + }); + + // Remove the current user from recipients (don't reply to yourself) + const currentUserPubkey = $userStore.pubkey; + if (currentUserPubkey) { + recipientPubkeys.delete(currentUserPubkey); + } + + // Build the recipient list with profile information + selectedRecipients = Array.from(recipientPubkeys).map(pubkey => { + const profile = authorProfiles.get(pubkey); + return { + pubkey: pubkey, + name: profile?.name || "", + displayName: profile?.displayName || "", + picture: profile?.picture || "", + about: "", // We don't store about in authorProfiles + nip05: "", // We don't store nip05 in authorProfiles + }; + }).filter(recipient => recipient.pubkey); // Ensure we have valid pubkeys + + console.log(`Pre-loaded ${selectedRecipients.length} recipients for reply:`, selectedRecipients.map(r => r.displayName || r.name || r.pubkey?.slice(0, 8))); + } else { + quotedContent = ""; + } + } + + function closeNewMessageModal() { + showNewMessageModal = false; + newMessageContent = ""; + selectedRecipients = []; + newMessageRelays = []; + isComposingMessage = false; + replyToMessage = null; + quotedContent = ""; + } + + // AI-NOTE: Recipient Selection Modal Functions + function openRecipientModal() { + showRecipientModal = true; + recipientSearch = ""; + recipientResults = []; + recipientLoading = false; + recipientCommunityStatus = {}; + isRecipientSearching = false; + // Focus the search input after a brief delay to ensure modal is rendered + setTimeout(() => { + recipientSearchInput?.focus(); + }, 100); } - function cancelReply() { - replyingTo = null; - isReplying = false; - replyContent = ""; - replyingToMessageId = null; - replyRelays = []; - senderOutboxRelays = []; - recipientInboxRelays = []; + function closeRecipientModal() { + showRecipientModal = false; + recipientSearch = ""; + recipientResults = []; + recipientLoading = false; + recipientCommunityStatus = {}; + isRecipientSearching = false; + + // Clear any pending search timeout + if (recipientSearchTimeout) { + clearTimeout(recipientSearchTimeout); + recipientSearchTimeout = null; + } } - async function sendReply() { - if (!replyingTo || !replyContent.trim()) return; + async function searchRecipients() { + if (!recipientSearch.trim()) { + recipientResults = []; + recipientCommunityStatus = {}; + return; + } + + // Prevent multiple concurrent searches + if (isRecipientSearching) { + return; + } + + console.log("Starting recipient search for:", recipientSearch.trim()); + + // Set loading state + recipientLoading = true; + isRecipientSearching = true; try { - // Find the original message being replied to - const originalMessage = publicMessages.find(msg => msg.id === replyingToMessageId); - const result = await createKind24Reply(replyContent, replyingTo, originalMessage); - - if (result.success) { - // Store relay information for display - replyRelays = result.relays || []; - - // Update the inbox/outbox arrays to match the actual relays being used - // Keep only the top 3 that are actually in the reply relay set - const replyRelaySet = new Set(replyRelays); - senderOutboxRelays = senderOutboxRelays - .filter(relay => replyRelaySet.has(relay)) - .slice(0, 3); - recipientInboxRelays = recipientInboxRelays - .filter(relay => replyRelaySet.has(relay)) - .slice(0, 3); - - // Clear reply state - replyingTo = null; - isReplying = false; - replyContent = ""; - replyingToMessageId = null; - // Optionally refresh messages - await fetchPublicMessages(); - } else { - console.error("Failed to send reply:", result.error); - // You could show an error message to the user here - } + console.log("Recipient search promise created, waiting for result..."); + const result = await searchProfiles(recipientSearch.trim()); + console.log("Recipient search completed, found profiles:", result.profiles.length); + console.log("Profile details:", result.profiles); + console.log("Community status:", result.Status); + + // Update state + recipientResults = result.profiles; + recipientCommunityStatus = result.Status; + + console.log( + "State updated - recipientResults length:", + recipientResults.length, + ); + console.log( + "State updated - recipientCommunityStatus keys:", + Object.keys(recipientCommunityStatus), + ); } catch (error) { - console.error("Error sending reply:", error); + console.error("Error searching recipients:", error); + recipientResults = []; + recipientCommunityStatus = {}; + } finally { + recipientLoading = false; + isRecipientSearching = false; + console.log( + "Recipient search finished - loading:", + recipientLoading, + "searching:", + isRecipientSearching, + ); + } + } + + // Reactive search with debouncing + $effect(() => { + // Clear existing timeout + if (recipientSearchTimeout) { + clearTimeout(recipientSearchTimeout); + } + + // If search is empty, clear results immediately + if (!recipientSearch.trim()) { + recipientResults = []; + recipientCommunityStatus = {}; + recipientLoading = false; + return; + } + + // Set loading state immediately for better UX + recipientLoading = true; + + // Debounce the search with 300ms delay + recipientSearchTimeout = setTimeout(() => { + searchRecipients(); + }, 300); + }); + + function selectRecipient(profile: NostrProfile) { + // Check if recipient is already selected + if (selectedRecipients.some(r => r.pubkey === profile.pubkey)) { + console.log("Recipient already selected:", profile.displayName || profile.name); + return; } + + // Add recipient to selection + selectedRecipients = [...selectedRecipients, profile]; + console.log("Selected recipient:", profile.displayName || profile.name); + + // Close the recipient modal (New Message modal stays open) + closeRecipientModal(); } - // Function to get relay information before sending - async function getReplyRelays() { - if (!replyingTo) return; + async function sendNewMessage() { + if (!newMessageContent.trim() || selectedRecipients.length === 0) return; try { - const originalMessage = publicMessages.find(msg => msg.id === replyingToMessageId); + isComposingMessage = true; - // Get sender's outbox relays and recipient's inbox relays - const ndk = get(ndkInstance); - if (ndk?.activeUser) { - // Get sender's outbox relays - const senderUser = ndk.activeUser; - const senderRelayList = await ndk.fetchEvent({ - kinds: [10002], - authors: [senderUser.pubkey], - }); - - if (senderRelayList) { - senderOutboxRelays = senderRelayList.tags - .filter(tag => tag[0] === 'r' && tag[1]) - .map(tag => tag[1]) - .slice(0, 3); // Limit to top 3 outbox relays - } - - // Get recipient's inbox relays - const recipientUser = ndk.getUser({ pubkey: replyingTo }); - const recipientRelayList = await ndk.fetchEvent({ - kinds: [10002], - authors: [replyingTo], - }); - - if (recipientRelayList) { - recipientInboxRelays = recipientRelayList.tags - .filter(tag => tag[0] === 'r' && tag[1]) - .map(tag => tag[1]) - .slice(0, 3); // Limit to top 3 inbox relays - } + // Create p-tags for all recipients + const pTags = selectedRecipients.map(recipient => ["p", recipient.pubkey!]); + + // Get all recipient pubkeys for relay calculation + const recipientPubkeys = selectedRecipients.map(r => r.pubkey!); + + // Calculate relay set using the same logic as kind24_utils + const senderPubkey = $userStore.pubkey; + if (!senderPubkey) { + throw new Error("No sender pubkey available"); } - // If we have content, use the actual reply function - if (replyContent.trim()) { - const result = await createKind24Reply(replyContent, replyingTo, originalMessage); - replyRelays = result.relays || []; + // Get relay sets for all recipients and combine them + const relaySetPromises = recipientPubkeys.map(recipientPubkey => + getKind24RelaySet(senderPubkey, recipientPubkey) + ); + const relaySets = await Promise.all(relaySetPromises); + + // Combine and deduplicate all relay sets + const allRelays = relaySets.flat(); + const uniqueRelays = [...new Set(allRelays)]; + newMessageRelays = uniqueRelays; + + // Create the kind 24 event with quoted content if replying + let finalContent = newMessageContent; + if (replyToMessage && quotedContent) { + // Generate the markdown quote format for the actual message + const neventUrl = getNeventUrl(replyToMessage); + const markdownQuote = `> QUOTED: ${quotedContent} • LINK: ${neventUrl}`; + finalContent = markdownQuote + "\n\n" + newMessageContent; + } + + const eventData = { + kind: 24, + content: finalContent, + tags: pTags, + pubkey: $userStore.pubkey || '', + created_at: Math.floor(Date.now() / 1000) + }; + + // Sign the event + let signedEvent; + if (typeof window !== "undefined" && window.nostr && window.nostr.signEvent) { + signedEvent = await window.nostr.signEvent(eventData); } else { - // If no content yet, just get the relay set for this recipient - const result = await getKind24RelaySet($userStore.pubkey || '', replyingTo); - replyRelays = result || []; - - // Update the inbox/outbox arrays to match the actual relays being used - // Keep only the top 3 that are actually in the reply relay set - const replyRelaySet = new Set(replyRelays); - senderOutboxRelays = senderOutboxRelays - .filter(relay => replyRelaySet.has(relay)) - .slice(0, 3); - recipientInboxRelays = recipientInboxRelays - .filter(relay => replyRelaySet.has(relay)) - .slice(0, 3); - + throw new Error("No signing method available"); + } + // Publish to relays using WebSocket pool like other components + const { WebSocketPool } = await import("$lib/data_structures/websocket_pool"); + let publishedToAny = false; + + for (const relayUrl of newMessageRelays) { + try { + const ws = await WebSocketPool.instance.acquire(relayUrl); + + await new Promise((resolve, reject) => { + const timeout = setTimeout(() => { + WebSocketPool.instance.release(ws); + reject(new Error("Timeout")); + }, 5000); + + ws.onmessage = (e) => { + const [type, id, ok, message] = JSON.parse(e.data); + if (type === "OK" && id === signedEvent.id) { + clearTimeout(timeout); + if (ok) { + publishedToAny = true; + WebSocketPool.instance.release(ws); + resolve(); + } else { + WebSocketPool.instance.release(ws); + reject(new Error(message)); + } + } + }; + + ws.send(JSON.stringify(["EVENT", signedEvent])); + }); + } catch (e) { + console.warn(`Failed to publish to ${relayUrl}:`, e); + } + } + + if (publishedToAny) { + // Close modal and refresh messages + closeNewMessageModal(); + await fetchPublicMessages(); + } else { + throw new Error("Failed to publish to any relay"); } } catch (error) { - console.error("Error getting relay information:", error); - replyRelays = []; - senderOutboxRelays = []; - recipientInboxRelays = []; + console.error("Error sending new message:", error); + // You could show an error message to the user here + } finally { + isComposingMessage = false; } } + + // AI-NOTE: Simplified profile fetching with better error handling async function fetchAuthorProfiles(events: NDKEvent[]) { const uniquePubkeys = new Set(); @@ -545,10 +721,30 @@ } }); - // Fetch relay information when reply content changes (for updates) + + + // Calculate relay set when recipients change $effect(() => { - if (isReplying && replyingTo && replyContent.trim() && replyRelays.length === 0) { - getReplyRelays(); + const senderPubkey = $userStore.pubkey; + if (selectedRecipients.length > 0 && senderPubkey) { + const recipientPubkeys = selectedRecipients.map(r => r.pubkey!); + + // Get relay sets for all recipients and combine them + const relaySetPromises = recipientPubkeys.map(recipientPubkey => + getKind24RelaySet(senderPubkey, recipientPubkey) + ); + + Promise.all(relaySetPromises).then(relaySets => { + // Combine and deduplicate all relay sets + const allRelays = relaySets.flat(); + const uniqueRelays = [...new Set(allRelays)]; + newMessageRelays = uniqueRelays; + }).catch(error => { + console.error("Error getting relay set:", error); + newMessageRelays = []; + }); + } else { + newMessageRelays = []; } }); @@ -558,6 +754,18 @@
Notifications +
+ + +
{#each ["to-me", "from-me", "public-messages"] as mode} @@ -569,6 +777,7 @@ {modeLabel} {/each} +
@@ -634,12 +843,16 @@ @@ -697,63 +910,6 @@
- - {#if isReplying && replyingToMessageId === message.id} - {@const recipientProfile = authorProfiles.get(message.pubkey)} -
-
- - Replying to: {recipientProfile?.displayName || recipientProfile?.name || `${message.pubkey.slice(0, 8)}...${message.pubkey.slice(-4)}`} - - -
-
- - -
- - -
- {#if replyRelays.length > 0} - - - {:else} -
-
- Loading relay information... -
- {/if} -
-
- {/if}
{/each}
@@ -845,4 +1001,236 @@ {/if} {/if}
+ + + +
+
+

+ {replyToMessage ? 'Reply to Message' : 'New Public Message'} +

+
+ + + {#if quotedContent} +
+
Replying to:
+
+ {@html renderContentWithLinks(quotedContent)} +
+
+ {/if} + + +
+
+ + Sending to {selectedRecipients.length} recipient{selectedRecipients.length !== 1 ? 's' : ''}: + + +
+ + {#if selectedRecipients.length === 0} +
+

+ No recipients selected. Click "Edit Recipients" to add recipients. +

+
+ {:else} +
+ {#each selectedRecipients as recipient} + + {recipient.displayName || recipient.name || `${recipient.pubkey?.slice(0, 8)}...`} + + + {/each} +
+ {/if} +
+ + + {#if selectedRecipients.length > 0 && newMessageRelays.length > 0} +
+ + Publishing to {newMessageRelays.length} relay{newMessageRelays.length !== 1 ? 's' : ''}: + +
+
+ {#each newMessageRelays as relay} +
+ {relay} +
+ {/each} +
+
+
+ {/if} + + +
+ + +
+ + +
+ + +
+
+
+ + + +
+
+

Select Recipients

+
+ +
+
+ + {#if recipientLoading} +
+
+
+ {/if} +
+ + {#if recipientResults.length > 0} +
+
    + {#each recipientResults as profile} + {@const isAlreadySelected = selectedRecipients.some(r => r.pubkey === profile.pubkey)} + + {/each} +
+
+ {:else if recipientSearch.trim()} +
No results found
+ {:else} +
+ Enter a search term to find users +
+ {/if} +
+
+
{/if} \ No newline at end of file diff --git a/src/lib/utils/profile_search.ts b/src/lib/utils/profile_search.ts index eeac332..ecf43ec 100644 --- a/src/lib/utils/profile_search.ts +++ b/src/lib/utils/profile_search.ts @@ -1,8 +1,8 @@ -import { ndkInstance } from "../ndk.ts"; +import { ndkInstance, activeInboxRelays } from "../ndk.ts"; import { getUserMetadata, getNpubFromNip05 } from "./nostrUtils.ts"; import NDK, { NDKRelaySet, NDKEvent } from "@nostr-dev-kit/ndk"; import { searchCache } from "./searchCache.ts"; -import { communityRelays, secondaryRelays } from "../consts.ts"; +import { searchRelays, communityRelays, secondaryRelays } from "../consts.ts"; import { get } from "svelte/store"; import type { NostrProfile, ProfileSearchResult } from "./search_types.ts"; import { @@ -264,12 +264,21 @@ async function quickRelaySearch( const normalizedSearchTerm = normalizeSearchTerm(searchTerm); console.log("Normalized search term for relay search:", normalizedSearchTerm); - // Use all profile relays for better coverage - const quickRelayUrls = [...communityRelays, ...secondaryRelays]; // Use all available relays - console.log("Using all relays for search:", quickRelayUrls); + // Use search relays (optimized for profiles) + user's inbox relays + community relays + const userInboxRelays = get(activeInboxRelays); + const quickRelayUrls = [ + ...searchRelays, // Dedicated profile search relays + ...userInboxRelays, // User's personal inbox relays + ...communityRelays, // Community relays + ...secondaryRelays // Secondary relays as fallback + ]; + + // Deduplicate relay URLs + const uniqueRelayUrls = [...new Set(quickRelayUrls)]; + console.log("Using relays for profile search:", uniqueRelayUrls); // Create relay sets for parallel search - const relaySets = quickRelayUrls + const relaySets = uniqueRelayUrls .map((url) => { try { return NDKRelaySet.fromRelayUrls([url], ndk); @@ -289,7 +298,7 @@ async function quickRelaySearch( let eventCount = 0; console.log( - `Starting search on relay ${index + 1}: ${quickRelayUrls[index]}`, + `Starting search on relay ${index + 1}: ${uniqueRelayUrls[index]}`, ); const sub = ndk.subscribe( @@ -354,7 +363,7 @@ async function quickRelaySearch( sub.on("eose", () => { console.log( - `Relay ${index + 1} (${quickRelayUrls[index]}) search completed, processed ${eventCount} events, found ${foundInRelay.length} matches`, + `Relay ${index + 1} (${uniqueRelayUrls[index]}) search completed, processed ${eventCount} events, found ${foundInRelay.length} matches`, ); resolve(foundInRelay); }); @@ -362,7 +371,7 @@ async function quickRelaySearch( // Short timeout for quick search setTimeout(() => { console.log( - `Relay ${index + 1} (${quickRelayUrls[index]}) search timed out after 1.5s, processed ${eventCount} events, found ${foundInRelay.length} matches`, + `Relay ${index + 1} (${uniqueRelayUrls[index]}) search timed out after 1.5s, processed ${eventCount} events, found ${foundInRelay.length} matches`, ); sub.stop(); resolve(foundInRelay); From a10f62c1e1d4d2eac4821db19bbc40493b83d0d2 Mon Sep 17 00:00:00 2001 From: silberengel Date: Sat, 9 Aug 2025 09:12:05 +0200 Subject: [PATCH 19/27] show all 100 messages --- src/lib/components/Notifications.svelte | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/src/lib/components/Notifications.svelte b/src/lib/components/Notifications.svelte index 45ee83e..548896b 100644 --- a/src/lib/components/Notifications.svelte +++ b/src/lib/components/Notifications.svelte @@ -17,7 +17,7 @@ import { Modal, Button } from "flowbite-svelte"; import { searchProfiles } from "$lib/utils/search_utility"; import type { NostrProfile } from "$lib/utils/search_types"; - import { PlusOutline } from "flowbite-svelte-icons"; + import { PlusOutline, ReplyOutline } from "flowbite-svelte-icons"; const { event } = $props<{ event: NDKEvent }>(); @@ -410,6 +410,13 @@ // Create p-tags for all recipients const pTags = selectedRecipients.map(recipient => ["p", recipient.pubkey!]); + // Add q tag if replying to a message (for jump-to functionality) + if (replyToMessage) { + // Get the first relay from newMessageRelays or use a fallback + const relayUrl = newMessageRelays[0] || "wss://freelay.sovbit.host/"; + pTags.push(["q", replyToMessage.id, relayUrl, replyToMessage.pubkey]); + } + // Get all recipient pubkeys for relay calculation const recipientPubkeys = selectedRecipients.map(r => r.pubkey!); @@ -851,9 +858,7 @@ title="Reply to this message" aria-label="Reply to this message" > - - - +
From 493c08daeeb9f7d7c8cc7eb7f98c1e8a75d4d5d3 Mon Sep 17 00:00:00 2001 From: silberengel Date: Sat, 9 Aug 2025 09:51:07 +0200 Subject: [PATCH 20/27] fixed jump quotes --- src/lib/components/Notifications.svelte | 55 ++++++++++++++++++++++++- 1 file changed, 54 insertions(+), 1 deletion(-) diff --git a/src/lib/components/Notifications.svelte b/src/lib/components/Notifications.svelte index 548896b..136917e 100644 --- a/src/lib/components/Notifications.svelte +++ b/src/lib/components/Notifications.svelte @@ -128,6 +128,59 @@ return content.slice(0, maxLength) + "..."; } + function truncateRenderedContent(renderedHtml: string, maxLength: number = 300): string { + // If the rendered HTML is short enough, return as-is + if (renderedHtml.length <= maxLength) return renderedHtml; + + // Check if there are any gray quote boxes (jump-to-message divs) + const hasQuoteBoxes = renderedHtml.includes('jump-to-message'); + + if (hasQuoteBoxes) { + // Split content into quote boxes and regular text + const quoteBoxPattern = /
]*>[^<]*<\/div>/g; + const quoteBoxes = renderedHtml.match(quoteBoxPattern) || []; + + // Remove quote boxes temporarily to measure text length + let textOnly = renderedHtml.replace(quoteBoxPattern, '|||QUOTEBOX|||'); + + // If text without quote boxes is still too long, truncate it + if (textOnly.length > maxLength) { + const availableLength = maxLength - (quoteBoxes.join('').length); + if (availableLength > 50) { // Leave some reasonable space for text + textOnly = textOnly.slice(0, availableLength) + "..."; + } else { + // If quote boxes take up too much space, just show them with minimal text + textOnly = textOnly.slice(0, 50) + "..."; + } + } + + // Restore quote boxes + let result = textOnly; + quoteBoxes.forEach(box => { + result = result.replace('|||QUOTEBOX|||', box); + }); + + return result; + } else { + // No quote boxes, simple truncation with HTML awareness + if (renderedHtml.includes('<')) { + // Has HTML tags, do a simple truncation but try to avoid breaking tags + const truncated = renderedHtml.slice(0, maxLength); + const lastTagStart = truncated.lastIndexOf('<'); + const lastTagEnd = truncated.lastIndexOf('>'); + + if (lastTagStart > lastTagEnd) { + // We're in the middle of a tag, truncate before it + return renderedHtml.slice(0, lastTagStart) + "..."; + } + return truncated + "..."; + } else { + // Plain text, simple truncation + return renderedHtml.slice(0, maxLength) + "..."; + } + } + } + function renderContentWithLinks(content: string): string { // Parse markdown links [text](url) and convert to HTML let rendered = content.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '$1'); @@ -907,7 +960,7 @@ {#if message.content}
- {@html renderContentWithLinks(truncateContent(message.content))} + {@html truncateRenderedContent(renderContentWithLinks(message.content), 300)}
{/if} From e8ce9bb8eb5289d849fdc92b419630ea31453423 Mon Sep 17 00:00:00 2001 From: silberengel Date: Sat, 9 Aug 2025 10:25:24 +0200 Subject: [PATCH 21/27] fix inbox/outbox selection --- src/lib/components/Notifications.svelte | 74 ++++++++++++++++++++++--- src/lib/utils/kind24_utils.ts | 54 +++++++++++++----- 2 files changed, 107 insertions(+), 21 deletions(-) diff --git a/src/lib/components/Notifications.svelte b/src/lib/components/Notifications.svelte index 136917e..8c6b992 100644 --- a/src/lib/components/Notifications.svelte +++ b/src/lib/components/Notifications.svelte @@ -460,8 +460,22 @@ try { isComposingMessage = true; - // Create p-tags for all recipients - const pTags = selectedRecipients.map(recipient => ["p", recipient.pubkey!]); + // Create p-tags for all recipients (ensure hex format) + const pTags = selectedRecipients.map(recipient => { + let pubkey = recipient.pubkey!; + // Convert npub to hex if needed + if (pubkey.startsWith('npub')) { + try { + const decoded = nip19.decode(pubkey); + if (decoded.type === 'npub') { + pubkey = decoded.data; + } + } catch (e) { + console.warn("[Send Message] Failed to decode npub:", pubkey, e); + } + } + return ["p", pubkey]; + }); // Add q tag if replying to a message (for jump-to functionality) if (replyToMessage) { @@ -470,8 +484,22 @@ pTags.push(["q", replyToMessage.id, relayUrl, replyToMessage.pubkey]); } - // Get all recipient pubkeys for relay calculation - const recipientPubkeys = selectedRecipients.map(r => r.pubkey!); + // Get all recipient pubkeys for relay calculation (ensure hex format) + const recipientPubkeys = selectedRecipients.map(r => { + let pubkey = r.pubkey!; + // Convert npub to hex if needed + if (pubkey.startsWith('npub')) { + try { + const decoded = nip19.decode(pubkey); + if (decoded.type === 'npub') { + pubkey = decoded.data; + } + } catch (e) { + console.warn("[Send Message Relay Calc] Failed to decode npub:", pubkey, e); + } + } + return pubkey; + }); // Calculate relay set using the same logic as kind24_utils const senderPubkey = $userStore.pubkey; @@ -786,8 +814,25 @@ // Calculate relay set when recipients change $effect(() => { const senderPubkey = $userStore.pubkey; + console.log("[Relay Effect] Recipients changed:", selectedRecipients.length, "Sender:", senderPubkey?.slice(0, 8)); + if (selectedRecipients.length > 0 && senderPubkey) { - const recipientPubkeys = selectedRecipients.map(r => r.pubkey!); + const recipientPubkeys = selectedRecipients.map(r => { + const pubkey = r.pubkey!; + // Convert npub to hex if needed + if (pubkey.startsWith('npub')) { + try { + const decoded = nip19.decode(pubkey); + if (decoded.type === 'npub') { + return decoded.data; + } + } catch (e) { + console.warn("[Relay Effect] Failed to decode npub:", pubkey, e); + } + } + return pubkey; + }); + console.log("[Relay Effect] Getting relay sets for recipients (hex):", recipientPubkeys.map(p => p.slice(0, 8))); // Get relay sets for all recipients and combine them const relaySetPromises = recipientPubkeys.map(recipientPubkey => @@ -795,15 +840,28 @@ ); Promise.all(relaySetPromises).then(relaySets => { + console.log("[Relay Effect] Received relay sets:", relaySets); // Combine and deduplicate all relay sets const allRelays = relaySets.flat(); const uniqueRelays = [...new Set(allRelays)]; - newMessageRelays = uniqueRelays; + console.log("[Relay Effect] Final relay list:", uniqueRelays); + + // If no relays found from NIP-65, use fallback relays + if (uniqueRelays.length === 0) { + console.log("[Relay Effect] No NIP-65 relays found, using fallback"); + const fallbackRelays = getAvailableRelays(); + newMessageRelays = fallbackRelays.slice(0, 5); // Limit to first 5 for performance + } else { + newMessageRelays = uniqueRelays; + } }).catch(error => { - console.error("Error getting relay set:", error); - newMessageRelays = []; + console.error("[Relay Effect] Error getting relay set:", error); + console.log("[Relay Effect] Using fallback relays due to error"); + const fallbackRelays = getAvailableRelays(); + newMessageRelays = fallbackRelays.slice(0, 5); }); } else { + console.log("[Relay Effect] Clearing relays - no recipients or sender"); newMessageRelays = []; } }); diff --git a/src/lib/utils/kind24_utils.ts b/src/lib/utils/kind24_utils.ts index edf362f..9d1271e 100644 --- a/src/lib/utils/kind24_utils.ts +++ b/src/lib/utils/kind24_utils.ts @@ -31,12 +31,11 @@ async function getUseroutboxRelays(ndk: NDK, user: NDKUser): Promise { if (tag[0] === 'r' && tag[1]) { // NIP-65: r tags with optional inbox/outbox markers const marker = tag[2]; - if (!marker || marker === 'outbox' || marker === 'inbox') { - // If no marker or marker is 'outbox', it's a outbox relay - // If marker is 'inbox', it's also a outbox relay (NIP-65 allows both) + if (!marker || marker === 'outbox' || marker === 'both') { + // If no marker, marker is 'outbox', or marker is 'both', it's an outbox relay outboxRelays.push(tag[1]); - } + // Note: inbox-only relays are NOT included in outbox relays } }); @@ -56,6 +55,7 @@ async function getUseroutboxRelays(ndk: NDK, user: NDKUser): Promise { */ async function getUserinboxRelays(ndk: NDK, user: NDKUser): Promise { try { + console.log(`[getUserinboxRelays] Fetching kind 10002 for user: ${user.pubkey.slice(0, 8)}`); const relayList = await ndk.fetchEvent( { @@ -65,27 +65,31 @@ async function getUserinboxRelays(ndk: NDK, user: NDKUser): Promise { ); if (!relayList) { + console.log(`[getUserinboxRelays] No kind 10002 relay list found for user: ${user.pubkey.slice(0, 8)}`); return []; } + console.log(`[getUserinboxRelays] Found relay list for user: ${user.pubkey.slice(0, 8)}, tags:`, relayList.tags); + const inboxRelays: string[] = []; relayList.tags.forEach((tag) => { if (tag[0] === 'r' && tag[1]) { // NIP-65: r tags with optional inbox/outbox markers const marker = tag[2]; - if (!marker || marker === 'inbox' || marker === 'outbox') { - // If no marker or marker is 'inbox', it's a inbox relay - // If marker is 'outbox', it's also a inbox relay (NIP-65 allows both) + console.log(`[getUserinboxRelays] Processing relay tag:`, tag, `marker: ${marker}`); + if (!marker || marker === 'inbox' || marker === 'both') { + // If no marker, marker is 'inbox', or marker is 'both', it's an inbox relay inboxRelays.push(tag[1]); - + console.log(`[getUserinboxRelays] Added inbox relay: ${tag[1]} (marker: ${marker || 'none'})`); } + // Note: outbox-only relays are NOT included in inbox relays } }); - + console.log(`[getUserinboxRelays] Final inbox relays for user ${user.pubkey.slice(0, 8)}:`, inboxRelays); return inboxRelays; } catch (error) { - + console.error(`[getUserinboxRelays] Error fetching inbox relays for user ${user.pubkey.slice(0, 8)}:`, error); return []; } } @@ -117,7 +121,13 @@ export async function createKind24Reply( // Get recipient's inbox relays (NIP-65) const recipientUser = ndk.getUser({ pubkey: recipientPubkey }); - const recipientinboxRelays = await getUserinboxRelays(ndk, recipientUser); + let recipientinboxRelays = await getUserinboxRelays(ndk, recipientUser); + + // Fallback: if no inbox relays found, use recipient's outbox relays + if (recipientinboxRelays.length === 0) { + console.log(`[createKind24Reply] No inbox relays found for recipient, falling back to outbox relays`); + recipientinboxRelays = await getUseroutboxRelays(ndk, recipientUser); + } // According to NIP-A4: Messages MUST be sent to the NIP-65 inbox relays of each receiver // and the outbox relay of the sender @@ -212,17 +222,29 @@ export async function getKind24RelaySet( throw new Error("NDK not available"); } + console.log(`[getKind24RelaySet] Getting relays for sender: ${senderPubkey.slice(0, 8)} -> recipient: ${recipientPubkey.slice(0, 8)}`); + // Get sender's outbox relays (NIP-65) const senderUser = ndk.getUser({ pubkey: senderPubkey }); const senderoutboxRelays = await getUseroutboxRelays(ndk, senderUser); + console.log(`[getKind24RelaySet] Sender outbox relays:`, senderoutboxRelays); // Get recipient's inbox relays (NIP-65) const recipientUser = ndk.getUser({ pubkey: recipientPubkey }); - const recipientinboxRelays = await getUserinboxRelays(ndk, recipientUser); + let recipientinboxRelays = await getUserinboxRelays(ndk, recipientUser); + console.log(`[getKind24RelaySet] Recipient inbox relays:`, recipientinboxRelays); + + // Fallback: if no inbox relays found, use recipient's outbox relays + if (recipientinboxRelays.length === 0) { + console.log(`[getKind24RelaySet] No inbox relays found for recipient, falling back to outbox relays`); + recipientinboxRelays = await getUseroutboxRelays(ndk, recipientUser); + console.log(`[getKind24RelaySet] Recipient outbox relays (used as fallback):`, recipientinboxRelays); + } // According to NIP-A4: Messages MUST be sent to the NIP-65 inbox relays of each receiver // and the outbox relay of the sender const targetRelays = [...new Set([...senderoutboxRelays, ...recipientinboxRelays])]; + console.log(`[getKind24RelaySet] Combined target relays:`, targetRelays); // Prioritize common relays between sender and recipient for better privacy const commonRelays = senderoutboxRelays.filter((relay: string) => @@ -235,6 +257,12 @@ export async function getKind24RelaySet( !senderoutboxRelays.includes(relay) ); + console.log(`[getKind24RelaySet] Common relays:`, commonRelays); + console.log(`[getKind24RelaySet] Sender-only relays:`, senderOnlyRelays); + console.log(`[getKind24RelaySet] Recipient-only relays:`, recipientOnlyRelays); + // Prioritize: common relays first, then sender outbox, then recipient inbox - return [...commonRelays, ...senderOnlyRelays, ...recipientOnlyRelays]; + const finalRelays = [...commonRelays, ...senderOnlyRelays, ...recipientOnlyRelays]; + console.log(`[getKind24RelaySet] Final relay list:`, finalRelays); + return finalRelays; } From d31b8bfc13e3333aa5c370e567e35208c5bb8256 Mon Sep 17 00:00:00 2001 From: silberengel Date: Sat, 9 Aug 2025 10:29:15 +0200 Subject: [PATCH 22/27] got rid of noise --- src/lib/utils.ts | 8 -------- 1 file changed, 8 deletions(-) diff --git a/src/lib/utils.ts b/src/lib/utils.ts index 18fad03..60237f8 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -20,13 +20,6 @@ export class InvalidKindError extends DecodeError { export function neventEncode(event: NDKEvent, relays: string[]) { 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, @@ -34,7 +27,6 @@ export function neventEncode(event: NDKEvent, relays: string[]) { author: event.pubkey, }); - console.log(`[neventEncode] Generated nevent:`, nevent); return nevent; } catch (error) { console.error(`[neventEncode] Error encoding nevent:`, error); From e39febc25fc8dd811b50b1139f080e4c9e842201 Mon Sep 17 00:00:00 2001 From: silberengel Date: Sat, 9 Aug 2025 10:37:55 +0200 Subject: [PATCH 23/27] reinstate ephemeral tag --- src/lib/components/Notifications.svelte | 23 ++++++++--------------- 1 file changed, 8 insertions(+), 15 deletions(-) diff --git a/src/lib/components/Notifications.svelte b/src/lib/components/Notifications.svelte index 8c6b992..fa8e8f4 100644 --- a/src/lib/components/Notifications.svelte +++ b/src/lib/components/Notifications.svelte @@ -12,6 +12,7 @@ import { nip19 } from "nostr-tools"; import { communityRelays, localRelays } from "$lib/consts"; import { createKind24Reply, getKind24RelaySet } from "$lib/utils/kind24_utils"; + import { createSignedEvent } from "$lib/utils/nostrEventService"; import RelayDisplay from "$lib/components/RelayDisplay.svelte"; import RelayInfoList from "$lib/components/RelayInfoList.svelte"; import { Modal, Button } from "flowbite-svelte"; @@ -527,21 +528,13 @@ finalContent = markdownQuote + "\n\n" + newMessageContent; } - const eventData = { - kind: 24, - content: finalContent, - tags: pTags, - pubkey: $userStore.pubkey || '', - created_at: Math.floor(Date.now() / 1000) - }; - - // Sign the event - let signedEvent; - if (typeof window !== "undefined" && window.nostr && window.nostr.signEvent) { - signedEvent = await window.nostr.signEvent(eventData); - } else { - throw new Error("No signing method available"); - } + // Create and sign the event using the unified function (includes expiration tag) + const { event: signedEvent } = await createSignedEvent( + finalContent, + $userStore.pubkey || '', + 24, + pTags + ); // Publish to relays using WebSocket pool like other components const { WebSocketPool } = await import("$lib/data_structures/websocket_pool"); From 9ea1462bcb2b1e0af22cde65dee2665b0f81a461 Mon Sep 17 00:00:00 2001 From: silberengel Date: Sun, 10 Aug 2025 15:11:55 +0200 Subject: [PATCH 24/27] Fixed reactivity inefficiency --- src/lib/components/Notifications.svelte | 65 +++++++++++++++---------- src/routes/+layout.svelte | 16 +++--- src/routes/events/+page.svelte | 16 +++--- 3 files changed, 58 insertions(+), 39 deletions(-) diff --git a/src/lib/components/Notifications.svelte b/src/lib/components/Notifications.svelte index fa8e8f4..f46607a 100644 --- a/src/lib/components/Notifications.svelte +++ b/src/lib/components/Notifications.svelte @@ -804,18 +804,32 @@ - // Calculate relay set when recipients change + // AI-NOTE: Refactored to avoid blocking $effect with async operations + // Calculate relay set when recipients change - non-blocking approach $effect(() => { const senderPubkey = $userStore.pubkey; console.log("[Relay Effect] Recipients changed:", selectedRecipients.length, "Sender:", senderPubkey?.slice(0, 8)); if (selectedRecipients.length > 0 && senderPubkey) { - const recipientPubkeys = selectedRecipients.map(r => { + // Start async relay set calculation without blocking the effect + updateRelaySet(selectedRecipients, senderPubkey); + } else { + console.log("[Relay Effect] Clearing relays - no recipients or sender"); + newMessageRelays = []; + } + }); + + /** + * Updates relay set asynchronously to avoid blocking the reactive system + */ + async function updateRelaySet(recipients: any[], senderPubkey: string) { + try { + const recipientPubkeys = recipients.map(r => { const pubkey = r.pubkey!; // Convert npub to hex if needed if (pubkey.startsWith('npub')) { try { - const decoded = nip19.decode(pubkey); + const decoded = nip19.decode(pubkey) as unknown as { type: string; data: string }; if (decoded.type === 'npub') { return decoded.data; } @@ -832,32 +846,29 @@ getKind24RelaySet(senderPubkey, recipientPubkey) ); - Promise.all(relaySetPromises).then(relaySets => { - console.log("[Relay Effect] Received relay sets:", relaySets); - // Combine and deduplicate all relay sets - const allRelays = relaySets.flat(); - const uniqueRelays = [...new Set(allRelays)]; - console.log("[Relay Effect] Final relay list:", uniqueRelays); - - // If no relays found from NIP-65, use fallback relays - if (uniqueRelays.length === 0) { - console.log("[Relay Effect] No NIP-65 relays found, using fallback"); - const fallbackRelays = getAvailableRelays(); - newMessageRelays = fallbackRelays.slice(0, 5); // Limit to first 5 for performance - } else { - newMessageRelays = uniqueRelays; - } - }).catch(error => { - console.error("[Relay Effect] Error getting relay set:", error); - console.log("[Relay Effect] Using fallback relays due to error"); + const relaySets = await Promise.all(relaySetPromises); + console.log("[Relay Effect] Received relay sets:", relaySets); + + // Combine and deduplicate all relay sets + const allRelays = relaySets.flat(); + const uniqueRelays = [...new Set(allRelays)]; + console.log("[Relay Effect] Final relay list:", uniqueRelays); + + // If no relays found from NIP-65, use fallback relays + if (uniqueRelays.length === 0) { + console.log("[Relay Effect] No NIP-65 relays found, using fallback"); const fallbackRelays = getAvailableRelays(); - newMessageRelays = fallbackRelays.slice(0, 5); - }); - } else { - console.log("[Relay Effect] Clearing relays - no recipients or sender"); - newMessageRelays = []; + newMessageRelays = fallbackRelays.slice(0, 5); // Limit to first 5 for performance + } else { + newMessageRelays = uniqueRelays; + } + } catch (error) { + console.error("[Relay Effect] Error getting relay set:", error); + console.log("[Relay Effect] Using fallback relays due to error"); + const fallbackRelays = getAvailableRelays(); + newMessageRelays = fallbackRelays.slice(0, 5); } - }); + } {#if isOwnProfile && $userStore.signedIn} diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index 1ae83af..3aae73f 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -19,17 +19,21 @@ let summary = "Alexandria is a digital library, utilizing Nostr events for curated publications and wiki pages."; - // Reactive effect to log relay configuration when stores change - $effect(() => { + // AI-NOTE: Refactored to avoid blocking $effect with logging operations + // Reactive effect to log relay configuration when stores change - non-blocking approach + $effect.pre(() => { const inboxRelays = $activeInboxRelays; const outboxRelays = $activeOutboxRelays; // Only log if we have relays (not empty arrays) if (inboxRelays.length > 0 || outboxRelays.length > 0) { - console.log('🔌 Relay Configuration Updated:'); - console.log('📥 Inbox Relays:', inboxRelays); - console.log('📤 Outbox Relays:', outboxRelays); - console.log(`📊 Total: ${inboxRelays.length} inbox, ${outboxRelays.length} outbox`); + // Defer logging to avoid blocking the reactive system + requestAnimationFrame(() => { + console.log('🔌 Relay Configuration Updated:'); + console.log('📥 Inbox Relays:', inboxRelays); + console.log('📤 Outbox Relays:', outboxRelays); + console.log(`📊 Total: ${inboxRelays.length} inbox, ${outboxRelays.length} outbox`); + }); } }); diff --git a/src/routes/events/+page.svelte b/src/routes/events/+page.svelte index f37d5a8..fc86dc5 100644 --- a/src/routes/events/+page.svelte +++ b/src/routes/events/+page.svelte @@ -392,17 +392,21 @@ import CommentViewer from "$lib/components/CommentViewer.svelte"; - // Reactive effect to log relay configuration when stores change - $effect(() => { + // AI-NOTE: Refactored to avoid blocking $effect with logging operations + // Reactive effect to log relay configuration when stores change - non-blocking approach + $effect.pre(() => { const inboxRelays = $activeInboxRelays; const outboxRelays = $activeOutboxRelays; // Only log if we have relays (not empty arrays) if (inboxRelays.length > 0 || outboxRelays.length > 0) { - console.log('🔌 Events Page - Relay Configuration Updated:'); - console.log('📥 Inbox Relays:', inboxRelays); - console.log('📤 Outbox Relays:', outboxRelays); - console.log(`📊 Total: ${inboxRelays.length} inbox, ${outboxRelays.length} outbox`); + // Defer logging to avoid blocking the reactive system + requestAnimationFrame(() => { + console.log('🔌 Events Page - Relay Configuration Updated:'); + console.log('📥 Inbox Relays:', inboxRelays); + console.log('📤 Outbox Relays:', outboxRelays); + console.log(`📊 Total: ${inboxRelays.length} inbox, ${outboxRelays.length} outbox`); + }); } }); From 176e710dff435df3b56b42e97878ce44a307e7d5 Mon Sep 17 00:00:00 2001 From: silberengel Date: Sun, 10 Aug 2025 15:49:56 +0200 Subject: [PATCH 25/27] simplified quote structure and corrected msg jump --- src/lib/components/Notifications.svelte | 139 ++++++++++++++++-------- src/lib/utils/kind24_utils.ts | 14 +-- 2 files changed, 98 insertions(+), 55 deletions(-) diff --git a/src/lib/components/Notifications.svelte b/src/lib/components/Notifications.svelte index f46607a..e915315 100644 --- a/src/lib/components/Notifications.svelte +++ b/src/lib/components/Notifications.svelte @@ -19,6 +19,7 @@ import { searchProfiles } from "$lib/utils/search_utility"; import type { NostrProfile } from "$lib/utils/search_types"; import { PlusOutline, ReplyOutline } from "flowbite-svelte-icons"; + import { parseBasicmarkup } from "$lib/utils/markup/basicMarkupParser"; const { event } = $props<{ event: NDKEvent }>(); @@ -182,31 +183,46 @@ } } - function renderContentWithLinks(content: string): string { - // Parse markdown links [text](url) and convert to HTML - let rendered = content.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '$1'); + async function parseContent(content: string): Promise { + if (!content) return ""; - // Handle quote format and convert to small gray bars like Jumble - const patterns = [ - /> QUOTED: ([^•]*?) • LINK:\s*\n((?:nostr:)?nevent[^\s]*)/g, - /> QUOTED: ([^\n]*?)\n> LINK: ((?:nostr:)?nevent[^\s]*)/g, - /> QUOTED: ([^•]*?) • LINK:\s*((?:nostr:)?nevent[^\s]*)/g, - /> QUOTED: ([^•]*?) • LINK: ((?:nostr:)?nevent[^\s]*)/g, // Without optional whitespace - ]; + let parsedContent = await parseBasicmarkup(content); - for (const pattern of patterns) { - const beforeReplace = rendered; - rendered = rendered.replace(pattern, (match, quotedText, neventUrl) => { - const encodedUrl = neventUrl.replace(/'/g, '''); - const cleanQuotedText = quotedText.trim(); - return `
${cleanQuotedText}
`; - }); - if (beforeReplace !== rendered) { - break; + return parsedContent; + } + + function renderQuotedContent(message: NDKEvent): string { + const qTags = message.getMatchingTags("q"); + if (qTags.length === 0) return ""; + + const qTag = qTags[0]; + const nevent = qTag[1]; + + // Extract event ID from nevent + let eventId = ''; + try { + const decoded = nip19.decode(nevent); + if (decoded.type === 'nevent' && decoded.data.id) { + eventId = decoded.data.id; + } + } catch (error) { + // If decode fails, try to extract hex ID directly + const hexMatch = nevent.match(/[a-f0-9]{64}/i); + if (hexMatch) { + eventId = hexMatch[0]; + } + } + + if (eventId) { + // Find the quoted message in our public messages + const quotedMessage = publicMessages.find(msg => msg.id === eventId); + if (quotedMessage) { + const quotedContent = quotedMessage.content ? quotedMessage.content.slice(0, 200) : "No content"; + return `
${quotedContent}
`; } } - return rendered; + return ""; } function getNotificationType(event: NDKEvent): string { @@ -226,35 +242,59 @@ goto(`/events?id=${nevent}`); } - function jumpToMessageInFeed(nevent: string) { + function jumpToMessageInFeed(eventIdOrNevent: string) { // Switch to public messages tab and scroll to the specific message notificationMode = "public-messages"; // Try to find and scroll to the specific message setTimeout(() => { - try { - // Decode the nevent to get the event ID - const decoded = nip19.decode(nevent); - if (decoded.type === 'nevent' && decoded.data.id) { - const eventId = decoded.data.id; + let eventId = eventIdOrNevent; + + // If it's a nevent URL, try to extract the event ID + if (eventIdOrNevent.startsWith('nostr:nevent') || eventIdOrNevent.startsWith('nevent')) { + try { + const decoded = nip19.decode(eventIdOrNevent); + if (decoded.type === 'nevent' && decoded.data.id) { + eventId = decoded.data.id; + } + } catch (error) { + // If decode fails, try to extract hex ID directly + const hexMatch = eventIdOrNevent.match(/[a-f0-9]{64}/i); + if (hexMatch) { + eventId = hexMatch[0]; + } else { + console.warn('Failed to extract event ID from nevent:', eventIdOrNevent); + return; + } + } + } + + // Find the message in our public messages + const targetMessage = publicMessages.find(msg => msg.id === eventId); + if (targetMessage) { + // Try to find the element in the DOM + const element = document.querySelector(`[data-event-id="${eventId}"]`); + if (element) { + // Check if element is in viewport + const rect = element.getBoundingClientRect(); + const isInView = ( + rect.top >= 0 && + rect.left >= 0 && + rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) && + rect.right <= (window.innerWidth || document.documentElement.clientWidth) + ); - // Find the message in our public messages - const targetMessage = publicMessages.find(msg => msg.id === eventId); - if (targetMessage) { - // Try to scroll to the element if it exists in the DOM - const element = document.querySelector(`[data-event-id="${eventId}"]`); - if (element) { - element.scrollIntoView({ behavior: 'smooth', block: 'center' }); - // Briefly highlight the message - element.classList.add('ring-2', 'ring-blue-500'); - setTimeout(() => { - element.classList.remove('ring-2', 'ring-blue-500'); - }, 2000); - } + // Only scroll if not in view + if (!isInView) { + element.scrollIntoView({ behavior: 'smooth', block: 'center' }); } + + // ALWAYS highlight the message in blue + element.classList.add('ring-2', 'ring-blue-500'); + setTimeout(() => { + element.classList.remove('ring-2', 'ring-blue-500'); + }, 2000); } - } catch (error) { - console.warn('Failed to jump to message:', error); } }, 100); } @@ -1020,9 +1060,18 @@ {/if}
+ {#if message.getMatchingTags("q").length > 0} +
+ {@html renderQuotedContent(message)} +
+ {/if} {#if message.content}
- {@html truncateRenderedContent(renderContentWithLinks(message.content), 300)} + {#await parseContent(message.content) then parsedContent} + {@html parsedContent} + {:catch} + {@html message.content} + {/await}
{/if} @@ -1136,7 +1185,11 @@
Replying to:
- {@html renderContentWithLinks(quotedContent)} + {#await parseContent(quotedContent) then parsedContent} + {@html parsedContent} + {:catch} + {@html quotedContent} + {/await}
{/if} diff --git a/src/lib/utils/kind24_utils.ts b/src/lib/utils/kind24_utils.ts index 9d1271e..cb43e57 100644 --- a/src/lib/utils/kind24_utils.ts +++ b/src/lib/utils/kind24_utils.ts @@ -151,18 +151,8 @@ export async function createKind24Reply( return { success: false, error: "No relays available for publishing" }; } - // Build content with quoted message if replying - let finalContent = content; - if (originalEvent) { - // Use multiple relays for better discoverability - const nevent = nip19.neventEncode({ - id: originalEvent.id, - relays: prioritizedRelays.slice(0, 3) // Use first 3 relays - }); - const quotedContent = originalEvent.content ? originalEvent.content.slice(0, 200) : "No content"; - // Use a more visible quote format with a clickable link - finalContent = `> QUOTED: ${quotedContent}\n> LINK: ${nevent}\n\n${content}`; - } + // Use the content as-is, quoted content is handled via q tag + const finalContent = content; // Build tags for the kind 24 event const tags: string[][] = [ From 22235235b31cd97b322ba89e1cfe5e9f42877c54 Mon Sep 17 00:00:00 2001 From: silberengel Date: Sun, 10 Aug 2025 16:08:48 +0200 Subject: [PATCH 26/27] got rid of hard-coded relays --- src/lib/components/EventInput.svelte | 5 +-- src/lib/components/Notifications.svelte | 56 +++++++------------------ src/lib/consts.ts | 2 + src/lib/utils/kind24_utils.ts | 7 +--- src/routes/contact/+page.svelte | 10 ++--- 5 files changed, 24 insertions(+), 56 deletions(-) diff --git a/src/lib/components/EventInput.svelte b/src/lib/components/EventInput.svelte index 0519692..cec2cde 100644 --- a/src/lib/components/EventInput.svelte +++ b/src/lib/components/EventInput.svelte @@ -29,6 +29,7 @@ import { Button } from "flowbite-svelte"; import { goto } from "$app/navigation"; import { WebSocketPool } from "$lib/data_structures/websocket_pool"; + import { anonymousRelays } from "$lib/consts"; let kind = $state(30040); let tags = $state<[string, string][]>([]); @@ -384,9 +385,7 @@ // Try to publish to relays directly const relays = [ - "wss://relay.damus.io", - "wss://relay.nostr.band", - "wss://nos.lol", + ...anonymousRelays, ...$activeOutboxRelays, ...$activeInboxRelays, ]; diff --git a/src/lib/components/Notifications.svelte b/src/lib/components/Notifications.svelte index e915315..4a98bc3 100644 --- a/src/lib/components/Notifications.svelte +++ b/src/lib/components/Notifications.svelte @@ -10,7 +10,7 @@ import { goto } from "$app/navigation"; import { get } from "svelte/store"; import { nip19 } from "nostr-tools"; - import { communityRelays, localRelays } from "$lib/consts"; + import { communityRelays, localRelays, anonymousRelays, searchRelays } from "$lib/consts"; import { createKind24Reply, getKind24RelaySet } from "$lib/utils/kind24_utils"; import { createSignedEvent } from "$lib/utils/nostrEventService"; import RelayDisplay from "$lib/components/RelayDisplay.svelte"; @@ -191,38 +191,24 @@ return parsedContent; } - function renderQuotedContent(message: NDKEvent): string { + async function renderQuotedContent(message: NDKEvent): Promise { const qTags = message.getMatchingTags("q"); if (qTags.length === 0) return ""; const qTag = qTags[0]; - const nevent = qTag[1]; - - // Extract event ID from nevent - let eventId = ''; - try { - const decoded = nip19.decode(nevent); - if (decoded.type === 'nevent' && decoded.data.id) { - eventId = decoded.data.id; - } - } catch (error) { - // If decode fails, try to extract hex ID directly - const hexMatch = nevent.match(/[a-f0-9]{64}/i); - if (hexMatch) { - eventId = hexMatch[0]; - } - } + const eventId = qTag[1]; if (eventId) { // Find the quoted message in our public messages const quotedMessage = publicMessages.find(msg => msg.id === eventId); if (quotedMessage) { const quotedContent = quotedMessage.content ? quotedMessage.content.slice(0, 200) : "No content"; - return `
${quotedContent}
`; + const parsedContent = await parseBasicmarkup(quotedContent); + return `
${parsedContent}
`; } } - return ""; + return ""; } function getNotificationType(event: NDKEvent): string { @@ -520,9 +506,7 @@ // Add q tag if replying to a message (for jump-to functionality) if (replyToMessage) { - // Get the first relay from newMessageRelays or use a fallback - const relayUrl = newMessageRelays[0] || "wss://freelay.sovbit.host/"; - pTags.push(["q", replyToMessage.id, relayUrl, replyToMessage.pubkey]); + pTags.push(["q", replyToMessage.id, newMessageRelays[0] || anonymousRelays[0]]); } // Get all recipient pubkeys for relay calculation (ensure hex format) @@ -559,14 +543,8 @@ const uniqueRelays = [...new Set(allRelays)]; newMessageRelays = uniqueRelays; - // Create the kind 24 event with quoted content if replying - let finalContent = newMessageContent; - if (replyToMessage && quotedContent) { - // Generate the markdown quote format for the actual message - const neventUrl = getNeventUrl(replyToMessage); - const markdownQuote = `> QUOTED: ${quotedContent} • LINK: ${neventUrl}`; - finalContent = markdownQuote + "\n\n" + newMessageContent; - } + // Use the content as-is, quoted content is handled via q tag + const finalContent = newMessageContent; // Create and sign the event using the unified function (includes expiration tag) const { event: signedEvent } = await createSignedEvent( @@ -649,16 +627,6 @@ } // Try search relays - const searchRelays = [ - "wss://profiles.nostr1.com", - "wss://aggr.nostr.land", - "wss://relay.noswhere.com", - "wss://nostr.wine", - "wss://relay.damus.io", - "wss://relay.nostr.band", - "wss://freelay.sovbit.host" - ]; - for (const relay of searchRelays) { try { const ndk = get(ndkInstance); @@ -1062,7 +1030,11 @@ {#if message.getMatchingTags("q").length > 0}
- {@html renderQuotedContent(message)} + {#await renderQuotedContent(message) then quotedHtml} + {@html quotedHtml} + {:catch} + + {/await}
{/if} {#if message.content} diff --git a/src/lib/consts.ts b/src/lib/consts.ts index f141e7b..b8e7f0d 100644 --- a/src/lib/consts.ts +++ b/src/lib/consts.ts @@ -33,6 +33,8 @@ export const secondaryRelays = [ export const anonymousRelays = [ "wss://freelay.sovbit.host", "wss://thecitadel.nostr1.com", + "wss://relay.damus.io", + "wss://relay.nostr.band" ]; export const lowbandwidthRelays = [ diff --git a/src/lib/utils/kind24_utils.ts b/src/lib/utils/kind24_utils.ts index cb43e57..69c59ed 100644 --- a/src/lib/utils/kind24_utils.ts +++ b/src/lib/utils/kind24_utils.ts @@ -5,6 +5,7 @@ import { NDKEvent, NDKRelaySet, NDKUser } from "@nostr-dev-kit/ndk"; import type NDK from "@nostr-dev-kit/ndk"; import { nip19 } from "nostr-tools"; import { createSignedEvent } from "./nostrEventService.ts"; +import { anonymousRelays } from "../consts"; /** * Fetches user's outbox relays from NIP-65 relay list @@ -161,11 +162,7 @@ export async function createKind24Reply( // Add q tag if replying to an original event if (originalEvent) { - const nevent = nip19.neventEncode({ - id: originalEvent.id, - relays: prioritizedRelays.slice(0, 3) // Use first 3 relays - }); - tags.push(["q", nevent, prioritizedRelays[0]]); + tags.push(["q", originalEvent.id, prioritizedRelays[0] || anonymousRelays[0]]); } // Create and sign the event using the unified function (includes expiration tag) diff --git a/src/routes/contact/+page.svelte b/src/routes/contact/+page.svelte index 4137220..b520ddc 100644 --- a/src/routes/contact/+page.svelte +++ b/src/routes/contact/+page.svelte @@ -11,7 +11,7 @@ } from "flowbite-svelte"; import { ndkInstance, ndkSignedIn, activeInboxRelays, activeOutboxRelays } from "$lib/ndk"; import { userStore } from "$lib/stores/userStore"; - import { communityRelays } from "$lib/consts"; + import { communityRelays, anonymousRelays } from "$lib/consts"; import type NDK from "@nostr-dev-kit/ndk"; import { NDKEvent, NDKRelaySet } from "@nostr-dev-kit/ndk"; // @ts-ignore - Workaround for Svelte component import issue @@ -62,13 +62,11 @@ const repoAddress = "naddr1qvzqqqrhnypzplfq3m5v3u5r0q9f255fdeyz8nyac6lagssx8zy4wugxjs8ajf7pqy88wumn8ghj7mn0wvhxcmmv9uqq5stvv4uxzmnywf5kz2elajr"; - // Use the new relay management system instead of hardcoded relays + // Use the new relay management system with anonymous relays as fallbacks const allRelays = [ - "wss://relay.damus.io", - "wss://relay.nostr.band", - "wss://nos.lol", ...$activeInboxRelays, ...$activeOutboxRelays, + ...anonymousRelays, ]; // Hard-coded repository owner pubkey and ID from the task @@ -213,7 +211,7 @@ ...(ndk.pool ? Array.from(ndk.pool.relays.values()) .filter( - (relay) => relay.url && !relay.url.includes("wss://nos.lol"), + (relay) => relay.url, ) .map((relay) => normalizeRelayUrl(relay.url)) : []), From 8479a8c7dceecae02e0b00536258ad1801a4455b Mon Sep 17 00:00:00 2001 From: silberengel Date: Sun, 10 Aug 2025 17:39:41 +0200 Subject: [PATCH 27/27] Redid the formatting. --- src/lib/components/Notifications.svelte | 397 ++++++------------------ src/lib/utils/kind24_utils.ts | 240 ++++---------- src/lib/utils/notification_utils.ts | 225 ++++++++++++++ 3 files changed, 386 insertions(+), 476 deletions(-) create mode 100644 src/lib/utils/notification_utils.ts diff --git a/src/lib/components/Notifications.svelte b/src/lib/components/Notifications.svelte index 4a98bc3..f0995c7 100644 --- a/src/lib/components/Notifications.svelte +++ b/src/lib/components/Notifications.svelte @@ -5,8 +5,6 @@ import { userStore } from "$lib/stores/userStore"; import { userPubkey, isLoggedIn } from "$lib/stores/authStore.Svelte"; import { ndkInstance, activeInboxRelays } from "$lib/ndk"; - import { neventEncode } from "$lib/utils"; - import { getUserMetadata, NDKRelaySetFromNDK } from "$lib/utils/nostrUtils"; import { goto } from "$app/navigation"; import { get } from "svelte/store"; import { nip19 } from "nostr-tools"; @@ -19,7 +17,19 @@ import { searchProfiles } from "$lib/utils/search_utility"; import type { NostrProfile } from "$lib/utils/search_types"; import { PlusOutline, ReplyOutline } from "flowbite-svelte-icons"; + import { + truncateContent, + truncateRenderedContent, + parseContent, + renderQuotedContent, + getNotificationType, + fetchAuthorProfiles + } from "$lib/utils/notification_utils"; + import { buildCompleteRelaySet } from "$lib/utils/relay_management"; + import { formatDate, neventEncode } from "$lib/utils"; + import { toNpub, getUserMetadata, NDKRelaySetFromNDK } from "$lib/utils/nostrUtils"; import { parseBasicmarkup } from "$lib/utils/markup/basicMarkupParser"; + import { userBadge } from "$lib/snippets/UserSnippets.svelte"; const { event } = $props<{ event: NDKEvent }>(); @@ -79,148 +89,9 @@ }); // AI-NOTE: Utility functions extracted to reduce code duplication - function getAvailableRelays(): string[] { - const userInboxRelays = $userStore.relays.inbox || []; - const userOutboxRelays = $userStore.relays.outbox || []; - const activeInboxRelayList = get(activeInboxRelays); - - const allRelays = [ - ...userInboxRelays, - ...userOutboxRelays, - ...localRelays, - ...communityRelays, - ...activeInboxRelayList - ]; - - return [...new Set(allRelays)]; - } - - function toNpub(pubkey: string): string | null { - if (!pubkey) return null; - try { - if (/^[a-f0-9]{64}$/i.test(pubkey)) { - return nip19.npubEncode(pubkey); - } - if (pubkey.startsWith("npub1")) return pubkey; - return null; - } catch { - return null; - } - } - function getNeventUrl(event: NDKEvent): string { - const relays = getAvailableRelays(); - return neventEncode(event, relays); - } - - function formatDate(timestamp: number): string { - const date = new Date(timestamp * 1000); - const now = new Date(); - const diffMs = now.getTime() - date.getTime(); - const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24)); - - if (diffDays === 0) return "Today"; - if (diffDays === 1) return "Yesterday"; - if (diffDays < 7) return `${diffDays} days ago`; - return date.toLocaleDateString(); - } - - function truncateContent(content: string, maxLength: number = 300): string { - if (content.length <= maxLength) return content; - return content.slice(0, maxLength) + "..."; - } - - function truncateRenderedContent(renderedHtml: string, maxLength: number = 300): string { - // If the rendered HTML is short enough, return as-is - if (renderedHtml.length <= maxLength) return renderedHtml; - - // Check if there are any gray quote boxes (jump-to-message divs) - const hasQuoteBoxes = renderedHtml.includes('jump-to-message'); - - if (hasQuoteBoxes) { - // Split content into quote boxes and regular text - const quoteBoxPattern = /
]*>[^<]*<\/div>/g; - const quoteBoxes = renderedHtml.match(quoteBoxPattern) || []; - - // Remove quote boxes temporarily to measure text length - let textOnly = renderedHtml.replace(quoteBoxPattern, '|||QUOTEBOX|||'); - - // If text without quote boxes is still too long, truncate it - if (textOnly.length > maxLength) { - const availableLength = maxLength - (quoteBoxes.join('').length); - if (availableLength > 50) { // Leave some reasonable space for text - textOnly = textOnly.slice(0, availableLength) + "..."; - } else { - // If quote boxes take up too much space, just show them with minimal text - textOnly = textOnly.slice(0, 50) + "..."; - } - } - - // Restore quote boxes - let result = textOnly; - quoteBoxes.forEach(box => { - result = result.replace('|||QUOTEBOX|||', box); - }); - - return result; - } else { - // No quote boxes, simple truncation with HTML awareness - if (renderedHtml.includes('<')) { - // Has HTML tags, do a simple truncation but try to avoid breaking tags - const truncated = renderedHtml.slice(0, maxLength); - const lastTagStart = truncated.lastIndexOf('<'); - const lastTagEnd = truncated.lastIndexOf('>'); - - if (lastTagStart > lastTagEnd) { - // We're in the middle of a tag, truncate before it - return renderedHtml.slice(0, lastTagStart) + "..."; - } - return truncated + "..."; - } else { - // Plain text, simple truncation - return renderedHtml.slice(0, maxLength) + "..."; - } - } - } - - async function parseContent(content: string): Promise { - if (!content) return ""; - - let parsedContent = await parseBasicmarkup(content); - - return parsedContent; - } - - async function renderQuotedContent(message: NDKEvent): Promise { - const qTags = message.getMatchingTags("q"); - if (qTags.length === 0) return ""; - - const qTag = qTags[0]; - const eventId = qTag[1]; - - if (eventId) { - // Find the quoted message in our public messages - const quotedMessage = publicMessages.find(msg => msg.id === eventId); - if (quotedMessage) { - const quotedContent = quotedMessage.content ? quotedMessage.content.slice(0, 200) : "No content"; - const parsedContent = await parseBasicmarkup(quotedContent); - return `
${parsedContent}
`; - } - } - - return ""; - } - - function getNotificationType(event: NDKEvent): string { - switch (event.kind) { - case 1: return "Reply"; - case 1111: return "Custom Reply"; - case 9802: return "Highlight"; - case 6: return "Repost"; - case 16: return "Generic Repost"; - case 24: return "Public Message"; - default: return `Kind ${event.kind}`; - } + // Use empty relay list for nevent encoding - relays will be discovered by the events page + return neventEncode(event, []); } function navigateToEvent(nevent: string) { @@ -605,88 +476,6 @@ } } - - - // AI-NOTE: Simplified profile fetching with better error handling - async function fetchAuthorProfiles(events: NDKEvent[]) { - const uniquePubkeys = new Set(); - events.forEach(event => { - if (event.pubkey) uniquePubkeys.add(event.pubkey); - }); - - const profilePromises = Array.from(uniquePubkeys).map(async (pubkey) => { - try { - const npub = toNpub(pubkey); - if (!npub) return; - - // Try cache first - let profile = await getUserMetadata(npub, false); - if (profile && (profile.name || profile.displayName || profile.picture)) { - authorProfiles.set(pubkey, profile); - return; - } - - // Try search relays - for (const relay of searchRelays) { - try { - const ndk = get(ndkInstance); - if (!ndk) break; - - const relaySet = NDKRelaySetFromNDK.fromRelayUrls([relay], ndk); - const profileEvent = await ndk.fetchEvent( - { kinds: [0], authors: [pubkey] }, - undefined, - relaySet - ); - - if (profileEvent) { - const profileData = JSON.parse(profileEvent.content); - authorProfiles.set(pubkey, { - name: profileData.name, - displayName: profileData.display_name || profileData.displayName, - picture: profileData.picture || profileData.image - }); - return; - } - } catch (error) { - console.warn(`[Notifications] Failed to fetch profile from ${relay}:`, error); - } - } - - // Try all available relays as fallback - const relays = getAvailableRelays(); - if (relays.length > 0) { - try { - const ndk = get(ndkInstance); - if (!ndk) return; - - const relaySet = NDKRelaySetFromNDK.fromRelayUrls(relays, ndk); - const profileEvent = await ndk.fetchEvent( - { kinds: [0], authors: [pubkey] }, - undefined, - relaySet - ); - - if (profileEvent) { - const profileData = JSON.parse(profileEvent.content); - authorProfiles.set(pubkey, { - name: profileData.name, - displayName: profileData.display_name || profileData.displayName, - picture: profileData.picture || profileData.image - }); - } - } catch (error) { - console.warn(`[Notifications] Failed to fetch profile from all relays:`, error); - } - } - } catch (error) { - console.warn(`[Notifications] Failed to fetch profile for ${pubkey}:`, error); - } - }); - - await Promise.allSettled(profilePromises); - } - // AI-NOTE: Simplified notification fetching async function fetchNotifications() { if (!$userStore.pubkey || !isOwnProfile) return; @@ -697,8 +486,11 @@ try { const ndk = get(ndkInstance); if (!ndk) throw new Error("No NDK instance available"); - - const relays = getAvailableRelays(); + + const userStoreValue = get(userStore); + const user = userStoreValue.signedIn && userStoreValue.pubkey ? ndk.getUser({ pubkey: userStoreValue.pubkey }) : null; + const relaySet = await buildCompleteRelaySet(ndk, user); + const relays = [...relaySet.inboxRelays, ...relaySet.outboxRelays]; if (relays.length === 0) throw new Error("No relays available"); const filter = { @@ -710,8 +502,8 @@ limit: 100, }; - const relaySet = NDKRelaySetFromNDK.fromRelayUrls(relays, ndk); - const events = await ndk.fetchEvents(filter, undefined, relaySet); + const ndkRelaySet = NDKRelaySetFromNDK.fromRelayUrls(relays, ndk); + const events = await ndk.fetchEvents(filter, undefined, ndkRelaySet); const eventArray = Array.from(events); // Filter out self-referential events @@ -729,7 +521,7 @@ .sort((a, b) => (b.created_at || 0) - (a.created_at || 0)) .slice(0, 100); - await fetchAuthorProfiles(notifications); + authorProfiles = await fetchAuthorProfiles(notifications); } catch (err) { console.error("[Notifications] Error fetching notifications:", err); error = err instanceof Error ? err.message : "Failed to fetch notifications"; @@ -749,15 +541,18 @@ const ndk = get(ndkInstance); if (!ndk) throw new Error("No NDK instance available"); - const relays = getAvailableRelays(); + const userStoreValue = get(userStore); + const user = userStoreValue.signedIn && userStoreValue.pubkey ? ndk.getUser({ pubkey: userStoreValue.pubkey }) : null; + const relaySet = await buildCompleteRelaySet(ndk, user); + const relays = [...relaySet.inboxRelays, ...relaySet.outboxRelays]; if (relays.length === 0) throw new Error("No relays available"); - const relaySet = NDKRelaySetFromNDK.fromRelayUrls(relays, ndk); + const ndkRelaySet = NDKRelaySetFromNDK.fromRelayUrls(relays, ndk); // Fetch only kind 24 messages const [messagesEvents, userMessagesEvents] = await Promise.all([ - ndk.fetchEvents({ kinds: [24 as any], "#p": [$userStore.pubkey], limit: 200 }, undefined, relaySet), - ndk.fetchEvents({ kinds: [24 as any], authors: [$userStore.pubkey], limit: 200 }, undefined, relaySet) + ndk.fetchEvents({ kinds: [24 as any], "#p": [$userStore.pubkey], limit: 200 }, undefined, ndkRelaySet), + ndk.fetchEvents({ kinds: [24 as any], authors: [$userStore.pubkey], limit: 200 }, undefined, ndkRelaySet) ]); const allMessages = [ @@ -774,7 +569,7 @@ .sort((a, b) => (b.created_at || 0) - (a.created_at || 0)) .slice(0, 200); - await fetchAuthorProfiles(publicMessages); + authorProfiles = await fetchAuthorProfiles(publicMessages); } catch (err) { console.error("[PublicMessages] Error fetching public messages:", err); error = err instanceof Error ? err.message : "Failed to fetch public messages"; @@ -865,16 +660,32 @@ // If no relays found from NIP-65, use fallback relays if (uniqueRelays.length === 0) { console.log("[Relay Effect] No NIP-65 relays found, using fallback"); - const fallbackRelays = getAvailableRelays(); - newMessageRelays = fallbackRelays.slice(0, 5); // Limit to first 5 for performance + const ndk = get(ndkInstance); + if (ndk) { + const userStoreValue = get(userStore); + const user = userStoreValue.signedIn && userStoreValue.pubkey ? ndk.getUser({ pubkey: userStoreValue.pubkey }) : null; + const relaySet = await buildCompleteRelaySet(ndk, user); + const fallbackRelays = [...relaySet.inboxRelays, ...relaySet.outboxRelays]; + newMessageRelays = fallbackRelays.slice(0, 5); // Limit to first 5 for performance + } else { + newMessageRelays = []; + } } else { newMessageRelays = uniqueRelays; } } catch (error) { console.error("[Relay Effect] Error getting relay set:", error); console.log("[Relay Effect] Using fallback relays due to error"); - const fallbackRelays = getAvailableRelays(); - newMessageRelays = fallbackRelays.slice(0, 5); + const ndk = get(ndkInstance); + if (ndk) { + const userStoreValue = get(userStore); + const user = userStoreValue.signedIn && userStoreValue.pubkey ? ndk.getUser({ pubkey: userStoreValue.pubkey }) : null; + const relaySet = await buildCompleteRelaySet(ndk, user); + const fallbackRelays = [...relaySet.inboxRelays, ...relaySet.outboxRelays]; + newMessageRelays = fallbackRelays.slice(0, 5); + } else { + newMessageRelays = []; + } } } @@ -933,7 +744,7 @@
- Filtered by user: {authorProfiles.get(filteredByUser)?.displayName || authorProfiles.get(filteredByUser)?.name || `${filteredByUser.slice(0, 8)}...${filteredByUser.slice(-4)}`} + Filtered by user: {@render userBadge(filteredByUser, authorProfiles.get(filteredByUser)?.displayName || authorProfiles.get(filteredByUser)?.name)}
- -
- - {authorProfile?.displayName || authorProfile?.name || `${message.pubkey.slice(0, 8)}...${message.pubkey.slice(-4)}`} - - {#if authorProfile?.name && authorProfile?.displayName && authorProfile.name !== authorProfile.displayName} - - (@{authorProfile.name}) - - {/if} -
+ {#if message.getMatchingTags("q").length > 0}
- {#await renderQuotedContent(message) then quotedHtml} + {#await renderQuotedContent(message, publicMessages) then quotedHtml} {@html quotedHtml} {:catch} @@ -1073,22 +879,27 @@ {@const authorProfile = authorProfiles.get(notification.pubkey)}
- +
- {#if authorProfile?.picture} - Author avatar (e.target as HTMLImageElement).style.display = 'none'} - /> - {:else} -
- - {(authorProfile?.displayName || authorProfile?.name || notification.pubkey.slice(0, 1)).toUpperCase()} - -
- {/if} +
+ {#if authorProfile?.picture} + Author avatar (e.target as HTMLImageElement).style.display = 'none'} + /> + {:else} +
+ + {(authorProfile?.displayName || authorProfile?.name || notification.pubkey.slice(0, 1)).toUpperCase()} + +
+ {/if} + + {@render userBadge(notification.pubkey, authorProfile?.displayName || authorProfile?.name)} + +
@@ -1109,21 +920,15 @@
- -
- - {authorProfile?.displayName || authorProfile?.name || `${notification.pubkey.slice(0, 8)}...${notification.pubkey.slice(-4)}`} - - {#if authorProfile?.name && authorProfile?.displayName && authorProfile.name !== authorProfile.displayName} - - (@{authorProfile.name}) - - {/if} -
+ {#if notification.content}
- {truncateContent(notification.content)} + {#await parseContent(notification.content) then parsedContent} + {@html parsedContent} + {:catch} + {@html truncateContent(notification.content)} + {/await}
{/if} @@ -1194,7 +999,7 @@
{#each selectedRecipients as recipient} - {recipient.displayName || recipient.name || `${recipient.pubkey?.slice(0, 8)}...`} + {@render userBadge(recipient.pubkey!, recipient.displayName || recipient.name)}