diff --git a/src/lib/components/EventDetails.svelte b/src/lib/components/EventDetails.svelte new file mode 100644 index 0000000..463dcfa --- /dev/null +++ b/src/lib/components/EventDetails.svelte @@ -0,0 +1,207 @@ + + +
+ {#if event.kind !== 0 && getEventTitle(event)} +

{getEventTitle(event)}

+ {:else if event.kind === 0 && profile && profile.name} +

{profile.name}

+ {/if} + +
+ {#if toNpub(event.pubkey)} + Author: {@render userBadge(toNpub(event.pubkey) as string, profile?.display_name || event.pubkey)} + {:else} + Author: {profile?.display_name || event.pubkey} + {/if} +
+ +
+ Kind: + {event.kind} + ({getEventTypeDisplay(event)}) +
+ + {#if getEventSummary(event)} +
+ Summary: +

{getEventSummary(event)}

+
+ {/if} + + {#if getEventHashtags(event).length} +
+ Tags: +
+ {#each getEventHashtags(event) as tag} + #{tag} + {/each} +
+
+ {/if} + + +
+ Content: + {#if event.kind === 0} + {#if profile} +
+
+ {#if profile.name} +
+
Name:
+
{profile.name}
+
+ {/if} + {#if profile.display_name} +
+
Display Name:
+
{profile.display_name}
+
+ {/if} + {#if profile.about} +
+
About:
+
{profile.about}
+
+ {/if} + {#if profile.picture} +
+
Picture:
+
+ Profile { (e.target as HTMLImageElement).src = '/favicon.png'; }} /> +
+
+ {/if} + {#if profile.banner} +
+
Banner:
+
+ Banner { (e.target as HTMLImageElement).src = '/favicon.png'; }} /> +
+
+ {/if} + {#if profile.website} +
+
Website:
+
+ {profile.website} +
+
+ {/if} + {#if profile.lud16} +
+
Lightning Address:
+
{profile.lud16}
+
+ {/if} + {#if profile.nip05} +
+
NIP-05:
+
{profile.nip05}
+
+ {/if} +
+
+ {:else} +
{event.content}
+ {/if} + {:else} +
+ {@html showFullContent ? parsedContent : contentPreview} + {#if !showFullContent && parsedContent.length > 250} + + {/if} +
+ {/if} +
+ + + {#if event.tags && event.tags.length} +
+ Event Tags: +
+ {#each event.tags as tag} + {@html renderTag(tag)} + {/each} +
+
+ {/if} + + +
+ + Show Raw Event JSON + +
+      {JSON.stringify(event.rawEvent(), null, 2)}
+    
+
+
\ No newline at end of file diff --git a/src/lib/components/EventSearch.svelte b/src/lib/components/EventSearch.svelte new file mode 100644 index 0000000..14f8e40 --- /dev/null +++ b/src/lib/components/EventSearch.svelte @@ -0,0 +1,218 @@ + + +
+
+ e.key === 'Enter' && searchEvent(true)} + /> + +
+ + {#if localError || error} + + {/if} + +
+
+ {#each Object.entries(relayStatuses) as [relay, status]} + + {/each} +
+ {#if !foundEvent && Object.values(relayStatuses).some(s => s === 'pending')} +
Searching relays...
+ {/if} + {#if !foundEvent && !searching && Object.values(relayStatuses).every(s => s !== 'pending')} +
Event not found on any relay.
+ {/if} +
+
\ No newline at end of file diff --git a/src/lib/components/PublicationFeed.svelte b/src/lib/components/PublicationFeed.svelte index ef3448f..c9224c6 100644 --- a/src/lib/components/PublicationFeed.svelte +++ b/src/lib/components/PublicationFeed.svelte @@ -2,84 +2,107 @@ import { indexKind } from '$lib/consts'; import { ndkInstance } from '$lib/ndk'; import { filterValidIndexEvents } from '$lib/utils'; - import { fetchEventWithFallback } from '$lib/utils/nostrUtils'; import { NDKRelaySet, type NDKEvent } from '@nostr-dev-kit/ndk'; import { Button, P, Skeleton, Spinner } from 'flowbite-svelte'; import ArticleHeader from './PublicationHeader.svelte'; import { onMount } from 'svelte'; - let { relays } = $props<{ relays: string[] }>(); + let { relays, fallbackRelays } = $props<{ relays: string[], fallbackRelays: string[] }>(); let eventsInView: NDKEvent[] = $state([]); let loadingMore: boolean = $state(false); let endOfFeed: boolean = $state(false); + let relayStatuses = $state>({}); + let loading: boolean = $state(true); let cutoffTimestamp: number = $derived( eventsInView?.at(eventsInView.length - 1)?.created_at ?? new Date().getTime() ); - async function getEvents( - before: number | undefined = undefined, - ): Promise { - try { - // First try to fetch a single event to verify we can connect to the relays - const testEvent = await fetchEventWithFallback($ndkInstance, { - kinds: [indexKind], - limit: 1, - until: before - }); - - if (!testEvent) { - console.warn('No events found in initial fetch'); - return; - } - - // If we found an event, proceed with fetching the full set - let eventSet = await $ndkInstance.fetchEvents( - { - kinds: [indexKind], - limit: 16, - until: before, - }, - { - groupable: false, - skipVerification: false, - skipValidation: false, - }, - NDKRelaySet.fromRelayUrls(relays, $ndkInstance) + async function getEvents(before: number | undefined = undefined) { + loading = true; + const ndk = $ndkInstance; + const primaryRelays: string[] = relays; + const fallback: string[] = fallbackRelays.filter((r: string) => !primaryRelays.includes(r)); + relayStatuses = Object.fromEntries(primaryRelays.map((r: string) => [r, 'pending'])); + let allEvents: NDKEvent[] = []; + // First, try primary relays + await Promise.all( + primaryRelays.map(async (relay: string) => { + try { + const relaySet = NDKRelaySet.fromRelayUrls([relay], ndk); + let eventSet = await ndk.fetchEvents( + { + kinds: [indexKind], + limit: 16, + until: before, + }, + { + groupable: false, + skipVerification: false, + skipValidation: false, + }, + relaySet + ).withTimeout(2500); + eventSet = filterValidIndexEvents(eventSet); + const eventArray = Array.from(eventSet); + if (eventArray.length > 0) { + allEvents = allEvents.concat(eventArray); + relayStatuses = { ...relayStatuses, [relay]: 'found' }; + } else { + relayStatuses = { ...relayStatuses, [relay]: 'notfound' }; + } + } catch { + relayStatuses = { ...relayStatuses, [relay]: 'notfound' }; + } + }) + ); + // If no events found, try fallback relays + if (allEvents.length === 0 && fallback.length > 0) { + relayStatuses = { ...relayStatuses, ...Object.fromEntries(fallback.map((r: string) => [r, 'pending'])) }; + await Promise.all( + fallback.map(async (relay: string) => { + try { + const relaySet = NDKRelaySet.fromRelayUrls([relay], ndk); + let eventSet = await ndk.fetchEvents( + { + kinds: [indexKind], + limit: 16, + until: before, + }, + { + groupable: false, + skipVerification: false, + skipValidation: false, + }, + relaySet + ).withTimeout(2500); + eventSet = filterValidIndexEvents(eventSet); + const eventArray = Array.from(eventSet); + if (eventArray.length > 0) { + allEvents = allEvents.concat(eventArray); + relayStatuses = { ...relayStatuses, [relay]: 'found' }; + } else { + relayStatuses = { ...relayStatuses, [relay]: 'notfound' }; + } + } catch { + relayStatuses = { ...relayStatuses, [relay]: 'notfound' }; + } + }) ); - eventSet = filterValidIndexEvents(eventSet); - - let eventArray = Array.from(eventSet); - eventArray?.sort((a, b) => b.created_at! - a.created_at!); - - if (!eventArray) { - return; - } - - endOfFeed = eventArray?.at(eventArray.length - 1)?.id === eventsInView?.at(eventsInView.length - 1)?.id; - - if (endOfFeed) { - return; - } - - const eventMap = new Map([...eventsInView, ...eventArray].map(event => [event.tagAddress(), event])); - const allEvents = Array.from(eventMap.values()); - const uniqueIds = new Set(allEvents.map(event => event.tagAddress())); - eventsInView = Array.from(uniqueIds) - .map(id => eventMap.get(id)) - .filter(event => event != null) as NDKEvent[]; - } catch (err) { - console.error('Error fetching events:', err); } + // Deduplicate and sort + const eventMap = new Map([...eventsInView, ...allEvents].map(event => [event.tagAddress(), event])); + const uniqueEvents = Array.from(eventMap.values()); + uniqueEvents.sort((a, b) => b.created_at! - a.created_at!); + eventsInView = uniqueEvents; + endOfFeed = false; // Could add logic to detect end + loading = false; } const getSkeletonIds = (): string[] => { const skeletonHeight = 124; // The height of the skeleton component in pixels. - - // Determine the number of skeletons to display based on the height of the screen. const skeletonCount = Math.floor(window.innerHeight / skeletonHeight) - 2; - const skeletonIds = []; for (let i = 0; i < skeletonCount; i++) { skeletonIds.push(`skeleton-${i}`); @@ -99,7 +122,12 @@
- {#if eventsInView.length === 0} +
+ {#each Object.entries(relayStatuses) as [relay, status]} + {relay}: {status} + {/each} +
+ {#if loading && eventsInView.length === 0} {#each getSkeletonIds() as id} {/each} diff --git a/src/lib/components/PublicationHeader.svelte b/src/lib/components/PublicationHeader.svelte index dc29d47..9e71f4e 100644 --- a/src/lib/components/PublicationHeader.svelte +++ b/src/lib/components/PublicationHeader.svelte @@ -24,7 +24,7 @@ ); let title: string = $derived(event.getMatchingTags('title')[0]?.[1]); - let author: string = $derived(event.getMatchingTags('author')[0]?.[1] ?? 'unknown'); + let author: string = $derived(getMatchingTags(event, 'author')[0]?.[1] ?? 'unknown'); let version: string = $derived(event.getMatchingTags('version')[0]?.[1] ?? '1'); let image: string = $derived(event.getMatchingTags('image')[0]?.[1] ?? null); let authorPubkey: string = $derived(event.getMatchingTags('p')[0]?.[1] ?? null); diff --git a/src/lib/components/PublicationSection.svelte b/src/lib/components/PublicationSection.svelte index de23463..6c2586a 100644 --- a/src/lib/components/PublicationSection.svelte +++ b/src/lib/components/PublicationSection.svelte @@ -5,6 +5,7 @@ import { TextPlaceholder } from "flowbite-svelte"; import { getContext } from "svelte"; import type { Asciidoctor, Document } from "asciidoctor"; + import { getMatchingTags } from '$lib/utils/nostrUtils'; let { address, @@ -109,7 +110,7 @@ {:then [leafTitle, leafContent, leafHierarchy, publicationType, divergingBranches]} {#each divergingBranches as [branch, depth]} - {@render sectionHeading(branch.getMatchingTags('title')[0]?.[1] ?? '', depth)} + {@render sectionHeading(getMatchingTags(branch, 'title')[0]?.[1] ?? '', depth)} {/each} {#if leafTitle} {@const leafDepth = leafHierarchy.length - 1} diff --git a/src/lib/components/RelayActions.svelte b/src/lib/components/RelayActions.svelte new file mode 100644 index 0000000..666b872 --- /dev/null +++ b/src/lib/components/RelayActions.svelte @@ -0,0 +1,243 @@ + + +
+ + + {#if $ndkInstance?.activeUser} + + {/if} +
+ +{#if foundRelays.length > 0} +
+ Found on {foundRelays.length} relay(s): +
+ {#each foundRelays as relay} + + {/each} +
+
+{/if} + +{#if broadcastSuccess} +
+ Event broadcast successfully to: +
+ {#each getConnectedRelays() as relay} + + {/each} +
+
+{/if} + +{#if broadcastError} +
+ {broadcastError} +
+{/if} + +
+ Found on: +
+ {#each getEventRelays(event) as relay} + + {/each} +
+
+ +{#if showRelayModal} +
+
+ +

Relay Search Results

+
+ {#each Object.entries({ + 'Standard Relays': standardRelays, + 'User Relays': Array.from($ndkInstance?.pool?.relays.values() || []).map(r => r.url), + 'Fallback Relays': fallbackRelays + }) as [groupName, groupRelays]} + {#if groupRelays.length > 0} +
+

+ {groupName} +

+ {#each groupRelays as relay} + + {/each} +
+ {/if} + {/each} +
+
+ +
+
+
+{/if} \ No newline at end of file diff --git a/src/lib/components/RelayDisplay.svelte b/src/lib/components/RelayDisplay.svelte new file mode 100644 index 0000000..1161f7c --- /dev/null +++ b/src/lib/components/RelayDisplay.svelte @@ -0,0 +1,59 @@ + + + + +
+ relay icon { (e.target as HTMLImageElement).src = '/favicon.png'; }} + /> + {relay} + {#if showStatus && status} + {#if status === 'pending'} + + + + + {:else if status === 'found'} + + {:else} + + {/if} + {/if} +
\ No newline at end of file diff --git a/src/lib/components/blog/BlogHeader.svelte b/src/lib/components/blog/BlogHeader.svelte index e264a3e..a91d0a4 100644 --- a/src/lib/components/blog/BlogHeader.svelte +++ b/src/lib/components/blog/BlogHeader.svelte @@ -10,7 +10,7 @@ const { rootId, event, onBlogUpdate, active = true } = $props<{ rootId: string, event: NDKEvent, onBlogUpdate?: any, active: boolean }>(); let title: string = $derived(event.getMatchingTags('title')[0]?.[1]); - let author: string = $derived(event.getMatchingTags('author')[0]?.[1] ?? 'unknown'); + let author: string = $derived(getMatchingTags(event, 'author')[0]?.[1] ?? 'unknown'); let image: string = $derived(event.getMatchingTags('image')[0]?.[1] ?? null); let authorPubkey: string = $derived(event.getMatchingTags('p')[0]?.[1] ?? null); let hashtags: string = $derived(event.getMatchingTags('t') ?? null); diff --git a/src/lib/components/util/ArticleNav.svelte b/src/lib/components/util/ArticleNav.svelte index 8d1ee40..b6ac5d7 100644 --- a/src/lib/components/util/ArticleNav.svelte +++ b/src/lib/components/util/ArticleNav.svelte @@ -17,7 +17,7 @@ }>(); let title: string = $derived(indexEvent.getMatchingTags('title')[0]?.[1]); - let author: string = $derived(indexEvent.getMatchingTags('author')[0]?.[1] ?? 'unknown'); + let author: string = $derived(indexgetMatchingTags(event, 'author')[0]?.[1] ?? 'unknown'); let pubkey: string = $derived(indexEvent.getMatchingTags('p')[0]?.[1] ?? null); let isLeaf: boolean = $derived(indexEvent.kind === 30041); diff --git a/src/lib/components/util/Details.svelte b/src/lib/components/util/Details.svelte index f228e2e..e776e9d 100644 --- a/src/lib/components/util/Details.svelte +++ b/src/lib/components/util/Details.svelte @@ -3,25 +3,26 @@ import CardActions from "$components/util/CardActions.svelte"; import Interactions from "$components/util/Interactions.svelte"; import { P } from "flowbite-svelte"; + import { getMatchingTags } from '$lib/utils/nostrUtils'; // isModal // - don't show interactions in modal view // - don't show all the details when _not_ in modal view let { event, isModal = false } = $props(); - let title: string = $derived(event.getMatchingTags('title')[0]?.[1]); - let author: string = $derived(event.getMatchingTags('author')[0]?.[1] ?? 'unknown'); - let version: string = $derived(event.getMatchingTags('version')[0]?.[1] ?? '1'); - let image: string = $derived(event.getMatchingTags('image')[0]?.[1] ?? null); - let originalAuthor: string = $derived(event.getMatchingTags('p')[0]?.[1] ?? null); - let summary: string = $derived(event.getMatchingTags('summary')[0]?.[1] ?? null); - let type: string = $derived(event.getMatchingTags('type')[0]?.[1] ?? null); - let language: string = $derived(event.getMatchingTags('l')[0]?.[1] ?? null); - let source: string = $derived(event.getMatchingTags('source')[0]?.[1] ?? null); - let publisher: string = $derived(event.getMatchingTags('published_by')[0]?.[1] ?? null); - let identifier: string = $derived(event.getMatchingTags('i')[0]?.[1] ?? null); - let hashtags: [] = $derived(event.getMatchingTags('t') ?? []); - let rootId: string = $derived(event.getMatchingTags('d')[0]?.[1] ?? null); + let title: string = $derived(getMatchingTags(event, 'title')[0]?.[1]); + let author: string = $derived(getMatchingTags(event, 'author')[0]?.[1] ?? 'unknown'); + let version: string = $derived(getMatchingTags(event, 'version')[0]?.[1] ?? '1'); + let image: string = $derived(getMatchingTags(event, 'image')[0]?.[1] ?? null); + let originalAuthor: string = $derived(getMatchingTags(event, 'p')[0]?.[1] ?? null); + let summary: string = $derived(getMatchingTags(event, 'summary')[0]?.[1] ?? null); + let type: string = $derived(getMatchingTags(event, 'type')[0]?.[1] ?? null); + let language: string = $derived(getMatchingTags(event, 'l')[0]?.[1] ?? null); + let source: string = $derived(getMatchingTags(event, 'source')[0]?.[1] ?? null); + let publisher: string = $derived(getMatchingTags(event, 'published_by')[0]?.[1] ?? null); + let identifier: string = $derived(getMatchingTags(event, 'i')[0]?.[1] ?? null); + let hashtags: string[] = $derived(getMatchingTags(event, 't').map(tag => tag[1])); + let rootId: string = $derived(getMatchingTags(event, 'd')[0]?.[1] ?? null); let kind = $derived(event.kind); @@ -67,7 +68,7 @@ {#if hashtags.length}
{#each hashtags as tag} - #{tag[1]} + #{tag} {/each}
{/if} diff --git a/src/lib/navigator/EventNetwork/NodeTooltip.svelte b/src/lib/navigator/EventNetwork/NodeTooltip.svelte index 4aedc8e..2f8d577 100644 --- a/src/lib/navigator/EventNetwork/NodeTooltip.svelte +++ b/src/lib/navigator/EventNetwork/NodeTooltip.svelte @@ -7,6 +7,7 @@ @@ -264,190 +53,10 @@ Use this page to view any event (npub, nprofile, nevent, naddr, note, pubkey, or eventID).

-
- e.key === 'Enter' && searchEvent()} - /> - -
- - {#if error} - - {/if} - - {#if event && typeof event.getMatchingTags === 'function'} -
- -
- {neventEncode(event, standardRelays)} -
- - -
- {#if event.kind !== 0 && getEventTitle(event)} -

{getEventTitle(event)}

- {:else if event.kind === 0 && profile && profile.name} -

{profile.name}

- {/if} -
- {#if toNpub(event.pubkey)} - Author: {@render userBadge(toNpub(event.pubkey) as string, profile?.display_name || event.pubkey)} - {:else} - Author: {profile?.display_name || event.pubkey} - {/if} -
-
- Kind: - {event.kind} - ({getEventTypeDisplay(event)}) -
- {#if getEventSummary(event)} -
- Summary: -

{getEventSummary(event)}

-
- {/if} - {#if getEventHashtags(event).length} -
- Tags: -
- {#each getEventHashtags(event) as tag} - #{tag} - {/each} -
-
- {/if} - - -
- Content: - {#if event.kind === 0} - {#if profile} -
-
- {#if profile.name} -
-
Name:
-
{profile.name}
-
- {/if} - {#if profile.display_name} -
-
Display Name:
-
{profile.display_name}
-
- {/if} - {#if profile.about} -
-
About:
-
{profile.about}
-
- {/if} - {#if profile.picture} -
-
Picture:
-
- Profile -
-
- {/if} - {#if profile.banner} -
-
Banner:
-
- Banner -
-
- {/if} - {#if profile.website} -
-
Website:
-
- {profile.website} -
-
- {/if} - {#if profile.lud16} -
-
Lightning Address:
-
{profile.lud16}
-
- {/if} - {#if profile.nip05} -
-
NIP-05:
-
{profile.nip05}
-
- {/if} -
-
- {:else} -
{event.content}
- {/if} - {:else} -
- {@html showFullContent ? parsedContent : contentPreview} - {#if !showFullContent && parsedContent.length > 250} - - {/if} -
- {/if} -
- - - {#if event.tags && event.tags.length} -
- Event Tags: -
- {#each event.tags as tag} - {@html renderTag(tag)} - {/each} -
-
- {/if} - - -
- - Show Raw Event JSON - -
-{JSON.stringify(event.rawEvent(), null, 2)}
-            
-
-
-
- {#if !getEventTitle(event) && !event.content} -
- No title or content available for this event. -
-            {JSON.stringify(event.rawEvent(), null, 2)}
-          
-
- {/if} - {:else if event} -
Fetched event is not a valid NDKEvent. See console for details.
+ + {#if event} + + {/if}
diff --git a/src/routes/publication/+page.ts b/src/routes/publication/+page.ts index e3d6bf5..b100f70 100644 --- a/src/routes/publication/+page.ts +++ b/src/routes/publication/+page.ts @@ -3,6 +3,7 @@ import type { Load } from '@sveltejs/kit'; import type { NDKEvent } from '@nostr-dev-kit/ndk'; import { nip19 } from 'nostr-tools'; import { getActiveRelays } from '$lib/ndk'; +import { getMatchingTags } from '$lib/utils/nostrUtils'; /** * Decodes an naddr identifier and returns a filter object @@ -96,7 +97,7 @@ export const load: Load = async ({ url, parent }: { url: URL; parent: () => Prom ? await fetchEventById(ndk, id) : await fetchEventByDTag(ndk, dTag!); - const publicationType = indexEvent?.getMatchingTags('type')[0]?.[1]; + const publicationType = getMatchingTags(indexEvent, 'type')[0]?.[1]; const fetchPromise = parser.fetch(indexEvent); return {