diff --git a/src/lib/components/CommentBox.svelte b/src/lib/components/CommentBox.svelte index eacb2de..c412cb8 100644 --- a/src/lib/components/CommentBox.svelte +++ b/src/lib/components/CommentBox.svelte @@ -4,7 +4,7 @@ import { nip19 } from "nostr-tools"; import { toNpub, getUserMetadata } from "$lib/utils/nostrUtils"; import { searchProfiles } from "$lib/utils/search_utility"; - import type { NostrProfile } from "$lib/utils/search_utility"; + import type { NostrProfile, ProfileSearchResult } from "$lib/utils/search_utility"; import { userPubkey } from '$lib/stores/authStore.Svelte'; import type { NDKEvent } from "$lib/utils/nostrUtils"; @@ -16,12 +16,6 @@ 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'; import { tick } from 'svelte'; import { goto } from "$app/navigation"; @@ -263,14 +257,25 @@ return; } + console.log('Starting search for:', mentionSearch.trim()); + // Set loading state mentionLoading = true; isSearching = true; try { + console.log('Search promise created, waiting for result...'); const result = await searchProfiles(mentionSearch.trim()); + console.log('Search completed, found profiles:', result.profiles.length); + console.log('Profile details:', result.profiles); + console.log('Community status:', result.Status); + + // Update state mentionResults = result.profiles; communityStatus = result.Status; + + console.log('State updated - mentionResults length:', mentionResults.length); + console.log('State updated - communityStatus keys:', Object.keys(communityStatus)); } catch (error) { console.error('Error searching mentions:', error); mentionResults = []; @@ -278,6 +283,7 @@ } finally { mentionLoading = false; isSearching = false; + console.log('Search finished - loading:', mentionLoading, 'searching:', isSearching); } } @@ -383,6 +389,7 @@ {#if mentionLoading}
Searching...
{:else if mentionResults.length > 0} +
Found {mentionResults.length} results
{/if} + + +
{#if event.kind !== 0} @@ -289,8 +448,30 @@ {@const tagInfo = getTagButtonInfo(tag)} {#if tagInfo.text && tagInfo.gotoValue} -
-
- -
+ #{tag} + + {/each} + + {/if} {/if} diff --git a/src/lib/components/PublicationSection.svelte b/src/lib/components/PublicationSection.svelte index 7d60996..fdbd0a5 100644 --- a/src/lib/components/PublicationSection.svelte +++ b/src/lib/components/PublicationSection.svelte @@ -10,6 +10,7 @@ import type { Asciidoctor, Document } from "asciidoctor"; import { getMatchingTags } from "$lib/utils/nostrUtils"; import { postProcessAdvancedAsciidoctorHtml } from "$lib/utils/markup/advancedAsciidoctorPostProcessor"; + import { goto } from '$app/navigation'; let { address, @@ -23,11 +24,20 @@ ref: (ref: HTMLElement) => void; } = $props(); + console.debug(`[PublicationSection] Received address: ${address}`); + console.debug(`[PublicationSection] Root address: ${rootAddress}`); + console.debug(`[PublicationSection] Leaves count: ${leaves.length}`); + const publicationTree: PublicationTree = getContext("publicationTree"); const asciidoctor: Asciidoctor = getContext("asciidoctor"); let leafEvent: Promise = $derived.by( - async () => await publicationTree.getEvent(address), + async () => { + console.debug(`[PublicationSection] Getting event for address: ${address}`); + const event = await publicationTree.getEvent(address); + console.debug(`[PublicationSection] Retrieved event: ${event?.id}`); + return event; + }, ); let rootEvent: Promise = $derived.by( @@ -52,6 +62,10 @@ return await postProcessAdvancedAsciidoctorHtml(asciidoctorHtml.toString()); }); + let leafHashtags: Promise = $derived.by( + async () => (await leafEvent)?.getMatchingTags("t").map((tag: string[]) => tag[1]) ?? [], + ); + let previousLeafEvent: NDKEvent | null = $derived.by(() => { let index: number; let event: NDKEvent | null = null; @@ -117,6 +131,15 @@ let sectionRef: HTMLElement; + function navigateToHashtagSearch(tag: string): void { + const encoded = encodeURIComponent(tag); + goto(`/events?t=${encoded}`, { + replaceState: false, + keepFocus: true, + noScroll: true, + }); + } + $effect(() => { if (!sectionRef) { return; @@ -125,6 +148,8 @@ ref(sectionRef); }); + +
- {#await Promise.all( [leafTitle, leafContent, leafHierarchy, publicationType, divergingBranches], )} + {#await Promise.all( [leafTitle, leafContent, leafHierarchy, publicationType, divergingBranches, leafEvent, leafHashtags], )} - {:then [leafTitle, leafContent, leafHierarchy, publicationType, divergingBranches]} + {:then [leafTitle, leafContent, leafHierarchy, publicationType, divergingBranches, resolvedLeafEvent, hashtags]} {@const contentString = leafContent.toString()} + + + {#each divergingBranches as [branch, depth]} {@render sectionHeading( getMatchingTags(branch, "title")[0]?.[1] ?? "", @@ -151,5 +179,20 @@ publicationType ?? "article", false, )} + {#if hashtags.length > 0} +
+ {#each hashtags as tag (tag)} + + {/each} +
+ {/if} {/await}
diff --git a/src/lib/components/cards/BlogHeader.svelte b/src/lib/components/cards/BlogHeader.svelte index 9b55a28..dcee584 100644 --- a/src/lib/components/cards/BlogHeader.svelte +++ b/src/lib/components/cards/BlogHeader.svelte @@ -62,6 +62,9 @@ + + + {#if image && active}
{id.label}:
{#if id.link} - + {:else} {id.value} {/if} diff --git a/src/lib/components/util/ContainingIndexes.svelte b/src/lib/components/util/ContainingIndexes.svelte new file mode 100644 index 0000000..1c7da12 --- /dev/null +++ b/src/lib/components/util/ContainingIndexes.svelte @@ -0,0 +1,105 @@ + + +{#if containingIndexes.length > 0 || loading || error} +
+

+ Containing Publications +

+ + {#if loading} +
+ Loading containing publications... +
+ {:else if error} +
+ {error} +
+ {:else if containingIndexes.length > 0} +
+ {#each containingIndexes.slice(0, 3) as indexEvent} + {@const title = + getMatchingTags(indexEvent, "title")[0]?.[1] || "Untitled"} + + {/each} + {#if containingIndexes.length > 3} + + +{containingIndexes.length - 3} more + + {/if} +
+ {:else} +
+ No containing publications found +
+ {/if} +
+{/if} diff --git a/src/lib/components/util/Details.svelte b/src/lib/components/util/Details.svelte index b4d7b93..9c6d312 100644 --- a/src/lib/components/util/Details.svelte +++ b/src/lib/components/util/Details.svelte @@ -4,6 +4,7 @@ import Interactions from "$components/util/Interactions.svelte"; import { P } from "flowbite-svelte"; import { getMatchingTags } from "$lib/utils/nostrUtils"; + import { goto } from '$app/navigation'; // isModal // - don't show interactions in modal view @@ -93,7 +94,11 @@ {#if hashtags.length}
{#each hashtags as tag} - #{tag} + {/each}
{/if} diff --git a/src/lib/consts.ts b/src/lib/consts.ts index b1e4b3c..7e78b2a 100644 --- a/src/lib/consts.ts +++ b/src/lib/consts.ts @@ -2,7 +2,7 @@ export const wikiKind = 30818; export const indexKind = 30040; export const zettelKinds = [30041, 30818]; export const communityRelay = "wss://theforest.nostr1.com"; -export const profileRelay = "wss://profiles.nostr1.com"; +export const profileRelays = ["wss://profiles.nostr1.com", "wss://aggr.nostr.land", "wss://relay.noswhere.com"]; export const standardRelays = [ "wss://thecitadel.nostr1.com", "wss://theforest.nostr1.com", diff --git a/src/lib/navigator/EventNetwork/utils/networkBuilder.ts b/src/lib/navigator/EventNetwork/utils/networkBuilder.ts index e05db37..3ab4eca 100644 --- a/src/lib/navigator/EventNetwork/utils/networkBuilder.ts +++ b/src/lib/navigator/EventNetwork/utils/networkBuilder.ts @@ -163,13 +163,19 @@ export function initializeGraphState(events: NDKEvent[]): GraphState { // Build set of referenced event IDs to identify root events const referencedIds = new Set(); events.forEach((event) => { - const aTags = getMatchingTags(event, "a"); - debug("Processing a-tags for event", { + // Handle both "a" tags (NIP-62) and "e" tags (legacy) + let tags = getMatchingTags(event, "a"); + if (tags.length === 0) { + tags = getMatchingTags(event, "e"); + } + + debug("Processing tags for event", { eventId: event.id, - aTagCount: aTags.length, + tagCount: tags.length, + tagType: tags.length > 0 ? (getMatchingTags(event, "a").length > 0 ? "a" : "e") : "none" }); - aTags.forEach((tag) => { + tags.forEach((tag) => { const id = extractEventIdFromATag(tag); if (id) referencedIds.add(id); }); @@ -284,7 +290,13 @@ export function processIndexEvent( if (level >= maxLevel) return; // Extract the sequence of nodes referenced by this index - const sequence = getMatchingTags(indexEvent, "a") + // Handle both "a" tags (NIP-62) and "e" tags (legacy) + let tags = getMatchingTags(indexEvent, "a"); + if (tags.length === 0) { + tags = getMatchingTags(indexEvent, "e"); + } + + const sequence = tags .map((tag) => extractEventIdFromATag(tag)) .filter((id): id is string => id !== null) .map((id) => state.nodeMap.get(id)) diff --git a/src/lib/ndk.ts b/src/lib/ndk.ts index 2e77c61..7ca1236 100644 --- a/src/lib/ndk.ts +++ b/src/lib/ndk.ts @@ -429,9 +429,22 @@ function createRelayWithAuth(url: string, ndk: NDK): NDKRelay { export function getActiveRelays(ndk: NDK): NDKRelaySet { const user = get(userStore); + + // Filter out problematic relays that are known to cause connection issues + const filterProblematicRelays = (relays: string[]) => { + return relays.filter(relay => { + // Filter out gitcitadel.nostr1.com which is causing connection issues + if (relay.includes('gitcitadel.nostr1.com')) { + console.warn(`[NDK.ts] Filtering out problematic relay: ${relay}`); + return false; + } + return true; + }); + }; + return get(feedType) === FeedType.UserRelays && user.signedIn ? new NDKRelaySet( - new Set(user.relays.inbox.map(relay => new NDKRelay( + new Set(filterProblematicRelays(user.relays.inbox).map(relay => new NDKRelay( relay, NDKRelayAuthPolicies.signIn({ ndk }), ndk, @@ -439,7 +452,7 @@ export function getActiveRelays(ndk: NDK): NDKRelaySet { ndk ) : new NDKRelaySet( - new Set(standardRelays.map(relay => new NDKRelay( + new Set(filterProblematicRelays(standardRelays).map(relay => new NDKRelay( relay, NDKRelayAuthPolicies.signIn({ ndk }), ndk, @@ -589,26 +602,39 @@ export async function getUserPreferredRelays( const inboxRelays = new Set(); const outboxRelays = new Set(); + // Filter out problematic relays + const filterProblematicRelay = (url: string): boolean => { + if (url.includes('gitcitadel.nostr1.com')) { + console.warn(`[NDK.ts] Filtering out problematic relay from user preferences: ${url}`); + return false; + } + return true; + }; + if (relayList == null) { const relayMap = await window.nostr?.getRelays?.(); Object.entries(relayMap ?? {}).forEach(([url, relayType]) => { - const relay = createRelayWithAuth(url, ndk); - if (relayType.read) inboxRelays.add(relay); - if (relayType.write) outboxRelays.add(relay); + if (filterProblematicRelay(url)) { + const relay = createRelayWithAuth(url, ndk); + if (relayType.read) inboxRelays.add(relay); + if (relayType.write) outboxRelays.add(relay); + } }); } else { relayList.tags.forEach((tag) => { - switch (tag[0]) { - case "r": - inboxRelays.add(createRelayWithAuth(tag[1], ndk)); - break; - case "w": - outboxRelays.add(createRelayWithAuth(tag[1], ndk)); - break; - default: - inboxRelays.add(createRelayWithAuth(tag[1], ndk)); - outboxRelays.add(createRelayWithAuth(tag[1], ndk)); - break; + if (filterProblematicRelay(tag[1])) { + switch (tag[0]) { + case "r": + inboxRelays.add(createRelayWithAuth(tag[1], ndk)); + break; + case "w": + outboxRelays.add(createRelayWithAuth(tag[1], ndk)); + break; + default: + inboxRelays.add(createRelayWithAuth(tag[1], ndk)); + outboxRelays.add(createRelayWithAuth(tag[1], ndk)); + break; + } } }); } diff --git a/src/lib/utils/community_checker.ts b/src/lib/utils/community_checker.ts index 6f906e7..eaf1dd8 100644 --- a/src/lib/utils/community_checker.ts +++ b/src/lib/utils/community_checker.ts @@ -55,9 +55,30 @@ export async function checkCommunity(pubkey: string): Promise { export async function checkCommunityStatus(profiles: Array<{ pubkey?: string }>): Promise> { const communityStatus: Record = {}; - for (const profile of profiles) { - if (profile.pubkey) { - communityStatus[profile.pubkey] = await checkCommunity(profile.pubkey); + // Run all community checks in parallel with timeout + const checkPromises = profiles.map(async (profile) => { + if (!profile.pubkey) return { pubkey: '', status: false }; + + try { + const status = await Promise.race([ + checkCommunity(profile.pubkey), + new Promise((resolve) => { + setTimeout(() => resolve(false), 2000); // 2 second timeout per check + }) + ]); + return { pubkey: profile.pubkey, status }; + } catch (error) { + console.warn('Community status check failed for', profile.pubkey, error); + return { pubkey: profile.pubkey, status: false }; + } + }); + + // Wait for all checks to complete + const results = await Promise.allSettled(checkPromises); + + for (const result of results) { + if (result.status === 'fulfilled' && result.value.pubkey) { + communityStatus[result.value.pubkey] = result.value.status; } } diff --git a/src/lib/utils/event_search.ts b/src/lib/utils/event_search.ts index 98c0fe0..95605b4 100644 --- a/src/lib/utils/event_search.ts +++ b/src/lib/utils/event_search.ts @@ -1,10 +1,10 @@ -import { ndkInstance } from '$lib/ndk'; -import { fetchEventWithFallback } from '$lib/utils/nostrUtils'; -import { nip19 } from '$lib/utils/nostrUtils'; -import { NDKEvent } from '@nostr-dev-kit/ndk'; -import { get } from 'svelte/store'; -import { wellKnownUrl, isValidNip05Address } from './search_utils'; -import { TIMEOUTS, VALIDATION } from './search_constants'; +import { ndkInstance } from "$lib/ndk"; +import { fetchEventWithFallback } from "$lib/utils/nostrUtils"; +import { nip19 } from "$lib/utils/nostrUtils"; +import { NDKEvent } from "@nostr-dev-kit/ndk"; +import { get } from "svelte/store"; +import { wellKnownUrl, isValidNip05Address } from "./search_utils"; +import { TIMEOUTS, VALIDATION } from "./search_constants"; /** * Search for a single event by ID or filter @@ -15,7 +15,9 @@ export async function searchEvent(query: string): Promise { let filterOrId: any = cleanedQuery; // If it's a valid hex string, try as event id first, then as pubkey (profile) - if (new RegExp(`^[a-f0-9]{${VALIDATION.HEX_LENGTH}}$`, 'i').test(cleanedQuery)) { + if ( + new RegExp(`^[a-f0-9]{${VALIDATION.HEX_LENGTH}}$`, "i").test(cleanedQuery) + ) { // Try as event id filterOrId = cleanedQuery; const eventResult = await fetchEventWithFallback( @@ -40,7 +42,10 @@ export async function searchEvent(query: string): Promise { return eventResult; } } else if ( - new RegExp(`^(nevent|note|naddr|npub|nprofile)[a-z0-9]{${VALIDATION.MIN_NOSTR_IDENTIFIER_LENGTH},}$`, 'i').test(cleanedQuery) + new RegExp( + `^(nevent|note|naddr|npub|nprofile)[a-z0-9]{${VALIDATION.MIN_NOSTR_IDENTIFIER_LENGTH},}$`, + "i", + ).test(cleanedQuery) ) { try { const decoded = nip19.decode(cleanedQuery); @@ -102,7 +107,9 @@ export async function searchEvent(query: string): Promise { /** * Search for NIP-05 address */ -export async function searchNip05(nip05Address: string): Promise { +export async function searchNip05( + nip05Address: string, +): Promise { // NIP-05 address pattern: user@domain if (!isValidNip05Address(nip05Address)) { throw new Error("Invalid NIP-05 address format. Expected: user@domain"); @@ -110,15 +117,15 @@ export async function searchNip05(nip05Address: string): Promise { + // Support all content event kinds that can be contained in indexes + const contentEventKinds = [30041, 30818, 30040, 30023]; + if (!contentEventKinds.includes(contentEvent.kind)) { + return []; + } + + try { + const ndk = get(ndkInstance); + + // Search for 30040 events that reference this content event + // We need to search for events that have an 'a' tag or 'e' tag referencing this event + const contentEventId = contentEvent.id; + const contentEventAddress = contentEvent.tagAddress(); + + // Search for index events that reference this content event + const indexEvents = await ndk.fetchEvents( + { + kinds: [30040], + "#a": [contentEventAddress], + }, + { + groupable: true, + skipVerification: false, + skipValidation: false, + }, + ); + + // Also search for events with 'e' tags (legacy format) + const indexEventsWithETags = await ndk.fetchEvents( + { + kinds: [30040], + "#e": [contentEventId], + }, + { + groupable: true, + skipVerification: false, + skipValidation: false, + }, + ); + + // Combine and deduplicate results + const allIndexEvents = new Set([...indexEvents, ...indexEventsWithETags]); + + // Filter to only include valid index events + const validIndexEvents = Array.from(allIndexEvents).filter((event) => { + // Check if it's a valid index event (has title, d tag, and either a or e tags) + const hasTitle = event.getMatchingTags("title").length > 0; + const hasDTag = event.getMatchingTags("d").length > 0; + const hasATags = event.getMatchingTags("a").length > 0; + const hasETags = event.getMatchingTags("e").length > 0; + + return hasTitle && hasDTag && (hasATags || hasETags); + }); + + return validIndexEvents; + } catch (error) { + console.error("[Search] Error finding containing index events:", error); + return []; + } +} diff --git a/src/lib/utils/nostrEventService.ts b/src/lib/utils/nostrEventService.ts index 8a59d8e..6a41c12 100644 --- a/src/lib/utils/nostrEventService.ts +++ b/src/lib/utils/nostrEventService.ts @@ -400,8 +400,18 @@ export async function publishEvent( * Navigate to the published event */ export function navigateToEvent(eventId: string): void { - const nevent = nip19.neventEncode({ id: eventId }); - goto(`/events?id=${nevent}`); + try { + // Validate that eventId is a valid hex string + if (!/^[0-9a-fA-F]{64}$/.test(eventId)) { + console.warn('Invalid event ID format:', eventId); + return; + } + + const nevent = nip19.neventEncode({ id: eventId }); + goto(`/events?id=${nevent}`); + } catch (error) { + console.error('Failed to encode event ID for navigation:', eventId, error); + } } // Helper functions to ensure relay and pubkey are always strings diff --git a/src/lib/utils/nostrUtils.ts b/src/lib/utils/nostrUtils.ts index dd272d4..e670316 100644 --- a/src/lib/utils/nostrUtils.ts +++ b/src/lib/utils/nostrUtils.ts @@ -156,12 +156,26 @@ export async function createProfileLinkWithVerification( const userRelays = Array.from(ndk.pool?.relays.values() || []).map( (r) => r.url, ); + + // Filter out problematic relays + const filterProblematicRelays = (relays: string[]) => { + return relays.filter(relay => { + if (relay.includes('gitcitadel.nostr1.com')) { + console.info(`[nostrUtils.ts] Filtering out problematic relay: ${relay}`); + return false; + } + return true; + }); + }; + const allRelays = [ ...standardRelays, ...userRelays, ...fallbackRelays, ].filter((url, idx, arr) => arr.indexOf(url) === idx); - const relaySet = NDKRelaySetFromNDK.fromRelayUrls(allRelays, ndk); + + const filteredRelays = filterProblematicRelays(allRelays); + const relaySet = NDKRelaySetFromNDK.fromRelayUrls(filteredRelays, ndk); const profileEvent = await ndk.fetchEvent( { kinds: [0], authors: [user.pubkey] }, undefined, @@ -272,26 +286,60 @@ export async function getNpubFromNip05(nip05: string): Promise { return null; } - // Fetch the well-known.json file + // Fetch the well-known.json file with timeout and CORS handling const url = wellKnownUrl(domain, name); - const response = await fetch(url); - if (!response.ok) { - console.error('[getNpubFromNip05] HTTP error:', response.status, response.statusText); - return null; - } - - const data = await response.json(); + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), 3000); // 3 second timeout - const pubkey = data.names?.[name]; - if (!pubkey) { - console.error('[getNpubFromNip05] No pubkey found for name:', name); + try { + const response = await fetch(url, { + signal: controller.signal, + mode: 'cors', + headers: { + 'Accept': 'application/json' + } + }); + + clearTimeout(timeoutId); + + if (!response.ok) { + console.error('[getNpubFromNip05] HTTP error:', response.status, response.statusText); + return null; + } + + const data = await response.json(); + + // Try exact match first + let pubkey = data.names?.[name]; + + // If not found, try case-insensitive search + if (!pubkey && data.names) { + const names = Object.keys(data.names); + const matchingName = names.find(n => n.toLowerCase() === name.toLowerCase()); + if (matchingName) { + pubkey = data.names[matchingName]; + console.log(`[getNpubFromNip05] Found case-insensitive match: ${name} -> ${matchingName}`); + } + } + + if (!pubkey) { + console.error('[getNpubFromNip05] No pubkey found for name:', name); + return null; + } + + // Convert pubkey to npub + const npub = nip19.npubEncode(pubkey); + return npub; + } catch (fetchError: unknown) { + clearTimeout(timeoutId); + if (fetchError instanceof Error && fetchError.name === 'AbortError') { + console.warn('[getNpubFromNip05] Request timeout for:', url); + } else { + console.warn('[getNpubFromNip05] CORS or network error for:', url); + } return null; } - - // Convert pubkey to npub - const npub = nip19.npubEncode(pubkey); - return npub; } catch (error) { console.error("[getNpubFromNip05] Error getting npub from nip05:", error); return null; @@ -366,6 +414,7 @@ export async function fetchEventWithFallback( ? Array.from(ndk.pool?.relays.values() || []) .filter((r) => r.status === 1) // Only use connected relays .map((r) => r.url) + .filter(url => !url.includes('gitcitadel.nostr1.com')) // Filter out problematic relay : []; // Determine which relays to use based on user authentication status diff --git a/src/lib/utils/profile_search.ts b/src/lib/utils/profile_search.ts index 29dc408..37b9e42 100644 --- a/src/lib/utils/profile_search.ts +++ b/src/lib/utils/profile_search.ts @@ -2,7 +2,7 @@ import { ndkInstance } from '$lib/ndk'; import { getUserMetadata, getNpubFromNip05 } from '$lib/utils/nostrUtils'; import { NDKRelaySet, NDKEvent } from '@nostr-dev-kit/ndk'; import { searchCache } from '$lib/utils/searchCache'; -import { communityRelay, profileRelay } from '$lib/consts'; +import { standardRelays, fallbackRelays } from '$lib/consts'; import { get } from 'svelte/store'; import type { NostrProfile, ProfileSearchResult } from './search_types'; import { fieldMatches, nip05Matches, normalizeSearchTerm, COMMON_DOMAINS, createProfileFromEvent } from './search_utils'; @@ -13,11 +13,14 @@ import { TIMEOUTS } from './search_constants'; * Search for profiles by various criteria (display name, name, NIP-05, npub) */ export async function searchProfiles(searchTerm: string): Promise { - const normalizedSearchTerm = searchTerm.toLowerCase().trim(); + const normalizedSearchTerm = normalizeSearchTerm(searchTerm); + + console.log('searchProfiles called with:', searchTerm, 'normalized:', normalizedSearchTerm); // Check cache first const cachedResult = searchCache.get('profile', normalizedSearchTerm); if (cachedResult) { + console.log('Found cached result for:', normalizedSearchTerm); const profiles = cachedResult.events.map(event => { try { const profileData = JSON.parse(event.content); @@ -27,24 +30,19 @@ export async function searchProfiles(searchTerm: string): Promise | null = null; - - // Set a timeout to force completion after profile search timeout - timeoutId = setTimeout(() => { - if (foundProfiles.length === 0) { - // Timeout reached, but no need to log this - } - }, TIMEOUTS.PROFILE_SEARCH); try { // Check if it's a valid npub/nprofile first @@ -58,9 +56,10 @@ export async function searchProfiles(searchTerm: string): Promise((resolve) => { - const checkComplete = () => { - if (timeoutId === null || foundProfiles.length > 0) { - resolve(); - } else { - setTimeout(checkComplete, 100); - } - }; - checkComplete(); - }); - // Cache the results if (foundProfiles.length > 0) { const events = foundProfiles.map(profile => { @@ -116,17 +106,12 @@ export async function searchProfiles(searchTerm: string): Promise { + const foundProfiles: NostrProfile[] = []; + + // Enhanced list of common domains for NIP-05 lookups + // Prioritize gitcitadel.com since we know it has profiles + const commonDomains = [ + 'gitcitadel.com', // Prioritize this domain + 'theforest.nostr1.com', + 'nostr1.com', + 'nostr.land', + 'sovbit.host', + 'damus.io', + 'snort.social', + 'iris.to', + 'coracle.social', + 'nostr.band', + 'nostr.wine', + 'purplepag.es', + 'relay.noswhere.com', + 'aggr.nostr.land', + 'nostr.sovbit.host', + 'freelay.sovbit.host', + 'nostr21.com', + 'greensoul.space', + 'relay.damus.io', + 'relay.nostr.band' + ]; + + // Normalize the search term for NIP-05 lookup + const normalizedSearchTerm = searchTerm.toLowerCase().trim(); + console.log('NIP-05 search: normalized search term:', normalizedSearchTerm); + + // Try gitcitadel.com first with extra debugging + const gitcitadelAddress = `${normalizedSearchTerm}@gitcitadel.com`; + console.log('NIP-05 search: trying gitcitadel.com first:', gitcitadelAddress); try { - for (const domain of COMMON_DOMAINS) { - const nip05Address = `${searchTerm}@${domain}`; - try { - const npub = await getNpubFromNip05(nip05Address); - if (npub) { - const metadata = await getUserMetadata(npub); - const profile: NostrProfile = { - ...metadata, - pubkey: npub - }; - return [profile]; - } - } catch (e) { - // Continue to next domain - } + const npub = await getNpubFromNip05(gitcitadelAddress); + if (npub) { + console.log('NIP-05 search: SUCCESS! found npub for gitcitadel.com:', npub); + const metadata = await getUserMetadata(npub); + const profile: NostrProfile = { + ...metadata, + pubkey: npub + }; + console.log('NIP-05 search: created profile for gitcitadel.com:', profile); + foundProfiles.push(profile); + return foundProfiles; // Return immediately if we found it on gitcitadel.com + } else { + console.log('NIP-05 search: no npub found for gitcitadel.com'); } } catch (e) { - console.error('[Search] NIP-05 domain search failed:', e); + console.log('NIP-05 search: error for gitcitadel.com:', e); } - return []; + + // If gitcitadel.com didn't work, try other domains + console.log('NIP-05 search: gitcitadel.com failed, trying other domains...'); + const otherDomains = commonDomains.filter(domain => domain !== 'gitcitadel.com'); + + // Search all other domains in parallel with timeout + const searchPromises = otherDomains.map(async (domain) => { + const nip05Address = `${normalizedSearchTerm}@${domain}`; + console.log('NIP-05 search: trying address:', nip05Address); + try { + const npub = await getNpubFromNip05(nip05Address); + if (npub) { + console.log('NIP-05 search: found npub for', nip05Address, ':', npub); + const metadata = await getUserMetadata(npub); + const profile: NostrProfile = { + ...metadata, + pubkey: npub + }; + console.log('NIP-05 search: created profile for', nip05Address, ':', profile); + return profile; + } else { + console.log('NIP-05 search: no npub found for', nip05Address); + } + } catch (e) { + console.log('NIP-05 search: error for', nip05Address, ':', e); + // Continue to next domain + } + return null; + }); + + // Wait for all searches with timeout + const results = await Promise.allSettled(searchPromises); + + for (const result of results) { + if (result.status === 'fulfilled' && result.value) { + foundProfiles.push(result.value); + } + } + + console.log('NIP-05 search: total profiles found:', foundProfiles.length); + return foundProfiles; } /** - * Search for profiles across relays + * Quick relay search with short timeout */ -async function searchProfilesAcrossRelays(searchTerm: string, ndk: any): Promise { +async function quickRelaySearch(searchTerm: string, ndk: any): Promise { + console.log('quickRelaySearch called with:', searchTerm); const foundProfiles: NostrProfile[] = []; - // Prioritize community relays for better search results - const allRelays = Array.from(ndk.pool.relays.values()) as any[]; - const prioritizedRelays = new Set([ - ...allRelays.filter((relay: any) => relay.url === communityRelay), - ...allRelays.filter((relay: any) => relay.url !== communityRelay) - ]); - const relaySet = new NDKRelaySet(prioritizedRelays as any, ndk); - - // Subscribe to profile events - const sub = ndk.subscribe( - { kinds: [0] }, - { closeOnEose: true }, - relaySet - ); - - return new Promise((resolve) => { - sub.on('event', (event: NDKEvent) => { - try { - if (!event.content) return; - const profileData = JSON.parse(event.content); - const displayName = profileData.displayName || profileData.display_name || ''; - const display_name = profileData.display_name || ''; - const name = profileData.name || ''; - const nip05 = profileData.nip05 || ''; - const about = profileData.about || ''; - - // Check if any field matches the search term - const matchesDisplayName = fieldMatches(displayName, searchTerm); - const matchesDisplay_name = fieldMatches(display_name, searchTerm); - const matchesName = fieldMatches(name, searchTerm); - const matchesNip05 = nip05Matches(nip05, searchTerm); - const matchesAbout = fieldMatches(about, searchTerm); - - if (matchesDisplayName || matchesDisplay_name || matchesName || matchesNip05 || matchesAbout) { - const profile = createProfileFromEvent(event, profileData); + // Normalize the search term for relay search + const normalizedSearchTerm = normalizeSearchTerm(searchTerm); + console.log('Normalized search term for relay search:', normalizedSearchTerm); + + // Use all profile relays for better coverage + const quickRelayUrls = [...standardRelays, ...fallbackRelays]; // Use all available relays + console.log('Using all relays for search:', quickRelayUrls); + + // Create relay sets for parallel search + const relaySets = quickRelayUrls.map(url => { + try { + return NDKRelaySet.fromRelayUrls([url], ndk); + } catch (e) { + console.warn(`Failed to create relay set for ${url}:`, e); + return null; + } + }).filter(Boolean); + + // Search all relays in parallel with short timeout + const searchPromises = relaySets.map(async (relaySet, index) => { + if (!relaySet) return []; + + return new Promise((resolve) => { + const foundInRelay: NostrProfile[] = []; + let eventCount = 0; + + console.log(`Starting search on relay ${index + 1}: ${quickRelayUrls[index]}`); + + const sub = ndk.subscribe( + { kinds: [0] }, + { closeOnEose: true, relaySet } + ); + + sub.on('event', (event: NDKEvent) => { + eventCount++; + try { + if (!event.content) return; + const profileData = JSON.parse(event.content); + const displayName = profileData.displayName || profileData.display_name || ''; + const display_name = profileData.display_name || ''; + const name = profileData.name || ''; + const nip05 = profileData.nip05 || ''; + const about = profileData.about || ''; + + // Check if any field matches the search term using normalized comparison + const matchesDisplayName = fieldMatches(displayName, normalizedSearchTerm); + const matchesDisplay_name = fieldMatches(display_name, normalizedSearchTerm); + const matchesName = fieldMatches(name, normalizedSearchTerm); + const matchesNip05 = nip05Matches(nip05, normalizedSearchTerm); + const matchesAbout = fieldMatches(about, normalizedSearchTerm); - // Check if we already have this profile - const existingIndex = foundProfiles.findIndex(p => p.pubkey === event.pubkey); - if (existingIndex === -1) { - foundProfiles.push(profile); + if (matchesDisplayName || matchesDisplay_name || matchesName || matchesNip05 || matchesAbout) { + console.log(`Found matching profile on relay ${index + 1}:`, { + name: profileData.name, + display_name: profileData.display_name, + nip05: profileData.nip05, + pubkey: event.pubkey, + searchTerm: normalizedSearchTerm + }); + const profile = createProfileFromEvent(event, profileData); + + // Check if we already have this profile in this relay + const existingIndex = foundInRelay.findIndex(p => p.pubkey === event.pubkey); + if (existingIndex === -1) { + foundInRelay.push(profile); + } } + } catch (e) { + // Invalid JSON or other error, skip } - } catch (e) { - // Invalid JSON or other error, skip - } + }); + + sub.on('eose', () => { + console.log(`Relay ${index + 1} (${quickRelayUrls[index]}) search completed, processed ${eventCount} events, found ${foundInRelay.length} matches`); + resolve(foundInRelay); + }); + + // 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`); + sub.stop(); + resolve(foundInRelay); + }, 1500); // 1.5 second timeout per relay }); + }); - sub.on('eose', () => { - if (foundProfiles.length > 0) { - // Deduplicate by pubkey, keep only newest - const deduped: Record = {}; - for (const profile of foundProfiles) { - const pubkey = profile.pubkey; - if (pubkey) { - // We don't have created_at from getUserMetadata, so just keep the first one - if (!deduped[pubkey]) { - deduped[pubkey] = { profile, created_at: 0 }; - } - } + // Wait for all searches to complete + const results = await Promise.allSettled(searchPromises); + + // Combine and deduplicate results + const allProfiles: Record = {}; + + for (const result of results) { + if (result.status === 'fulfilled') { + for (const profile of result.value) { + if (profile.pubkey) { + allProfiles[profile.pubkey] = profile; } - const dedupedProfiles = Object.values(deduped).map(x => x.profile); - resolve(dedupedProfiles); - } else { - resolve([]); } - }); - }); + } + } + + console.log(`Total unique profiles found: ${Object.keys(allProfiles).length}`); + return Object.values(allProfiles); } \ No newline at end of file diff --git a/src/lib/utils/search_constants.ts b/src/lib/utils/search_constants.ts index 2fa927a..577c53d 100644 --- a/src/lib/utils/search_constants.ts +++ b/src/lib/utils/search_constants.ts @@ -14,7 +14,10 @@ export const TIMEOUTS = { PROFILE_SEARCH: 15000, /** Timeout for subscription search operations */ - SUBSCRIPTION_SEARCH: 30000, + SUBSCRIPTION_SEARCH: 10000, + + /** Timeout for second-order search operations */ + SECOND_ORDER_SEARCH: 5000, /** Timeout for relay diagnostics */ RELAY_DIAGNOSTICS: 5000, diff --git a/src/lib/utils/subscription_search.ts b/src/lib/utils/subscription_search.ts index 30f4dda..46cc49f 100644 --- a/src/lib/utils/subscription_search.ts +++ b/src/lib/utils/subscription_search.ts @@ -3,7 +3,7 @@ import { getMatchingTags, getNpubFromNip05 } from '$lib/utils/nostrUtils'; import { nip19 } from '$lib/utils/nostrUtils'; import { NDKRelaySet, NDKEvent } from '@nostr-dev-kit/ndk'; import { searchCache } from '$lib/utils/searchCache'; -import { communityRelay, profileRelay } from '$lib/consts'; +import { communityRelay, profileRelays } from '$lib/consts'; import { get } from 'svelte/store'; import type { SearchResult, SearchSubscriptionType, SearchFilter, SearchCallbacks, SecondOrderSearchParams } from './search_types'; import { fieldMatches, nip05Matches, normalizeSearchTerm, COMMON_DOMAINS, isEmojiReaction } from './search_utils'; @@ -209,17 +209,17 @@ async function createProfileSearchFilter(normalizedSearchTerm: string): Promise< */ function createPrimaryRelaySet(searchType: SearchSubscriptionType, ndk: any): NDKRelaySet { if (searchType === 'n') { - // For profile searches, use profile relay first - const profileRelays = Array.from(ndk.pool.relays.values()).filter((relay: any) => - relay.url === profileRelay || relay.url === profileRelay + '/' + // For profile searches, use profile relays first + const profileRelaySet = Array.from(ndk.pool.relays.values()).filter((relay: any) => + profileRelays.some(profileRelay => relay.url === profileRelay || relay.url === profileRelay + '/') ); - return new NDKRelaySet(new Set(profileRelays) as any, ndk); + return new NDKRelaySet(new Set(profileRelaySet) as any, ndk); } else { // For other searches, use community relay first - const communityRelays = Array.from(ndk.pool.relays.values()).filter((relay: any) => + const communityRelaySet = Array.from(ndk.pool.relays.values()).filter((relay: any) => relay.url === communityRelay || relay.url === communityRelay + '/' ); - return new NDKRelaySet(new Set(communityRelays) as any, ndk); + return new NDKRelaySet(new Set(communityRelaySet) as any, ndk); } } @@ -308,8 +308,13 @@ function processContentEvent(event: NDKEvent, searchType: SearchSubscriptionType if (event.id) { searchState.eventIds.add(event.id); } - const aTags = getMatchingTags(event, "a"); - aTags.forEach((tag: string[]) => { + // Handle both "a" tags (NIP-62) and "e" tags (legacy) + let tags = getMatchingTags(event, "a"); + if (tags.length === 0) { + tags = getMatchingTags(event, "e"); + } + + tags.forEach((tag: string[]) => { if (tag[1]) { searchState.eventAddresses.add(tag[1]); } @@ -338,9 +343,9 @@ function hasResults(searchState: any, searchType: SearchSubscriptionType): boole */ function createSearchResult(searchState: any, searchType: SearchSubscriptionType, normalizedSearchTerm: string): SearchResult { return { - events: searchType === 'n' ? searchState.foundProfiles : searchState.firstOrderEvents, + events: searchType === 'n' ? searchState.foundProfiles : searchType === 't' ? searchState.tTagEvents : searchState.firstOrderEvents, secondOrder: [], - tTagEvents: searchType === 't' ? searchState.tTagEvents : [], + tTagEvents: [], eventIds: searchState.eventIds, addresses: searchState.eventAddresses, searchType: searchType, @@ -364,8 +369,8 @@ async function searchOtherRelaysInBackground( const otherRelays = new NDKRelaySet( new Set(Array.from(ndk.pool.relays.values()).filter((relay: any) => { if (searchType === 'n') { - // For profile searches, exclude profile relay from fallback search - return relay.url !== profileRelay && relay.url !== profileRelay + '/'; + // For profile searches, exclude profile relays from fallback search + return !profileRelays.some(profileRelay => relay.url === profileRelay || relay.url === profileRelay + '/'); } else { // For other searches, exclude community relay from fallback search return relay.url !== communityRelay && relay.url !== communityRelay + '/'; @@ -525,9 +530,9 @@ function processTTagEoseResults(searchState: any): SearchResult { } return { - events: [], + events: searchState.tTagEvents, secondOrder: [], - tTagEvents: searchState.tTagEvents, + tTagEvents: [], eventIds: new Set(), addresses: new Set(), searchType: 't', @@ -564,87 +569,87 @@ async function performSecondOrderSearchInBackground( try { const ndk = get(ndkInstance); let allSecondOrderEvents: NDKEvent[] = []; - - if (searchType === 'n' && targetPubkey) { - // Search for events that mention this pubkey via p-tags - const pTagFilter = { "#p": [targetPubkey] }; - const pTagEvents = await ndk.fetchEvents( - pTagFilter, - { 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)); - allSecondOrderEvents = [...allSecondOrderEvents, ...filteredEvents]; - - } else if (searchType === 'd') { - // Search for events that reference the original events via e-tags and a-tags - - // Search for events that reference the original events via e-tags - if (eventIds.size > 0) { - const eTagFilter = { "#e": Array.from(eventIds) }; - const eTagEvents = await ndk.fetchEvents( - eTagFilter, + + // Set a timeout for second-order search + const timeoutPromise = new Promise((_, reject) => { + setTimeout(() => reject(new Error('Second-order search timeout')), TIMEOUTS.SECOND_ORDER_SEARCH); + }); + + const searchPromise = (async () => { + if (searchType === 'n' && targetPubkey) { + // Search for events that mention this pubkey via p-tags + const pTagFilter = { '#p': [targetPubkey] }; + const pTagEvents = await ndk.fetchEvents( + pTagFilter, { closeOnEose: true }, new NDKRelaySet(new Set(Array.from(ndk.pool.relays.values())), ndk), ); - // Filter out emoji reactions - const filteredETagEvents = Array.from(eTagEvents).filter(event => !isEmojiReaction(event)); - allSecondOrderEvents = [...allSecondOrderEvents, ...filteredETagEvents]; - } - - // Search for events that reference the original events via a-tags - if (addresses.size > 0) { - const aTagFilter = { "#a": Array.from(addresses) }; - const aTagEvents = await ndk.fetchEvents( - aTagFilter, - { closeOnEose: true }, - new NDKRelaySet(new Set(Array.from(ndk.pool.relays.values())), ndk), - ); - + const filteredEvents = Array.from(pTagEvents).filter(event => !isEmojiReaction(event)); + allSecondOrderEvents = [...allSecondOrderEvents, ...filteredEvents]; + } else if (searchType === 'd') { + // Parallel fetch for #e and #a tag events + const relaySet = new NDKRelaySet(new Set(Array.from(ndk.pool.relays.values())), ndk); + const [eTagEvents, aTagEvents] = await Promise.all([ + eventIds.size > 0 + ? ndk.fetchEvents( + { '#e': Array.from(eventIds) }, + { closeOnEose: true }, + relaySet + ) + : Promise.resolve([]), + addresses.size > 0 + ? ndk.fetchEvents( + { '#a': Array.from(addresses) }, + { closeOnEose: true }, + relaySet + ) + : Promise.resolve([]), + ]); // Filter out emoji reactions + const filteredETagEvents = Array.from(eTagEvents).filter(event => !isEmojiReaction(event)); const filteredATagEvents = Array.from(aTagEvents).filter(event => !isEmojiReaction(event)); - allSecondOrderEvents = [...allSecondOrderEvents, ...filteredATagEvents]; + allSecondOrderEvents = [...allSecondOrderEvents, ...filteredETagEvents, ...filteredATagEvents]; } - } - - // Deduplicate by event ID - const uniqueSecondOrder = new Map(); - allSecondOrderEvents.forEach(event => { - if (event.id) { - uniqueSecondOrder.set(event.id, event); + + // Deduplicate by event ID + const uniqueSecondOrder = new Map(); + allSecondOrderEvents.forEach(event => { + if (event.id) { + uniqueSecondOrder.set(event.id, event); + } + }); + + let deduplicatedSecondOrder = Array.from(uniqueSecondOrder.values()); + + // Remove any events already in first order + const firstOrderIds = new Set(firstOrderEvents.map(e => e.id)); + deduplicatedSecondOrder = deduplicatedSecondOrder.filter(e => !firstOrderIds.has(e.id)); + + // Sort by creation date (newest first) and limit to newest results + const sortedSecondOrder = deduplicatedSecondOrder + .sort((a, b) => (b.created_at || 0) - (a.created_at || 0)) + .slice(0, SEARCH_LIMITS.SECOND_ORDER_RESULTS); + + // Update the search results with second-order events + const result: SearchResult = { + events: firstOrderEvents, + secondOrder: sortedSecondOrder, + tTagEvents: [], + eventIds: searchType === 'n' ? new Set(firstOrderEvents.map(p => p.id)) : eventIds, + addresses: searchType === 'n' ? new Set() : addresses, + searchType: searchType, + searchTerm: '' // This will be set by the caller + }; + + // Notify UI of updated results + if (callbacks?.onSecondOrderUpdate) { + callbacks.onSecondOrderUpdate(result); } - }); - - let deduplicatedSecondOrder = Array.from(uniqueSecondOrder.values()); - - // Remove any events already in first order - const firstOrderIds = new Set(firstOrderEvents.map(e => e.id)); - deduplicatedSecondOrder = deduplicatedSecondOrder.filter(e => !firstOrderIds.has(e.id)); - - // Sort by creation date (newest first) and limit to newest results - const sortedSecondOrder = deduplicatedSecondOrder - .sort((a, b) => (b.created_at || 0) - (a.created_at || 0)) - .slice(0, SEARCH_LIMITS.SECOND_ORDER_RESULTS); - - // Update the search results with second-order events - const result: SearchResult = { - events: firstOrderEvents, - secondOrder: sortedSecondOrder, - tTagEvents: [], - eventIds: searchType === 'n' ? new Set(firstOrderEvents.map(p => p.id)) : eventIds, - addresses: searchType === 'n' ? new Set() : addresses, - searchType: searchType, - searchTerm: '' // This will be set by the caller - }; - - // Notify UI of updated results - if (callbacks?.onSecondOrderUpdate) { - callbacks.onSecondOrderUpdate(result); - } - + })(); + + // Race between search and timeout + await Promise.race([searchPromise, timeoutPromise]); } catch (err) { console.error(`[Search] Error in second-order ${searchType}-tag search:`, err); } diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index 27c3693..21a723d 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -1,37 +1,13 @@