diff --git a/src/lib/components/CommentBox.svelte b/src/lib/components/CommentBox.svelte index ef8bb61..f4b9a86 100644 --- a/src/lib/components/CommentBox.svelte +++ b/src/lib/components/CommentBox.svelte @@ -5,8 +5,20 @@ import { getUserMetadata, toNpub, - type NostrProfile, } from "$lib/utils/nostrUtils"; + + // Extend NostrProfile locally to include pubkey for mention search results + type NostrProfile = { + name?: string; + displayName?: string; + nip05?: string; + picture?: string; + about?: string; + banner?: string; + website?: string; + lud16?: string; + pubkey?: string; + }; import { activePubkey } from '$lib/ndk'; import type { NDKEvent } from "$lib/utils/nostrUtils"; import { @@ -17,6 +29,12 @@ publishEvent, navigateToEvent, } from "$lib/utils/nostrEventService"; + import { get } from 'svelte/store'; + import { ndkInstance } from '$lib/ndk'; + import type NDK from '@nostr-dev-kit/ndk'; + import { NDKRelaySet } from '@nostr-dev-kit/ndk'; + import { NDKRelay } from '@nostr-dev-kit/ndk'; + import { communityRelay } from '$lib/consts'; const props = $props<{ event: NDKEvent; @@ -32,6 +50,75 @@ let showFallbackRelays = $state(false); let userProfile = $state(null); + // Add state for modals and search + let showMentionModal = $state(false); + let showWikilinkModal = $state(false); + let mentionSearch = $state(''); + let mentionResults = $state([]); + let mentionLoading = $state(false); + let wikilinkTarget = $state(''); + let wikilinkLabel = $state(''); + let mentionSearchTimeout: ReturnType | null = null; + let nip05Search = $state(''); + let nip05Results = $state([]); + let nip05Loading = $state(false); + + // Add a cache for pubkeys with kind 1 events on communityRelay + const forestCache: Record = {}; + + async function checkForest(pubkey: string): Promise { + if (forestCache[pubkey] !== undefined) { + return forestCache[pubkey]; + } + // Query the communityRelay for kind 1 events by this pubkey + try { + const relayUrl = communityRelay[0]; + const ws = new WebSocket(relayUrl); + return await new Promise((resolve) => { + ws.onopen = () => { + // NIP-01 filter for kind 1 events by pubkey + ws.send(JSON.stringify([ + 'REQ', 'alexandria-forest', { kinds: [1], authors: [pubkey], limit: 1 } + ])); + }; + ws.onmessage = (event) => { + const data = JSON.parse(event.data); + if (data[0] === 'EVENT' && data[2]?.kind === 1) { + forestCache[pubkey] = true; + ws.close(); + resolve(true); + } else if (data[0] === 'EOSE') { + forestCache[pubkey] = false; + ws.close(); + resolve(false); + } + }; + ws.onerror = () => { + forestCache[pubkey] = false; + ws.close(); + resolve(false); + }; + }); + } catch { + forestCache[pubkey] = false; + return false; + } + } + + // Track which pubkeys have forest status loaded + let forestStatus: Record = $state({}); + + $effect(() => { + // When mentionResults change, check forest status for each + for (const profile of mentionResults) { + if (profile.pubkey && forestStatus[profile.pubkey] === undefined) { + checkForest(profile.pubkey).then((hasForest) => { + forestStatus = { ...forestStatus, [profile.pubkey!]: hasForest }; + }); + } + } + }); + $effect(() => { if (!activePubkey) { userProfile = null; @@ -72,9 +159,11 @@ { label: "Link", action: () => insertMarkup("[", "](url)") }, { label: "Image", action: () => insertMarkup("![", "](url)") }, { label: "Quote", action: () => insertMarkup("> ", "") }, - { label: "List", action: () => insertMarkup("- ", "") }, + { label: "List", action: () => insertMarkup("* ", "") }, { label: "Numbered List", action: () => insertMarkup("1. ", "") }, { label: "Hashtag", action: () => insertMarkup("#", "") }, + { label: '@', action: () => { mentionSearch = ''; mentionResults = []; showMentionModal = true; } }, + { label: 'Wikilink', action: () => { showWikilinkModal = true; } }, ]; function insertMarkup(prefix: string, suffix: string) { @@ -191,6 +280,183 @@ isSubmitting = false; } } + + // Insert at cursor helper + function insertAtCursor(text: string) { + const textarea = document.querySelector('textarea'); + if (!textarea) return; + const start = textarea.selectionStart; + const end = textarea.selectionEnd; + content = content.substring(0, start) + text + content.substring(end); + updatePreview(); + setTimeout(() => { + textarea.focus(); + textarea.selectionStart = textarea.selectionEnd = start + text.length; + }, 0); + } + + // Real Nostr profile search logic + async function searchMentions() { + mentionLoading = true; + mentionResults = []; + const searchTerm = mentionSearch.trim(); + if (!searchTerm) { + mentionLoading = false; + return; + } + // NIP-05 pattern: user@domain + if (/^[a-z0-9._-]+@[a-z0-9.-]+$/i.test(searchTerm)) { + try { + const [name, domain] = searchTerm.split('@'); + const res = await fetch(`https://${domain}/.well-known/nostr.json?name=${name}`); + const data = await res.json(); + const pubkey = data.names?.[name]; + if (pubkey) { + // Fetch kind:0 event for pubkey from theforest first + const ndk: NDK = get(ndkInstance); + if (!ndk) { + mentionLoading = false; + return; + } + // Try theforest relay first + const { communityRelay } = await import('$lib/consts'); + const forestRelays = communityRelay.map(url => ndk.pool.relays.get(url) ?? ndk.pool.getRelay(url)); + let events = await ndk.fetchEvents({ kinds: [0], authors: [pubkey] }, { closeOnEose: true }, new NDKRelaySet(new Set(forestRelays), ndk)); + let eventArr = Array.from(events); + if (eventArr.length === 0) { + // Fallback to all relays + const relaySet = new NDKRelaySet(new Set(Array.from(ndk.pool.relays.values())), ndk); + events = await ndk.fetchEvents({ kinds: [0], authors: [pubkey] }, { closeOnEose: true }, relaySet); + eventArr = Array.from(events); + } + if (eventArr.length > 0) { + try { + const event = eventArr[0]; + const profileData = JSON.parse(event.content); + mentionResults = [{ ...profileData, pubkey }]; + } catch { + mentionResults = []; + } + } else { + mentionResults = []; + } + } else { + mentionResults = []; + } + } catch { + mentionResults = []; + } + mentionLoading = false; + return; + } + // Fallback: search by display name or name + const ndk: NDK = get(ndkInstance); + if (!ndk) { + mentionLoading = false; + return; + } + // Try theforest relay first + const { communityRelay } = await import('$lib/consts'); + const forestRelays = communityRelay.map(url => ndk.pool.relays.get(url) ?? ndk.pool.getRelay(url)); + let foundProfiles: Record = {}; + let relaySet = new NDKRelaySet(new Set(forestRelays), ndk); + let filter = { kinds: [0] }; + let sub = ndk.subscribe(filter, { closeOnEose: true }, relaySet); + sub.on('event', (event: any) => { + try { + if (!event.content) return; + const profileData = JSON.parse(event.content); + const displayName = profileData.display_name || profileData.displayName || ''; + const name = profileData.name || ''; + const searchLower = searchTerm.toLowerCase(); + if ( + displayName.toLowerCase().includes(searchLower) || + name.toLowerCase().includes(searchLower) + ) { + // Deduplicate by pubkey, keep only newest + const pubkey = event.pubkey; + const created_at = event.created_at || 0; + if (!foundProfiles[pubkey] || foundProfiles[pubkey].created_at < created_at) { + foundProfiles[pubkey] = { + profile: { ...profileData, pubkey }, + created_at, + }; + } + } + } catch {} + }); + sub.on('eose', async () => { + const forestResults = Object.values(foundProfiles).map(x => x.profile); + if (forestResults.length > 0) { + mentionResults = forestResults; + mentionLoading = false; + return; + } + // Fallback to all relays + foundProfiles = {}; + const allRelays: NDKRelay[] = Array.from(ndk.pool.relays.values()); + relaySet = new NDKRelaySet(new Set(allRelays), ndk); + sub = ndk.subscribe(filter, { closeOnEose: true }, relaySet); + sub.on('event', (event: any) => { + try { + if (!event.content) return; + const profileData = JSON.parse(event.content); + const displayName = profileData.display_name || profileData.displayName || ''; + const name = profileData.name || ''; + const searchLower = searchTerm.toLowerCase(); + if ( + displayName.toLowerCase().includes(searchLower) || + name.toLowerCase().includes(searchLower) + ) { + // Deduplicate by pubkey, keep only newest + const pubkey = event.pubkey; + const created_at = event.created_at || 0; + if (!foundProfiles[pubkey] || foundProfiles[pubkey].created_at < created_at) { + foundProfiles[pubkey] = { + profile: { ...profileData, pubkey }, + created_at, + }; + } + } + } catch {} + }); + sub.on('eose', () => { + mentionResults = Object.values(foundProfiles).map(x => x.profile); + mentionLoading = false; + }); + }); + } + + function selectMention(profile: NostrProfile) { + // Always insert nostr:npub... for the selected profile + const npub = toNpub(profile.pubkey); + if (profile && npub) { + insertAtCursor(`nostr:${npub}`); + } + showMentionModal = false; + mentionSearch = ''; + mentionResults = []; + } + + function insertWikilink() { + if (!wikilinkTarget.trim()) return; + let markup = ''; + if (wikilinkLabel.trim()) { + markup = `[[${wikilinkTarget}|${wikilinkLabel}]]`; + } else { + markup = `[[${wikilinkTarget}]]`; + } + insertAtCursor(markup); + showWikilinkModal = false; + wikilinkTarget = ''; + wikilinkLabel = ''; + } + + // Add a helper to shorten npub + function shortenNpub(npub: string | undefined) { + if (!npub) return ''; + return npub.slice(0, 8) + '…' + npub.slice(-4); + }
@@ -204,6 +470,80 @@
+ + {#if showMentionModal} +
+
+

Mention User

+ + + {#if mentionLoading} +
Searching...
+ {:else if mentionResults.length > 0} +
    + {#each mentionResults as profile} + + {/each} +
+ {:else} +
No results
+ {/if} +
+ +
+
+
+ {/if} + + + {#if showWikilinkModal} +
+
+

Insert Wikilink

+ + +
+ + +
+
+
+ {/if} +
-
-
- - -
-
- - - {#if dTagError} -
{dTagError}
- {/if} -
-
- -
- {#if loading} - Publishing... {/if} - {#if error} -
{error}
+ {#if kind === 30040} +
+ 30040 - Publication Index: {get30040EventDescription()} +
{/if} - {#if success} -
{success}
-
Relays: {publishedRelays.join(', ')}
- {#if lastPublishedEventId} -
- Event ID: {lastPublishedEventId} - - View your event - +
+
+ +
+ {#each tags as [key, value], i} +
+ updateTag(i, (e.target as HTMLInputElement).value, tags[i][1])} /> + updateTag(i, tags[i][0], (e.target as HTMLInputElement).value)} /> +
- {/if} + {/each} +
+ +
+
+
+
+ + +
+
+ + +
+
+ + + {#if dTagError} +
{dTagError}
+ {/if} +
+
+ +
+ {#if loading} + Publishing... + {/if} + {#if error} +
{error}
+ {/if} + {#if success} +
{success}
+
Relays: {publishedRelays.join(', ')}
+ {#if lastPublishedEventId} +
+ Event ID: {lastPublishedEventId} + + View your event + +
{/if} - -
-{/if} \ No newline at end of file + {/if} + + \ No newline at end of file diff --git a/src/lib/components/EventSearch.svelte b/src/lib/components/EventSearch.svelte index 3ad6e96..d83c6eb 100644 --- a/src/lib/components/EventSearch.svelte +++ b/src/lib/components/EventSearch.svelte @@ -7,7 +7,7 @@ import { goto } from "$app/navigation"; import type { NDKEvent } from "$lib/utils/nostrUtils"; import RelayDisplay from "./RelayDisplay.svelte"; - import { getActiveRelays } from "$lib/ndk"; + import { NDKRelaySet } from "@nostr-dev-kit/ndk"; const { loading, @@ -38,6 +38,8 @@ ); let foundEvent = $state(null); let searching = $state(false); + let activeSub: any = null; + let foundProfiles: NDKEvent[] = []; $effect(() => { if (searchValue) { @@ -47,7 +49,7 @@ $effect(() => { if (dTagValue) { - searchByDTag(dTagValue); + searchBySubscription('d', dTagValue); } }); @@ -55,66 +57,231 @@ foundEvent = event; }); - async function searchByDTag(dTag: string) { + async function searchBySubscription(searchType: 'd' | 't' | 'n', searchTerm: string) { localError = null; searching = true; if (onLoadingChange) { onLoadingChange(true); } - // Convert d-tag to lowercase for consistent searching - const normalizedDTag = dTag.toLowerCase(); + const normalizedSearchTerm = searchTerm.toLowerCase(); + const ndk = $ndkInstance; + if (!ndk) { + localError = 'NDK not initialized'; + searching = false; + if (onLoadingChange) { onLoadingChange(false); } + return; + } - try { - console.log("[Events] Searching for events with d-tag:", normalizedDTag); - const ndk = $ndkInstance; - if (!ndk) { - localError = "NDK not initialized"; - searching = false; - if (onLoadingChange) { onLoadingChange(false); } - return; + // Use all relays from the NDK pool + const relaySet = new NDKRelaySet(new Set(Array.from(ndk.pool.relays.values())), ndk); + let timeoutId: ReturnType | null = null; + let firstOrderEvents: NDKEvent[] = []; + let secondOrderEvents: NDKEvent[] = []; + let tTagEvents: NDKEvent[] = []; + let eventIds = new Set(); + let eventAddresses = new Set(); + let foundProfiles: NDKEvent[] = []; + + // Helper function to clean up subscription and timeout + const cleanup = () => { + if (timeoutId) { + clearTimeout(timeoutId); + timeoutId = null; + } + if (activeSub) { + activeSub.stop(); + activeSub = null; } + searching = false; + if (onLoadingChange) { onLoadingChange(false); } + }; + + // Helper function to check if a profile field matches the search term + const fieldMatches = (field: string) => { + if (!field) return false; + const fieldLower = field.toLowerCase(); + const searchLower = normalizedSearchTerm.toLowerCase(); + if (fieldLower === searchLower) return true; + if (fieldLower.includes(searchLower)) return true; + const words = fieldLower.split(/\s+/); + return words.some(word => word.includes(searchLower)); + }; + + // Set a timeout to force completion after 15 seconds + timeoutId = setTimeout(() => { + console.log(`[Events] ${searchType.toUpperCase()}-tag search timeout reached`); + if (searchType === 'n' && foundProfiles.length === 0) { + localError = `No profiles found matching: ${searchTerm} (search timed out)`; + onSearchResults([], [], [], new Set(), new Set()); + } else if (searchType === 'd' && firstOrderEvents.length === 0) { + localError = `No events found with d-tag: ${searchTerm} (search timed out)`; + onSearchResults([], [], [], new Set(), new Set()); + } else if (searchType === 't' && tTagEvents.length === 0) { + localError = `No events found with t-tag: ${searchTerm} (search timed out)`; + onSearchResults([], [], [], new Set(), new Set()); + } + cleanup(); + }, 15000); + + let filter: any; + let subscriptionType: string; + + switch (searchType) { + case 'd': + filter = { "#d": [normalizedSearchTerm] }; + subscriptionType = 'd-tag'; + break; + case 't': + filter = { "#t": [normalizedSearchTerm] }; + subscriptionType = 't-tag'; + break; + case 'n': + filter = { kinds: [0] }; + subscriptionType = 'profile'; + break; + } - const filter = { "#d": [normalizedDTag] }; - const relaySet = getActiveRelays(ndk); + console.log(`[Events] Starting ${subscriptionType} search for:`, normalizedSearchTerm); - // Fetch multiple events with the same d-tag - const events = await ndk.fetchEvents( - filter, - { closeOnEose: true }, - relaySet, - ); - const eventArray = Array.from(events); + // Subscribe to events + const sub = ndk.subscribe( + filter, + { closeOnEose: true }, + relaySet + ); - if (eventArray.length === 0) { - localError = `No events found with d-tag: ${normalizedDTag}`; - onSearchResults([], [], [], new Set(), new Set()); - searching = false; - if (onLoadingChange) { onLoadingChange(false); } - return; + sub.on('event', (event) => { + try { + if (searchType === 'n') { + // Profile search logic + if (!event.content) return; + const profileData = JSON.parse(event.content); + const displayName = profileData.display_name || profileData.displayName || ''; + const name = profileData.name || ''; + const nip05 = profileData.nip05 || ''; + + if (fieldMatches(displayName) || fieldMatches(name) || fieldMatches(nip05.split('@')[0])) { + foundProfiles = [...foundProfiles, event]; + onSearchResults(foundProfiles, [], [], new Set(foundProfiles.map(p => p.id)), new Set()); + } + } else { + // d-tag and t-tag search logic + if (event.kind === 7) return; // Skip emoji reactions + + if (searchType === 'd') { + firstOrderEvents = [...firstOrderEvents, event]; + + // Collect event IDs and addresses for second-order search + if (event.id) { + eventIds.add(event.id); + } + const aTags = getMatchingTags(event, "a"); + aTags.forEach((tag: string[]) => { + if (tag[1]) { + eventAddresses.add(tag[1]); + } + }); + } else if (searchType === 't') { + tTagEvents = [...tTagEvents, event]; + } + } + } catch (e) { + // Invalid JSON or other error, skip } + }); - // Collect all event IDs and addresses for second-order search - const eventIds = new Set(); - const eventAddresses = new Set(); + sub.on('eose', () => { + console.log(`[Events] ${subscriptionType} search EOSE received`); - eventArray.forEach(event => { - if (event.id) { - eventIds.add(event.id); + if (searchType === 'n') { + if (foundProfiles.length === 0) { + localError = `No profiles found matching: ${searchTerm}`; + onSearchResults([], [], [], new Set(), new Set()); + } else { + // Deduplicate by pubkey, keep only newest + const deduped: Record = {}; + for (const event of foundProfiles) { + const pubkey = event.pubkey; + const created_at = event.created_at || 0; + if (!deduped[pubkey] || deduped[pubkey].created_at < created_at) { + deduped[pubkey] = { event, created_at }; + } + } + const dedupedProfiles = Object.values(deduped).map(x => x.event); + onSearchResults(dedupedProfiles, [], [], new Set(dedupedProfiles.map(p => p.id)), new Set()); } - // Add a-tag addresses (kind:pubkey:d) - const aTags = getMatchingTags(event, "a"); - aTags.forEach((tag: string[]) => { - if (tag[1]) { - eventAddresses.add(tag[1]); + } else if (searchType === 'd') { + if (firstOrderEvents.length === 0) { + localError = `No events found with d-tag: ${searchTerm}`; + onSearchResults([], [], [], new Set(), new Set()); + } else { + // Deduplicate by kind, pubkey, and d-tag, keep only newest event for each combination + const deduped: Record = {}; + for (const event of firstOrderEvents) { + const dTag = getMatchingTags(event, 'd')[0]?.[1] || ''; + const key = `${event.kind}:${event.pubkey}:${dTag}`; + const created_at = event.created_at || 0; + if (!deduped[key] || deduped[key].created_at < created_at) { + deduped[key] = { event, created_at }; + } } - }); - }); + const dedupedEvents = Object.values(deduped).map(x => x.event); + onSearchResults(dedupedEvents, [], [], eventIds, eventAddresses); + localError = `Found ${dedupedEvents.length} unique d-tag events. Searching for second-order results...`; + // Perform second-order search in background + firstOrderEvents = dedupedEvents; + performSecondOrderSearch(); + } + } else if (searchType === 't') { + if (tTagEvents.length === 0) { + localError = `No events found with t-tag: ${searchTerm}`; + onSearchResults([], [], [], new Set(), new Set()); + } else { + console.log("[Events] T-tag search completed, found", tTagEvents.length, "events"); + onSearchResults([], [], tTagEvents, new Set(), new Set()); + } + } + + cleanup(); + }); + + // Helper function to perform second-order search for d-tag searches + async function performSecondOrderSearch() { + if (eventIds.size === 0 && eventAddresses.size === 0) { + // No references to search for, just search for t-tag events + console.log("[Events] No references found, searching for t-tag events only"); + try { + const tTagFilter = { '#t': [normalizedSearchTerm] }; + const tTagEventsSet = await ndk.fetchEvents( + tTagFilter, + { closeOnEose: true }, + relaySet, + ); + + const tTagEvents = Array.from(tTagEventsSet).filter(e => + e.kind !== 7 && + !firstOrderEvents.some(fe => fe.id === e.id) + ); - // Search for second-order events that reference the original events - const secondOrderEvents = new Set(); + console.log("[Events] T-tag search completed:", { + firstOrder: firstOrderEvents.length, + tTag: tTagEvents.length + }); + + // Clear the "searching" message + localError = null; + + onSearchResults(firstOrderEvents, [], tTagEvents, eventIds, eventAddresses); + } catch (err) { + console.error("[Events] Error in t-tag search:", err); + localError = null; + onSearchResults(firstOrderEvents, [], [], eventIds, eventAddresses); + } + return; + } + + console.log("[Events] Starting second-order search..."); - if (eventIds.size > 0 || eventAddresses.size > 0) { - console.log("[Events] Searching for second-order events..."); - + try { // Search for events with e tags referencing the original events if (eventIds.size > 0) { const eTagFilter = { "#e": Array.from(eventIds) }; @@ -123,7 +290,11 @@ { closeOnEose: true }, relaySet, ); - eTagEvents.forEach(event => secondOrderEvents.add(event)); + eTagEvents.forEach(event => { + if (event.kind !== 7) { // Skip emoji reactions + secondOrderEvents.push(event); + } + }); } // Search for events with a tags referencing the original events @@ -134,35 +305,36 @@ { closeOnEose: true }, relaySet, ); - aTagEvents.forEach(event => secondOrderEvents.add(event)); + aTagEvents.forEach(event => { + if (event.kind !== 7) { // Skip emoji reactions + secondOrderEvents.push(event); + } + }); } // Search for events with content containing nevent/naddr/note references - // This is a more complex search that requires fetching recent events and checking content // Limit the search to recent events to avoid performance issues const recentEvents = await ndk.fetchEvents( { - limit: 500, // Reduced limit for better performance - since: Math.floor(Date.now() / 1000) - (7 * 24 * 60 * 60) // Last 7 days + limit: 10000, + since: Math.floor(Date.now() / 1000) - (30 * 24 * 60 * 60) // Last 30 days }, { closeOnEose: true }, relaySet, ); recentEvents.forEach(event => { - if (event.content) { - // Check for nevent references with more precise matching + if (event.content && event.kind !== 7) { + // Check for nevent references eventIds.forEach(id => { - // Look for complete nevent references const neventPattern = new RegExp(`nevent1[a-z0-9]{50,}`, 'i'); const matches = event.content.match(neventPattern); if (matches) { - // Verify the nevent contains the event ID matches.forEach(match => { try { const decoded = nip19.decode(match); if (decoded && decoded.type === 'nevent' && decoded.data.id === id) { - secondOrderEvents.add(event); + secondOrderEvents.push(event); } } catch (e) { // Invalid nevent, skip @@ -171,19 +343,18 @@ } }); - // Check for naddr references with more precise matching + // Check for naddr references eventAddresses.forEach(address => { const naddrPattern = new RegExp(`naddr1[a-z0-9]{50,}`, 'i'); const matches = event.content.match(naddrPattern); if (matches) { - // Verify the naddr contains the address matches.forEach(match => { try { const decoded = nip19.decode(match); if (decoded && decoded.type === 'naddr') { const decodedAddress = `${decoded.data.kind}:${decoded.data.pubkey}:${decoded.data.identifier}`; if (decodedAddress === address) { - secondOrderEvents.add(event); + secondOrderEvents.push(event); } } } catch (e) { @@ -193,17 +364,16 @@ } }); - // Check for note references (event IDs) with more precise matching + // Check for note references eventIds.forEach(id => { const notePattern = new RegExp(`note1[a-z0-9]{50,}`, 'i'); const matches = event.content.match(notePattern); if (matches) { - // Verify the note contains the event ID matches.forEach(match => { try { const decoded = nip19.decode(match); if (decoded && decoded.type === 'note' && decoded.data === id) { - secondOrderEvents.add(event); + secondOrderEvents.push(event); } } catch (e) { // Invalid note, skip @@ -213,58 +383,64 @@ }); } }); - } - // Combine first-order and second-order events - const allEvents = [...eventArray, ...Array.from(secondOrderEvents)]; - - // Remove duplicates based on event ID - const uniqueEvents = new Map(); - allEvents.forEach(event => { - if (event.id) { - uniqueEvents.set(event.id, event); - } - }); - - const finalEvents = Array.from(uniqueEvents.values()); - - // Separate first-order and second-order events - const firstOrderSet = new Set(eventArray.map(e => e.id)); - const firstOrder = finalEvents.filter(e => firstOrderSet.has(e.id)); - const secondOrder = finalEvents.filter(e => !firstOrderSet.has(e.id)); - - // Remove kind 7 (emoji reactions) from both first-order and second-order results - const filteredFirstOrder = firstOrder.filter(e => e.kind !== 7); - const filteredSecondOrder = secondOrder.filter(e => e.kind !== 7); - - // --- t: search --- - // Search for events with a matching t-tag (topic/tag) - const tTagFilter = { '#t': [normalizedDTag] }; - const tTagEventsSet = await ndk.fetchEvents( - tTagFilter, - { closeOnEose: true }, - relaySet, - ); - // Remove any events already in first or second order - const tTagEvents = Array.from(tTagEventsSet).filter(e => - e.kind !== 7 && - !firstOrderSet.has(e.id) && - !filteredSecondOrder.some(se => se.id === e.id) - ); + // Remove duplicates from second-order events + const uniqueSecondOrder = new Map(); + secondOrderEvents.forEach(event => { + if (event.id) { + uniqueSecondOrder.set(event.id, event); + } + }); + let deduplicatedSecondOrder = Array.from(uniqueSecondOrder.values()); - onSearchResults(filteredFirstOrder, filteredSecondOrder, tTagEvents, eventIds, eventAddresses); - searching = false; - if (onLoadingChange) { onLoadingChange(false); } - return; - } catch (err) { - console.error("[Events] Error searching by d-tag:", err); - onSearchResults([], [], [], new Set(), new Set()); - searching = false; - if (onLoadingChange) { onLoadingChange(false); } - return; + // Remove any events already in firstOrderEvents (d-tag section) + const firstOrderIds = new Set(firstOrderEvents.map(e => e.id)); + deduplicatedSecondOrder = deduplicatedSecondOrder.filter(e => !firstOrderIds.has(e.id)); + + // Search for t-tag events + const tTagFilter = { '#t': [normalizedSearchTerm] }; + const tTagEventsSet = await ndk.fetchEvents( + tTagFilter, + { closeOnEose: true }, + relaySet, + ); + + // Remove any events already in first or second order + const firstOrderSet = new Set(firstOrderEvents.map(e => e.id)); + const secondOrderSet = new Set(deduplicatedSecondOrder.map(e => e.id)); + + const tTagEvents = Array.from(tTagEventsSet).filter(e => + e.kind !== 7 && + !firstOrderSet.has(e.id) && + !secondOrderSet.has(e.id) + ); + + console.log("[Events] Second-order search completed:", { + firstOrder: firstOrderEvents.length, + secondOrder: deduplicatedSecondOrder.length, + tTag: tTagEvents.length + }); + + // Clear the "searching" message + localError = null; + + // Update results with second-order and t-tag events + onSearchResults(firstOrderEvents, deduplicatedSecondOrder, tTagEvents, eventIds, eventAddresses); + } catch (err) { + console.error("[Events] Error in second-order search:", err); + // Clear the "searching" message + localError = null; + // Return first-order results even if second-order search fails + onSearchResults(firstOrderEvents, [], [], eventIds, eventAddresses); + } } + + if (activeSub) { activeSub.stop(); } + activeSub = sub; } + + async function searchEvent( clearInput: boolean = true, queryOverride?: string, @@ -297,6 +473,24 @@ } } + // Check if this is a t-tag search + if (query.toLowerCase().startsWith("t:")) { + const searchTerm = query.slice(2).trim(); + if (searchTerm) { + await searchBySubscription('t', searchTerm); + return; + } + } + + // Check if this is an npub search + if (query.toLowerCase().startsWith("n:")) { + const searchTerm = query.slice(2).trim(); + if (searchTerm) { + await searchBySubscription('n', searchTerm); + return; + } + } + // Only update the URL if this is a manual search if (clearInput) { const encoded = encodeURIComponent(query); @@ -471,6 +665,9 @@ localError = null; foundEvent = null; relayStatuses = {}; + if (activeSub) { activeSub.stop(); activeSub = null; } + foundProfiles = []; + onSearchResults([], [], [], new Set(), new Set()); if (onClear) { onClear(); } @@ -481,7 +678,7 @@
e.key === "Enter" && searchEvent(true)} /> diff --git a/src/lib/consts.ts b/src/lib/consts.ts index ac908fd..86bb122 100644 --- a/src/lib/consts.ts +++ b/src/lib/consts.ts @@ -23,8 +23,8 @@ export const fallbackRelays = [ "wss://indexer.coracle.social", "wss://relay.noswhere.com", "wss://aggr.nostr.land", - "wss://nostr.wine", "wss://nostr.land", + "wss://nostr.wine", "wss://nostr.sovbit.host", "wss://freelay.sovbit.host", "wss://nostr21.com", diff --git a/src/lib/ndk.ts b/src/lib/ndk.ts index 196ee03..ff130dd 100644 --- a/src/lib/ndk.ts +++ b/src/lib/ndk.ts @@ -15,6 +15,7 @@ import { anonymousRelays, } from "./consts"; import { feedType } from "./stores"; +import { userPubkey } from '$lib/stores/authStore'; export const ndkInstance: Writable = writable(); @@ -435,21 +436,11 @@ function createRelayWithAuth(url: string, ndk: NDK): NDKRelay { } export function getActiveRelays(ndk: NDK): NDKRelaySet { - // Use anonymous relays if user is not signed in - const isSignedIn = ndk.signer && ndk.activeUser; - const relays = isSignedIn ? standardRelays : anonymousRelays; - - return get(feedType) === FeedType.UserRelays - ? new NDKRelaySet( - new Set( - get(inboxRelays).map((relay) => createRelayWithAuth(relay, ndk)), - ), - ndk, - ) - : new NDKRelaySet( - new Set(relays.map((relay) => createRelayWithAuth(relay, ndk))), - ndk, - ); + // Use all relays currently in the NDK pool + return new NDKRelaySet( + new Set(Array.from(ndk.pool.relays.values())), + ndk, + ); } /** @@ -522,6 +513,7 @@ export async function loginWithExtension( } activePubkey.set(signerUser.pubkey); + userPubkey.set(signerUser.pubkey); const [persistedInboxes, persistedOutboxes] = getPersistedRelays(signerUser); @@ -561,6 +553,7 @@ export function logout(user: NDKUser): void { clearLogin(); clearPersistedRelays(user); activePubkey.set(null); + userPubkey.set(null); ndkSignedIn.set(false); ndkInstance.set(initNdk()); // Re-initialize with anonymous instance } diff --git a/src/routes/events/+page.svelte b/src/routes/events/+page.svelte index 2a1c12a..410b02d 100644 --- a/src/routes/events/+page.svelte +++ b/src/routes/events/+page.svelte @@ -12,7 +12,6 @@ import { getMatchingTags, toNpub } from "$lib/utils/nostrUtils"; import EventInput from '$lib/components/EventInput.svelte'; import { userPubkey, isLoggedIn } from '$lib/stores/authStore'; - import RelayStatus from '$lib/components/RelayStatus.svelte'; import { testAllRelays, logRelayDiagnostics } from '$lib/utils/relayDiagnostics'; import CopyToClipboard from '$lib/components/util/CopyToClipboard.svelte'; import { neventEncode, naddrEncode } from '$lib/utils'; @@ -172,9 +171,6 @@ }); onMount(() => { - // Initialize userPubkey from localStorage if available - const pubkey = localStorage.getItem('userPubkey'); - userPubkey.set(pubkey); userRelayPreference = localStorage.getItem('useUserRelays') === 'true'; // Run relay diagnostics to help identify connection issues @@ -183,7 +179,7 @@
-
+
Events
@@ -465,5 +461,11 @@
{/if} + + {#if !event && searchResults.length === 0 && secondOrderResults.length === 0 && tTagResults.length === 0 && !searchValue && !dTagValue} +
+ +
+ {/if}