Browse Source

Successful implementation of event search and userBadges.

master
Silberengel 10 months ago
parent
commit
8c88dc43c7
  1. 1
      .gitignore
  2. 2
      README.md
  3. 5713
      package-lock.json
  4. 7
      src/lib/components/Login.svelte
  5. 4
      src/lib/components/PublicationHeader.svelte
  6. 4
      src/lib/components/blog/BlogHeader.svelte
  7. 4
      src/lib/components/util/ArticleNav.svelte
  8. 8
      src/lib/components/util/CardActions.svelte
  9. 8
      src/lib/components/util/Details.svelte
  10. 191
      src/lib/components/util/InlineProfile.svelte
  11. 4
      src/lib/consts.ts
  12. 12
      src/lib/snippets/UserSnippets.svelte
  13. 22
      src/lib/utils/nostrUtils.ts
  14. 1
      src/routes/[...catchall]/+page.svelte
  15. 6
      src/routes/about/+page.svelte
  16. 86
      src/routes/events/+page.svelte
  17. 2
      test_data/AsciidocFiles/21lessons.adoc
  18. 2
      test_data/AsciidocFiles/Rauhnaechte.adoc
  19. 4
      tests/integration/markupIntegration.test.ts
  20. 8
      tests/integration/markupTestfile.md

1
.gitignore vendored

@ -8,7 +8,6 @@ node_modules
!.env.example !.env.example
vite.config.js.timestamp-* vite.config.js.timestamp-*
vite.config.ts.timestamp-* vite.config.ts.timestamp-*
package-lock.json
# tests # tests
/tests/e2e/html-report/*.html /tests/e2e/html-report/*.html

2
README.md

@ -120,4 +120,4 @@ npx playwright test
## Markup Support ## Markup Support
Alexandria supports both Markdown and AsciiDoc markup for different content types. For a detailed list of supported tags and features in the basic and advanced markdown parsers, as well as information about AsciiDoc usage for publications and wikis, see [MarkupInfo.md](https://next-alexandria.gitcitadel.eu/src/lib/utils/markup/MarkupInfo.md). Alexandria supports both Markdown and AsciiDoc markup for different content types. For a detailed list of supported tags and features in the basic and advanced markdown parsers, as well as information about AsciiDoc usage for publications and wikis, see [MarkupInfo.md](./src/lib/utils/markup/MarkupInfo.md).

5713
package-lock.json generated

File diff suppressed because it is too large Load Diff

7
src/lib/components/Login.svelte

@ -1,13 +1,10 @@
<script lang='ts'> <script lang='ts'>
import { type NDKUserProfile } from '@nostr-dev-kit/ndk'; import { type NDKUserProfile } from '@nostr-dev-kit/ndk';
import { activePubkey, loginWithExtension, logout, ndkInstance, ndkSignedIn, persistLogin } from '$lib/ndk'; import { activePubkey, loginWithExtension, ndkInstance, ndkSignedIn, persistLogin } from '$lib/ndk';
import { Avatar, Button, Popover, Tooltip } from 'flowbite-svelte'; import { Avatar, Button, Popover } from 'flowbite-svelte';
import Profile from "$components/util/Profile.svelte"; import Profile from "$components/util/Profile.svelte";
let profile = $state<NDKUserProfile | null>(null); let profile = $state<NDKUserProfile | null>(null);
let pfp = $derived(profile?.image);
let username = $derived(profile?.name);
let tag = $derived(profile?.name);
let npub = $state<string | undefined >(undefined); let npub = $state<string | undefined >(undefined);
let signInFailed = $state<boolean>(false); let signInFailed = $state<boolean>(false);

4
src/lib/components/PublicationHeader.svelte

@ -5,7 +5,7 @@
import { standardRelays } from '../consts'; import { standardRelays } from '../consts';
import { Card, Img } from "flowbite-svelte"; import { Card, Img } from "flowbite-svelte";
import CardActions from "$components/util/CardActions.svelte"; import CardActions from "$components/util/CardActions.svelte";
import InlineProfile from "$components/util/InlineProfile.svelte"; import { userBadge } from "$lib/snippets/UserSnippets.svelte";
const { event } = $props<{ event: NDKEvent }>(); const { event } = $props<{ event: NDKEvent }>();
@ -46,7 +46,7 @@
<h3 class='text-base font-normal'> <h3 class='text-base font-normal'>
by by
{#if authorPubkey != null} {#if authorPubkey != null}
<InlineProfile pubkey={authorPubkey} name={author} /> {@render userBadge(authorPubkey, author)}
{:else} {:else}
{author} {author}
{/if} {/if}

4
src/lib/components/blog/BlogHeader.svelte

@ -2,7 +2,7 @@
import type { NDKEvent } from '@nostr-dev-kit/ndk'; import type { NDKEvent } from '@nostr-dev-kit/ndk';
import { scale } from 'svelte/transition'; import { scale } from 'svelte/transition';
import { Card, Img } from "flowbite-svelte"; import { Card, Img } from "flowbite-svelte";
import InlineProfile from "$components/util/InlineProfile.svelte"; import { userBadge } from "$lib/snippets/UserSnippets.svelte";
import Interactions from "$components/util/Interactions.svelte"; import Interactions from "$components/util/Interactions.svelte";
import { quintOut } from "svelte/easing"; import { quintOut } from "svelte/easing";
import CardActions from "$components/util/CardActions.svelte"; import CardActions from "$components/util/CardActions.svelte";
@ -38,7 +38,7 @@
<div class='space-y-4'> <div class='space-y-4'>
<div class="flex flex-row justify-between my-2"> <div class="flex flex-row justify-between my-2">
<div class="flex flex-col"> <div class="flex flex-col">
<InlineProfile pubkey={authorPubkey} name={author} /> {@render userBadge(authorPubkey, author)}
<span class='text-gray-500'>{publishedAt()}</span> <span class='text-gray-500'>{publishedAt()}</span>
</div> </div>
<CardActions event={event} /> <CardActions event={event} />

4
src/lib/components/util/ArticleNav.svelte

@ -2,7 +2,7 @@
import { BookOutline, CaretLeftOutline, CloseOutline, GlobeOutline } from "flowbite-svelte-icons"; import { BookOutline, CaretLeftOutline, CloseOutline, GlobeOutline } from "flowbite-svelte-icons";
import { Button } from "flowbite-svelte"; import { Button } from "flowbite-svelte";
import { publicationColumnVisibility } from "$lib/stores"; import { publicationColumnVisibility } from "$lib/stores";
import InlineProfile from "$components/util/InlineProfile.svelte"; import { userBadge } from "$lib/snippets/UserSnippets.svelte";
import type { NDKEvent } from "@nostr-dev-kit/ndk"; import type { NDKEvent } from "@nostr-dev-kit/ndk";
import { onDestroy, onMount } from "svelte"; import { onDestroy, onMount } from "svelte";
@ -131,7 +131,7 @@
{/if} {/if}
</div> </div>
<div class="flex flex-grow text justify-center items-center"> <div class="flex flex-grow text justify-center items-center">
<p class="max-w-[60vw] line-ellipsis"><b class="text-nowrap">{title}</b> <span class="whitespace-nowrap">by <InlineProfile pubkey={pubkey} name={author} /></span></p> <p class="max-w-[60vw] line-ellipsis"><b class="text-nowrap">{title}</b> <span class="whitespace-nowrap">by {@render userBadge(pubkey, author)}</span></p>
</div> </div>
<div class="flex justify-end items-center space-x-2 md:min-w-52 min-w-8"> <div class="flex justify-end items-center space-x-2 md:min-w-52 min-w-8">
{#if $publicationColumnVisibility.inner} {#if $publicationColumnVisibility.inner}

8
src/lib/components/util/CardActions.svelte

@ -8,7 +8,7 @@
import { Button, Modal, Popover } from "flowbite-svelte"; import { Button, Modal, Popover } from "flowbite-svelte";
import { standardRelays, FeedType } from "$lib/consts"; import { standardRelays, FeedType } from "$lib/consts";
import { neventEncode, naddrEncode } from "$lib/utils"; import { neventEncode, naddrEncode } from "$lib/utils";
import InlineProfile from "$components/util/InlineProfile.svelte"; import { userBadge } from "$lib/snippets/UserSnippets.svelte";
import { feedType } from "$lib/stores"; import { feedType } from "$lib/stores";
import { inboxRelays, ndkSignedIn } from "$lib/ndk"; import { inboxRelays, ndkSignedIn } from "$lib/ndk";
import type { NDKEvent } from "@nostr-dev-kit/ndk"; import type { NDKEvent } from "@nostr-dev-kit/ndk";
@ -83,7 +83,7 @@
function getIdentifier(type: 'nevent' | 'naddr'): string { function getIdentifier(type: 'nevent' | 'naddr'): string {
const encodeFn = type === 'nevent' ? neventEncode : naddrEncode; const encodeFn = type === 'nevent' ? neventEncode : naddrEncode;
const identifier = encodeFn(event, activeRelays); const identifier = encodeFn(event, activeRelays);
console.debug(`[CardActions] ${type} identifier for event ${event.id}:`, identifier); console.debug("[CardActions] ${type} identifier for event ${event.id}:", identifier);
return identifier; return identifier;
} }
@ -165,7 +165,7 @@
<h1 class="text-3xl font-bold mt-5">{title || 'Untitled'}</h1> <h1 class="text-3xl font-bold mt-5">{title || 'Untitled'}</h1>
<h2 class="text-base font-bold">by <h2 class="text-base font-bold">by
{#if originalAuthor} {#if originalAuthor}
<InlineProfile pubkey={originalAuthor} /> {@render userBadge(originalAuthor, author)}
{:else} {:else}
{author || 'Unknown'} {author || 'Unknown'}
{/if} {/if}
@ -183,7 +183,7 @@
{/if} {/if}
<div class="flex flex-row"> <div class="flex flex-row">
<h4 class='text-base font-normal mt-2'>Index author: <InlineProfile pubkey={event.pubkey} /></h4> <h4 class='text-base font-normal mt-2'>Index author: {@render userBadge(event.pubkey, author)}</h4>
</div> </div>
<div class="flex flex-col pb-4 space-y-1"> <div class="flex flex-col pb-4 space-y-1">

8
src/lib/components/util/Details.svelte

@ -1,5 +1,5 @@
<script lang="ts"> <script lang="ts">
import InlineProfile from "$components/util/InlineProfile.svelte"; import { userBadge } from "$lib/snippets/UserSnippets.svelte";
import CardActions from "$components/util/CardActions.svelte"; import CardActions from "$components/util/CardActions.svelte";
import Interactions from "$components/util/Interactions.svelte"; import Interactions from "$components/util/Interactions.svelte";
import { P } from "flowbite-svelte"; import { P } from "flowbite-svelte";
@ -31,7 +31,7 @@
<div class="flex flex-col relative mb-2"> <div class="flex flex-col relative mb-2">
{#if !isModal} {#if !isModal}
<div class="flex flex-row justify-between items-center"> <div class="flex flex-row justify-between items-center">
<P class='text-base font-normal'><InlineProfile pubkey={event.pubkey} /></P> <P class='text-base font-normal'>{@render userBadge(event.pubkey, author)}</P>
<CardActions event={event}></CardActions> <CardActions event={event}></CardActions>
</div> </div>
{/if} {/if}
@ -46,7 +46,7 @@
<h2 class="text-base font-bold"> <h2 class="text-base font-bold">
by by
{#if originalAuthor !== null} {#if originalAuthor !== null}
<InlineProfile pubkey={originalAuthor} name={author} /> {@render userBadge(originalAuthor, author)}
{:else} {:else}
{author} {author}
{/if} {/if}
@ -80,7 +80,7 @@
{:else} {:else}
<span>Author:</span> <span>Author:</span>
{/if} {/if}
<InlineProfile pubkey={event.pubkey} /> {@render userBadge(event.pubkey, author)}
</h4> </h4>
</div> </div>

191
src/lib/components/util/InlineProfile.svelte

@ -1,60 +1,183 @@
<script lang='ts'> <script lang='ts'>
import { Avatar } from 'flowbite-svelte'; import { Avatar } from 'flowbite-svelte';
import { type NDKUserProfile } from "@nostr-dev-kit/ndk"; import NDK, { type NDKUserProfile } from "@nostr-dev-kit/ndk";
import { ndkInstance } from '$lib/ndk'; import { ndkInstance } from '$lib/ndk';
import { userBadge } from '$lib/snippets/UserSnippets.svelte'; import { userBadge } from '$lib/snippets/UserSnippets.svelte';
let { pubkey, name = null } = $props();
const externalProfileDestination = './events?id=' // Component configuration types
type AvatarSize = 'sm' | 'md' | 'lg';
let loading = $state(true); // Component props interface
let anon = $state(false); interface $$Props {
let npub = $state(''); pubkey: string; // Required: The Nostr public key of the user
name?: string | null; // Optional: Display name override
showAvatar?: boolean; // Optional: Whether to show the avatar (default: true)
avatarSize?: AvatarSize; // Optional: Size of the avatar (default: 'md')
}
// Destructure and set default props
let {
pubkey,
name = null,
showAvatar = true,
avatarSize = 'md' as AvatarSize
} = $props();
console.log('[InlineProfile] Initialized with props:', {
pubkey,
name,
showAvatar,
avatarSize
});
// Constants
const EXTERNAL_PROFILE_DESTINATION = './events?id=';
// Component state type definition
type ProfileState = {
loading: boolean; // Whether we're currently loading the profile
error: string | null; // Any error that occurred during loading
profile: NDKUserProfile | null; // The user's profile data
npub: string; // The user's npub (bech32 encoded pubkey)
};
// Initialize component state
let state = $state<ProfileState>({
loading: true,
error: null,
profile: null,
npub: ''
});
// Derived values from state
const pfp = $derived(state.profile?.image);
const username = $derived(state.profile?.name);
const isAnonymous = $derived(!state.profile?.name && !name);
// Log derived values reactively when they change
$effect(() => {
console.log('[InlineProfile] Derived values updated:', {
pfp,
username,
isAnonymous,
hasProfile: !!state.profile,
hasNpub: !!state.npub,
profileState: {
loading: state.loading,
error: state.error,
hasProfile: !!state.profile,
npub: state.npub
}
});
});
let profile = $state<NDKUserProfile | null>(null); // Avatar size classes mapping
let pfp = $derived(profile?.image); const avatarClasses: Record<AvatarSize, string> = {
let username = $derived(profile?.name); sm: 'h-5 w-5',
md: 'h-7 w-7',
lg: 'h-9 w-9'
};
/**
* Fetches user data from NDK
* @param pubkey - The Nostr public key to fetch data for
*/
async function fetchUserData(pubkey: string) { async function fetchUserData(pubkey: string) {
let user; console.log('[InlineProfile] fetchUserData called with pubkey:', pubkey);
user = $ndkInstance
.getUser({ pubkey: pubkey ?? undefined }); if (!pubkey) {
console.warn('[InlineProfile] No pubkey provided to fetchUserData');
state.error = 'No pubkey provided';
state.loading = false;
return;
}
try {
console.log('[InlineProfile] Getting NDK instance');
const ndk = $ndkInstance as NDK;
npub = user.npub; console.log('[InlineProfile] Creating NDK user object');
const user = ndk.getUser({ pubkey });
user.fetchProfile() console.log('[InlineProfile] Getting npub');
.then(userProfile => { state.npub = user.npub;
profile = userProfile; console.log('[InlineProfile] Got npub:', state.npub);
if (!profile?.name) anon = true;
loading = false; console.log('[InlineProfile] Fetching user profile');
const userProfile = await user.fetchProfile();
console.log('[InlineProfile] Got user profile:', {
name: userProfile?.name,
displayName: userProfile?.displayName,
nip05: userProfile?.nip05,
hasImage: !!userProfile?.image
}); });
state.profile = userProfile;
state.loading = false;
} catch (error) {
console.error('[InlineProfile] Error fetching user data:', error);
state.error = error instanceof Error ? error.message : 'Failed to fetch profile';
state.loading = false;
}
} }
// Fetch data when component mounts /**
* Shortens an npub string for display
* @param long - The npub string to shorten
* @returns Shortened npub string
*/
function shortenNpub(long: string | undefined): string {
if (!long) return '';
const shortened = `${long.slice(0, 8)}…${long.slice(-4)}`;
console.log('[InlineProfile] Shortened npub:', { original: long, shortened });
return shortened;
}
// Effect to fetch user data when pubkey changes
$effect(() => { $effect(() => {
console.log('[InlineProfile] Effect triggered, pubkey:', pubkey);
if (pubkey) { if (pubkey) {
fetchUserData(pubkey); fetchUserData(pubkey);
} else {
console.warn('[InlineProfile] No pubkey available for effect');
} }
}); });
function shortenNpub(long: string|undefined) {
if (!long) return '';
return long.slice(0, 8) + '…' + long.slice(-4);
}
</script> </script>
{#if loading} <!-- Component Template -->
{#if state.loading}
<!-- Loading state -->
<span class="animate-pulse" title="Loading profile...">
{name ?? '…'} {name ?? '…'}
{:else if anon } </span>
{@render userBadge(npub, username)} {:else if state.error}
{:else if npub } <!-- Error state -->
<a href={externalProfileDestination + npub} title={name ?? username}> <span class="text-red-500" title={state.error}>
<Avatar rounded {name ?? shortenNpub(pubkey)}
class='h-7 w-7 mx-1 cursor-pointer inline bg-transparent' </span>
{:else if isAnonymous}
<!-- Anonymous user state -->
{@render userBadge(pubkey, name)}
{:else if state.npub}
<!-- Authenticated user with profile -->
<a
href={EXTERNAL_PROFILE_DESTINATION + state.npub}
title={name ?? username}
class="inline-flex items-center hover:opacity-80 transition-opacity"
>
{#if showAvatar}
<Avatar
rounded
class={`${avatarClasses[avatarSize]} mx-1 cursor-pointer inline bg-transparent`}
src={pfp} src={pfp}
alt={username} /> alt={username ?? 'User avatar'}
{@render userBadge(npub, username)} />
{/if}
{@render userBadge(pubkey, name)}
</a> </a>
{:else} {:else}
{name ?? pubkey} <!-- Fallback state -->
<span title="No profile data available">
{name ?? shortenNpub(pubkey)}
</span>
{/if} {/if}

4
src/lib/consts.ts

@ -9,7 +9,9 @@ export const fallbackRelays = [
'wss://relay.noswhere.com', 'wss://relay.noswhere.com',
'wss://relay.damus.io', 'wss://relay.damus.io',
'wss://relay.nostr.band', 'wss://relay.nostr.band',
'wss://relay.lumina.rocks' 'wss://relay.lumina.rocks',
'wss://nostr.wine',
'wss://nostr.land'
]; ];
export enum FeedType { export enum FeedType {

12
src/lib/snippets/UserSnippets.svelte

@ -1,15 +1,19 @@
<script module lang='ts'> <script module lang='ts'>
import { createProfileLink, createProfileLinkWithVerification } from '$lib/utils/nostrUtils'; import { createProfileLink, createProfileLinkWithVerification, toNpub } from '$lib/utils/nostrUtils';
export { userBadge }; export { userBadge };
</script> </script>
{#snippet userBadge(identifier: string, displayText: string | undefined)} {#snippet userBadge(identifier: string, displayText: string | undefined)}
{#await createProfileLinkWithVerification(identifier, displayText)} {#if toNpub(identifier)}
{@html createProfileLink(identifier, displayText)} {#await createProfileLinkWithVerification(toNpub(identifier) as string, displayText)}
{@html createProfileLink(toNpub(identifier) as string, displayText)}
{:then html} {:then html}
{@html html} {@html html}
{:catch} {:catch}
{@html createProfileLink(identifier, displayText)} {@html createProfileLink(toNpub(identifier) as string, displayText)}
{/await} {/await}
{:else}
{displayText ?? ''}
{/if}
{/snippet} {/snippet}

22
src/lib/utils/nostrUtils.ts

@ -105,7 +105,7 @@ export function createProfileLink(identifier: string, displayText: string | unde
const defaultText = `${cleanId.slice(0, 8)}...${cleanId.slice(-4)}`; const defaultText = `${cleanId.slice(0, 8)}...${cleanId.slice(-4)}`;
const escapedText = escapeHtml(displayText || defaultText); const escapedText = escapeHtml(displayText || defaultText);
return `<a href=""./events?id=${escapedId}"" class="npub-badge" target="_blank">@${escapedText}</a>`; return `<a href="./events?id=${escapedId}" class="npub-badge" target="_blank">@${escapedText}</a>`;
} }
/** /**
@ -149,9 +149,9 @@ export async function createProfileLinkWithVerification(identifier: string, disp
const type = nip05.endsWith('edu') ? 'edu' : 'standard'; const type = nip05.endsWith('edu') ? 'edu' : 'standard';
switch (type) { switch (type) {
case 'edu': case 'edu':
return `<span class="npub-badge"><a href="https://njump.me/${escapedId}" target="_blank">@${displayIdentifier}</a>${graduationCapSvg}</span>`; return `<span class="npub-badge"><a href="./events?id=${escapedId}" target="_blank">@${displayIdentifier}</a>${graduationCapSvg}</span>`;
case 'standard': case 'standard':
return `<span class="npub-badge"><a href="https://njump.me/${escapedId}" target="_blank">@${displayIdentifier}</a>${badgeCheckSvg}</span>`; return `<span class="npub-badge"><a href="./events?id=${escapedId}" target="_blank">@${displayIdentifier}</a>${badgeCheckSvg}</span>`;
} }
} }
/** /**
@ -358,3 +358,19 @@ export async function fetchEventWithFallback(
return null; return null;
} }
} }
/**
* Converts a hex pubkey to npub, or returns npub if already encoded.
*/
export function toNpub(pubkey: string | undefined): string | null {
if (!pubkey) return null;
try {
if (/^[a-f0-9]{64}$/i.test(pubkey)) {
return nip19.npubEncode(pubkey);
}
if (pubkey.startsWith('npub1')) return pubkey;
return null;
} catch {
return null;
}
}

1
src/routes/[...catchall]/+page.svelte

@ -1,5 +1,4 @@
<script lang="ts"> <script lang="ts">
import { page } from '$app/stores';
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import { Button, P } from 'flowbite-svelte'; import { Button, P } from 'flowbite-svelte';
</script> </script>

6
src/routes/about/+page.svelte

@ -20,15 +20,15 @@
> >
{/if} {/if}
</div> </div>
<Img src="/screenshots/old_books.jpg" alt="Alexandria icon" /> <Img src="./screenshots/old_books.jpg" alt="Alexandria icon" />
<P class="mb-3"> <P class="mb-3">
Alexandria is a reader and writer for <A Alexandria is a reader and writer for <A
href="/publication?d=gitcitadel-project-documentation-curated-publications-specification-7-by-stella-v-1" href="./publication?d=gitcitadel-project-documentation-curated-publications-specification-7-by-stella-v-1"
>curated publications</A >curated publications</A
> (in Asciidoc), wiki pages (Asciidoc), and will eventually also support long-form > (in Asciidoc), wiki pages (Asciidoc), and will eventually also support long-form
articles (markup). It is produced by the <A articles (markup). It is produced by the <A
href="/publication?d=gitcitadel-project-documentation-gitcitadel-project-1-by-stella-v-1" href="./publication?d=gitcitadel-project-documentation-by-stella-v-1"
>GitCitadel project team</A >GitCitadel project team</A
>. >.
</P> </P>

86
src/routes/events/+page.svelte

@ -11,7 +11,8 @@
import { getMimeTags, getEventType } from "$lib/utils/mime"; import { getMimeTags, getEventType } from "$lib/utils/mime";
import { page } from "$app/stores"; import { page } from "$app/stores";
import { nip19 } from 'nostr-tools'; import { nip19 } from 'nostr-tools';
import InlineProfile from '$lib/components/util/InlineProfile.svelte'; import { userBadge } from "$lib/snippets/UserSnippets.svelte";
import { toNpub } from "$lib/utils/nostrUtils";
let searchQuery = $state(""); let searchQuery = $state("");
let event = $state<NDKEvent | null>(null); let event = $state<NDKEvent | null>(null);
@ -39,50 +40,72 @@
error = null; error = null;
event = null; event = null;
console.log('[Events] searchEvent called with query:', searchQuery); // Clean the query
let cleanedQuery = searchQuery.trim().replace(/^nostr:/, '');
let filterOrId: any = cleanedQuery;
console.log('[Events] Cleaned query:', cleanedQuery);
// If it's a 64-char hex, try as event id first, then as pubkey (profile)
if (/^[a-f0-9]{64}$/i.test(cleanedQuery)) {
// Try as event id
filterOrId = cleanedQuery;
event = await fetchEventWithFallback($ndkInstance, filterOrId, 10000);
if (!event) {
// Try as pubkey (profile event)
filterOrId = { kinds: [0], authors: [cleanedQuery] };
event = await fetchEventWithFallback($ndkInstance, filterOrId, 10000);
}
// ... handle not found, etc.
return;
} else if (/^(nevent|note|naddr|npub|nprofile)[a-z0-9]+$/i.test(cleanedQuery)) {
try { try {
let filterOrId: any = searchQuery.trim(); const decoded = nip19.decode(cleanedQuery);
// Try to decode bech32 (nevent, naddr, note, npub, nprofile)
if (/^(nevent|note|naddr|npub|nprofile)[a-z0-9]+$/i.test(searchQuery.trim())) {
try {
const decoded = nip19.decode(searchQuery.trim());
console.log('[Events] Decoded NIP-19:', decoded); console.log('[Events] Decoded NIP-19:', decoded);
if (decoded.type === 'nevent') { switch (decoded.type) {
case 'nevent':
filterOrId = decoded.data.id; filterOrId = decoded.data.id;
} else if (decoded.type === 'note') { break;
case 'note':
filterOrId = decoded.data; filterOrId = decoded.data;
} else if (decoded.type === 'naddr') { break;
case 'naddr':
filterOrId = { filterOrId = {
kinds: [decoded.data.kind], kinds: [decoded.data.kind],
authors: [decoded.data.pubkey], authors: [decoded.data.pubkey],
'#d': [decoded.data.identifier], '#d': [decoded.data.identifier],
}; };
} else if (decoded.type === 'nprofile') { break;
// Fetch kind 0 (profile) event for pubkey case 'nprofile':
filterOrId = { filterOrId = {
kinds: [0], kinds: [0],
authors: [decoded.data.pubkey], authors: [decoded.data.pubkey],
}; };
} else if (decoded.type === 'npub') { break;
// Fetch kind 0 (profile) event for pubkey case 'npub':
filterOrId = { filterOrId = {
kinds: [0], kinds: [0],
authors: [decoded.data], authors: [decoded.data],
}; };
break;
default:
filterOrId = cleanedQuery;
} }
console.log('[Events] Using filterOrId:', filterOrId); console.log('[Events] Using filterOrId:', filterOrId);
} catch (e) { } catch (e) {
console.error('[Events] Invalid Nostr identifier:', searchQuery, e); console.error('[Events] Invalid Nostr identifier:', cleanedQuery, e);
error = 'Invalid Nostr identifier.'; error = 'Invalid Nostr identifier.';
loading = false; loading = false;
return; return;
} }
} }
// Optionally: handle NIP-05 here if you want
// else if (cleanedQuery.includes('@')) { ... }
try {
// Use our new utility function to fetch the event // Use our new utility function to fetch the event
console.log('[Events] Fetching event with filterOrId:', filterOrId); console.log('Searching for event:', filterOrId);
event = await fetchEventWithFallback($ndkInstance, filterOrId); event = await fetchEventWithFallback($ndkInstance, filterOrId, 10000);
if (!event) { if (!event) {
console.warn('[Events] Event not found for filterOrId:', filterOrId); console.warn('[Events] Event not found for filterOrId:', filterOrId);
@ -103,19 +126,19 @@
if (eventType === 'addressable') { if (eventType === 'addressable') {
const dTag = event.getMatchingTags('d')[0]?.[1]; const dTag = event.getMatchingTags('d')[0]?.[1];
if (dTag) { if (dTag) {
return `/publication?id=${event.id}`; return './publication?id=${event.id}';
} }
} }
if (event.kind === 30818) { if (event.kind === 30818) {
return `/wiki?id=${event.id}`; return './wiki?id=${event.id}';
} }
const nevent = neventEncode(event, standardRelays); const nevent = neventEncode(event, standardRelays);
return `https://njump.me/${nevent}`; return './events?id=${nevent}';
} }
function getEventTypeDisplay(event: NDKEvent): string { function getEventTypeDisplay(event: NDKEvent): string {
const [mTag, MTag] = getMimeTags(event.kind || 0); const [mTag, MTag] = getMimeTags(event.kind || 0);
return MTag[1].split('/')[1] || `Event Kind ${event.kind}`; return MTag[1].split('/')[1] || 'Event Kind ${event.kind}';
} }
function getEventTitle(event: NDKEvent): string { function getEventTitle(event: NDKEvent): string {
@ -158,7 +181,7 @@
tags: [["d", dtag]], tags: [["d", dtag]],
}; };
const naddr = naddrEncode(fakeEvent as any, standardRelays); const naddr = naddrEncode(fakeEvent as any, standardRelays);
return `<a href='./events?id=${naddr}' class='text-primary-600 underline' target='_blank'>${match}</a>`; return "<a href='./events?id=${naddr}' class='text-primary-600 underline' target='_blank'>${match}</a>";
} catch { } catch {
return match; return match;
} }
@ -168,7 +191,7 @@
json = json.replace(EVENT_ID_REGEX, (match) => { json = json.replace(EVENT_ID_REGEX, (match) => {
try { try {
const nevent = neventEncode({ id: match, kind: 1 } as NDKEvent, standardRelays); const nevent = neventEncode({ id: match, kind: 1 } as NDKEvent, standardRelays);
return `<a href='./events?id=${nevent}' class='text-primary-600 underline' target='_blank'>${match}</a>`; return "<a href='./events?id=${nevent}' class='text-primary-600 underline' target='_blank'>${match}</a>";
} catch { } catch {
return match; return match;
} }
@ -184,12 +207,12 @@
if (tag[0] === 'a' && tag.length > 1) { if (tag[0] === 'a' && tag.length > 1) {
const [kind, pubkey, d] = tag[1].split(':'); const [kind, pubkey, d] = tag[1].split(':');
// Use type assertion as any to satisfy NDKEvent signature for naddrEncode // Use type assertion as any to satisfy NDKEvent signature for naddrEncode
return `<a href='/events?id=${naddrEncode({kind: +kind, pubkey, tags: [["d", d]], content: '', id: '', sig: ''} as any, standardRelays)}' class='underline text-primary-700'>a:${tag[1]}</a>`; return "<a href='/events?id=${naddrEncode({kind: +kind, pubkey, tags: [['d', d]], content: '', id: '', sig: ''} as any, standardRelays)}' class='underline text-primary-700'>a:${tag[1]}</a>";
} else if (tag[0] === 'e' && tag.length > 1) { } else if (tag[0] === 'e' && tag.length > 1) {
// Use type assertion as any to satisfy NDKEvent signature for neventEncode // Use type assertion as any to satisfy NDKEvent signature for neventEncode
return `<a href='/events?id=${neventEncode({id: tag[1], kind: 1, content: '', tags: [], pubkey: '', sig: ''} as any, standardRelays)}' class='underline text-primary-700'>e:${tag[1]}</a>`; return "<a href='/events?id=${neventEncode({id: tag[1], kind: 1, content: '', tags: [], pubkey: '', sig: ''} as any, standardRelays)}' class='underline text-primary-700'>e:${tag[1]}</a>";
} else { } else {
return `<span class='bg-primary-50 text-primary-800 px-2 py-1 rounded text-xs font-mono'>${tag[0]}:${tag[1]}</span>`; return "<span class='bg-primary-50 text-primary-800 px-2 py-1 rounded text-xs font-mono'>${tag[0]}:${tag[1]}</span>";
} }
} }
@ -238,7 +261,7 @@
</div> </div>
<P class="mb-3"> <P class="mb-3">
Use this page to view any event (npub, nprofile, nevent, naddr, or hexID). Use this page to view any event (npub, nprofile, nevent, naddr, note, pubkey, or eventID).
</P> </P>
<div class="flex gap-2"> <div class="flex gap-2">
@ -264,7 +287,7 @@
href={"https://njump.me/" + encodeURIComponent(searchQuery.trim())} href={"https://njump.me/" + encodeURIComponent(searchQuery.trim())}
target="_blank" target="_blank"
rel="noopener" rel="noopener"
>njump</a>. >Njump</a>.
</div> </div>
{/if} {/if}
</div> </div>
@ -285,8 +308,11 @@
<h2 class="text-2xl font-bold text-gray-900 dark:text-gray-100">{profile.name}</h2> <h2 class="text-2xl font-bold text-gray-900 dark:text-gray-100">{profile.name}</h2>
{/if} {/if}
<div class="flex items-center space-x-2"> <div class="flex items-center space-x-2">
<span class="text-gray-600 dark:text-gray-400">Author:</span> {#if toNpub(event.pubkey)}
<InlineProfile pubkey={event.pubkey} /> <span class="text-gray-600 dark:text-gray-400">Author: {@render userBadge(toNpub(event.pubkey) as string, profile?.display_name || event.pubkey)}</span>
{:else}
<span class="text-gray-600 dark:text-gray-400">Author: {profile?.display_name || event.pubkey}</span>
{/if}
</div> </div>
<div class="flex items-center space-x-2"> <div class="flex items-center space-x-2">
<span class="text-gray-600 dark:text-gray-400">Kind:</span> <span class="text-gray-600 dark:text-gray-400">Kind:</span>

2
test_data/AsciidocFiles/21lessons.adoc

@ -1372,7 +1372,7 @@ Thanks to the countless authors and content producers who influenced my thinking
Last but not least, thanks to all the bitcoin maximalists, shitcoin minimalists, shills, bots, and shitposters which reside in the beautiful garden that is Bitcoin twitter. And finally, thank _you_ for reading this. I hope you enjoyed it as much as I did enjoy writing it. Last but not least, thanks to all the bitcoin maximalists, shitcoin minimalists, shills, bots, and shitposters which reside in the beautiful garden that is Bitcoin twitter. And finally, thank _you_ for reading this. I hope you enjoyed it as much as I did enjoy writing it.
Feel free to reach out to me https://twitter.com/dergigi[on X] or https://njump.me/npub1dergggklka99wwrs92yz8wdjs952h2ux2ha2ed598ngwu9w7a6fsh9xzpc[on Nostr] nostr:npub1dergggklka99wwrs92yz8wdjs952h2ux2ha2ed598ngwu9w7a6fsh9xzpc. My DMs are open. Feel free to reach out to me https://twitter.com/dergigi[on X] or https://next-alexandria.gitcitadel.eu/events?id=npub1dergggklka99wwrs92yz8wdjs952h2ux2ha2ed598ngwu9w7a6fsh9xzpc[on Nostr] nostr:npub1dergggklka99wwrs92yz8wdjs952h2ux2ha2ed598ngwu9w7a6fsh9xzpc. My DMs are open.
== Thank You == Thank You

2
test_data/AsciidocFiles/Rauhnaechte.adoc

@ -157,7 +157,7 @@ Namesake: *Silvester*
The seventh Rauhnacht, associated with the month of July and the namesake _Silvester_, carries the theme of _Preparation for What’s to Come._ The seventh Rauhnacht, associated with the month of July and the namesake _Silvester_, carries the theme of _Preparation for What’s to Come._
It represents the gateway — the transition from a past phase into a new one. Since the introduction of the Gregorian calendar, this transition has been celebrated by many on December 31st, a day dedicated to the Roman bishop Silvester. His death commemorates the end of Christian persecution and the establishment of Christianity as a state religion. (For spiritual insights on Christ consciousness, I recommend https://njump.me/naddr1qvzqqqr4gupzp35mw8w9vn7ux59vmhle98e96usz4s28pjr53psgh4ke3epxhfmrqyvhwumn8ghj7un9d3shjtnndehhyapwwdhkx6tpdshszythwden5te0dehhxarj9emkjmn99uqp2d3ctaynzefcvex5vamxvf04sunzfu6nqdgtaz38t[this article I recently shared]). It represents the gateway — the transition from a past phase into a new one. Since the introduction of the Gregorian calendar, this transition has been celebrated by many on December 31st, a day dedicated to the Roman bishop Silvester. His death commemorates the end of Christian persecution and the establishment of Christianity as a state religion. (For spiritual insights on Christ consciousness, I recommend https://next-alexandria.gitcitadel.eu/events?id=naddr1qvzqqqr4gupzp35mw8w9vn7ux59vmhle98e96usz4s28pjr53psgh4ke3epxhfmrqyvhwumn8ghj7un9d3shjtnndehhyapwwdhkx6tpdshszythwden5te0dehhxarj9emkjmn99uqp2d3ctaynzefcvex5vamxvf04sunzfu6nqdgtaz38t[this article I recently shared]).
Every transition holds the opportunity to change, reshape, and perceive life with fresh eyes. Use this day to prepare for the new year: Every transition holds the opportunity to change, reshape, and perceive life with fresh eyes. Use this day to prepare for the new year:

4
tests/integration/markupIntegration.test.ts

@ -33,7 +33,7 @@ describe('Markup Integration Test', () => {
expect(output).toMatch(/<a[^>]+href="https:\/\/github.com\/nostrability\/nostrability\/issues\/146"/); expect(output).toMatch(/<a[^>]+href="https:\/\/github.com\/nostrability\/nostrability\/issues\/146"/);
// Hashtags // Hashtags
expect(output).toContain('text-primary-600'); expect(output).toContain('text-primary-600');
// Nostr identifiers (should be njump.me links) // Nostr identifiers (should be Alexandria links)
expect(output).toContain('./events?id=npub1l5sga6xg72phsz5422ykujprejwud075ggrr3z2hwyrfgr7eylqstegx9z'); expect(output).toContain('./events?id=npub1l5sga6xg72phsz5422ykujprejwud075ggrr3z2hwyrfgr7eylqstegx9z');
// Wikilinks // Wikilinks
expect(output).toContain('wikilink'); expect(output).toContain('wikilink');
@ -76,7 +76,7 @@ describe('Markup Integration Test', () => {
expect(output).toMatch(/<a[^>]+href="https:\/\/github.com\/nostrability\/nostrability\/issues\/146"/); expect(output).toMatch(/<a[^>]+href="https:\/\/github.com\/nostrability\/nostrability\/issues\/146"/);
// Hashtags // Hashtags
expect(output).toContain('text-primary-600'); expect(output).toContain('text-primary-600');
// Nostr identifiers (should be njump.me links) // Nostr identifiers (should be Alexandria links)
expect(output).toContain('./events?id=npub1l5sga6xg72phsz5422ykujprejwud075ggrr3z2hwyrfgr7eylqstegx9z'); expect(output).toContain('./events?id=npub1l5sga6xg72phsz5422ykujprejwud075ggrr3z2hwyrfgr7eylqstegx9z');
// Wikilinks // Wikilinks
expect(output).toContain('wikilink'); expect(output).toContain('wikilink');

8
tests/integration/markupTestfile.md

@ -111,16 +111,16 @@ https://upload.wikimedia.org/wikipedia/commons/f/f1/Heart_coraz%C3%B3n.svg
This is an implementation of [Nostr-flavored markup](https://github.com/nostrability/nostrability/issues/146) for #gitstuff issue notes. This is an implementation of [Nostr-flavored markup](https://github.com/nostrability/nostrability/issues/146) for #gitstuff issue notes.
You can even turn Alexandria URLs into embedded events, if they have hexids or bech32 addresses: You can even turn Alexandria URLs into embedded events, if they have hexids or bech32 addresses:
http://localhost:4173/publication?id=nevent1qqstjcyerjx4laxlxc70cwzuxf3u9kkzuhdhgtu8pwrzvh7k5d5zdngpzemhxue69uhhyetvv9ujumn0wd68ytnzv9hxgq3qm3xdppkd0njmrqe2ma8a6ys39zvgp5k8u22mev8xsnqp4nh80srq0ylvuw https://next-alexandria.gitcitadel.eu/events?id=nevent1qqstjcyerjx4laxlxc70cwzuxf3u9kkzuhdhgtu8pwrzvh7k5d5zdngpzemhxue69uhhyetvv9ujumn0wd68ytnzv9hxgq3qm3xdppkd0njmrqe2ma8a6ys39zvgp5k8u22mev8xsnqp4nh80srq0ylvuw
But not if they have d-tags: But not if they have d-tags:
http://next-alexandria.gitcitadel.eu/publication?d=relay-test-thecitadel-by-unknown-v-1 https://next-alexandria.gitcitadel.eu/publication?d=relay-test-thecitadel-by-unknown-v-1
And within a markup tag: [markup link title](http://alexandria.gitcitadel.com/publication?id=84ad65f7a321404f55d97c2208dd3686c41724e6c347d3ee53cfe16f67cdfb7c). And within a markup tag: [markup link title](https://next-alexandria.gitcitadel.com/publication?id=84ad65f7a321404f55d97c2208dd3686c41724e6c347d3ee53cfe16f67cdfb7c).
And to localhost: http://localhost:4173/publication?id=c36b54991e459221f444612d88ea94ef5bb4a1b93863ef89b1328996746f6d25 And to localhost: http://localhost:4173/publication?id=c36b54991e459221f444612d88ea94ef5bb4a1b93863ef89b1328996746f6d25
http://localhost:4173/profile?id=nprofile1qqs99d9qw67th0wr5xh05de4s9k0wjvnkxudkgptq8yg83vtulad30gxyk5sf https://next-alexandria.gitcitadel.eu/events?id=nprofile1qqs99d9qw67th0wr5xh05de4s9k0wjvnkxudkgptq8yg83vtulad30gxyk5sf
You can even include code inline, like `<div class="leather min-h-full w-full flex flex-col items-center">` or You can even include code inline, like `<div class="leather min-h-full w-full flex flex-col items-center">` or

Loading…
Cancel
Save