Browse Source

pass known event to /event page

master
Silberengel 4 weeks ago
parent
commit
2f7010a40d
  1. 2
      src/lib/components/EventMenu.svelte
  2. 2
      src/lib/components/content/EmbeddedEventBlurb.svelte
  3. 2
      src/lib/components/content/HighlightOverlay.svelte
  4. 2
      src/lib/components/content/QuotedContext.svelte
  5. 2
      src/lib/components/content/ReferencedEventPreview.svelte
  6. 2
      src/lib/components/content/ReplyContext.svelte
  7. 2
      src/lib/components/find/SearchAddressableEvents.svelte
  8. 25
      src/lib/components/layout/ProfileBadge.svelte
  9. 2
      src/lib/components/layout/SearchBox.svelte
  10. 2
      src/lib/components/layout/UnifiedSearch.svelte
  11. 2
      src/lib/components/profile/BookmarksPanel.svelte
  12. 2
      src/lib/components/profile/ProfileEventsPanel.svelte
  13. 2
      src/lib/components/write/CreateEventForm.svelte
  14. 2
      src/lib/components/write/EditEventForm.svelte
  15. 3
      src/lib/components/write/FindEventForm.svelte
  16. 6
      src/lib/modules/comments/Comment.svelte
  17. 2
      src/lib/modules/discussions/DiscussionCard.svelte
  18. 2
      src/lib/modules/discussions/DiscussionList.svelte
  19. 48
      src/lib/modules/events/EventView.svelte
  20. 14
      src/lib/modules/feed/FeedPost.svelte
  21. 6
      src/lib/modules/feed/HighlightCard.svelte
  22. 6
      src/lib/modules/feed/Reply.svelte
  23. 134
      src/lib/modules/profiles/ProfilePage.svelte
  24. 11
      src/lib/services/event-links.ts
  25. 10
      src/lib/services/nostr/event-index-loader.ts
  26. 4
      src/routes/cache/+page.svelte
  27. 3
      src/routes/discussions/+page.svelte
  28. 112
      src/routes/event/[id]/+page.svelte
  29. 6
      src/routes/find/+page.svelte
  30. 4
      src/routes/highlights/+page.svelte
  31. 13
      src/routes/profile/[pubkey]/+page.svelte
  32. 2
      src/routes/repos/+page.svelte
  33. 2
      src/routes/repos/[naddr]/+page.svelte
  34. 2
      src/routes/topics/+page.svelte

2
src/lib/components/EventMenu.svelte

@ -271,6 +271,8 @@ @@ -271,6 +271,8 @@
function viewEvent() {
closeMenu();
// Store event in sessionStorage so the event page can use it without re-fetching
sessionStorage.setItem('aitherboard_preloadedEvent', JSON.stringify(event));
goto(getEventLink(event));
}

2
src/lib/components/content/EmbeddedEventBlurb.svelte

@ -168,6 +168,8 @@ @@ -168,6 +168,8 @@
<button
class="view-button"
onclick={() => {
// Store event in sessionStorage so the event page can use it without re-fetching
sessionStorage.setItem('aitherboard_preloadedEvent', JSON.stringify(event!));
goto(getEventLink(event!));
}}
aria-label="View referenced post"

2
src/lib/components/content/HighlightOverlay.svelte

@ -18,6 +18,8 @@ @@ -18,6 +18,8 @@
let tooltipPosition = $state({ top: 0, left: 0 });
function openHighlight(highlight: Highlight) {
// Store event in sessionStorage so the event page can use it without re-fetching
sessionStorage.setItem('aitherboard_preloadedEvent', JSON.stringify(highlight.event));
goto(`/event/${highlight.event.id}`);
}

2
src/lib/components/content/QuotedContext.svelte

@ -148,6 +148,8 @@ @@ -148,6 +148,8 @@
<button
class="view-button"
onclick={() => {
// Store event in sessionStorage so the event page can use it without re-fetching
sessionStorage.setItem('aitherboard_preloadedEvent', JSON.stringify(quotedEvent!));
goto(getEventLink(quotedEvent!));
}}
aria-label="View quoted post"

2
src/lib/components/content/ReferencedEventPreview.svelte

@ -208,6 +208,8 @@ @@ -208,6 +208,8 @@
function handleViewEvent() {
if (referencedEvent) {
// Store event in sessionStorage so the event page can use it without re-fetching
sessionStorage.setItem('aitherboard_preloadedEvent', JSON.stringify(referencedEvent));
goto(`/event/${referencedEvent.id}`);
}
}

2
src/lib/components/content/ReplyContext.svelte

@ -195,6 +195,8 @@ @@ -195,6 +195,8 @@
<button
class="view-button"
onclick={() => {
// Store event in sessionStorage so the event page can use it without re-fetching
sessionStorage.setItem('aitherboard_preloadedEvent', JSON.stringify(parentEvent!));
goto(getEventLink(parentEvent!));
}}
aria-label="View original post"

2
src/lib/components/find/SearchAddressableEvents.svelte

@ -438,6 +438,8 @@ @@ -438,6 +438,8 @@
function handleResultClick(event: NostrEvent) {
// Navigate to /event route with event ID
// Store event in sessionStorage so the event page can use it without re-fetching
sessionStorage.setItem('aitherboard_preloadedEvent', JSON.stringify(event));
goto(`/event/${event.id}`);
}

25
src/lib/components/layout/ProfileBadge.svelte

@ -1,7 +1,10 @@ @@ -1,7 +1,10 @@
<script lang="ts">
import { getActivityStatus, getActivityMessage } from '../../services/auth/activity-tracker.js';
import { fetchProfile } from '../../services/user-data.js';
import { getProfile } from '../../services/cache/profile-cache.js';
import { nip19 } from 'nostr-tools';
import { goto } from '$app/navigation';
import type { NostrEvent } from '../../types/nostr.js';
interface Props {
pubkey: string;
@ -142,9 +145,29 @@ @@ -142,9 +145,29 @@
let shouldShowNip05 = $derived.by(() => {
return !!(profile?.nip05 && profile.nip05.length > 0);
});
// Handle click to pass kind 0 event to profile page
async function handleProfileClick(e: MouseEvent) {
e.preventDefault();
// Try to get the kind 0 event from cache
try {
const cached = await getProfile(pubkey);
if (cached && cached.event) {
// Store the kind 0 event in sessionStorage so the profile page can use it
sessionStorage.setItem('aitherboard_preloadedProfileEvent', JSON.stringify(cached.event));
}
} catch (error) {
// Cache read failed - continue without preloaded event
console.warn('Failed to get profile event from cache:', error);
}
// Navigate to profile page
goto(`/profile/${pubkey}`);
}
</script>
<a href="/profile/{pubkey}" class="profile-badge inline-flex min-w-0 max-w-full" class:picture-only={pictureOnly} class:inline-badge={inline} class:gap-2={!inline}>
<a href="/profile/{pubkey}" onclick={handleProfileClick} class="profile-badge inline-flex min-w-0 max-w-full" class:picture-only={pictureOnly} class:inline-badge={inline} class:gap-2={!inline}>
{#if !inline || pictureOnly}
{#if profile?.picture && !imageError}
{@const compressedPictureUrl = (() => {

2
src/lib/components/layout/SearchBox.svelte

@ -191,6 +191,8 @@ @@ -191,6 +191,8 @@
function handleResultClick(event: NostrEvent) {
showResults = false;
searchQuery = '';
// Store event in sessionStorage so the event page can use it without re-fetching
sessionStorage.setItem('aitherboard_preloadedEvent', JSON.stringify(event));
goto(`/event/${event.id}`);
}

2
src/lib/components/layout/UnifiedSearch.svelte

@ -1100,6 +1100,8 @@ @@ -1100,6 +1100,8 @@
function handleResultClick(event: NostrEvent) {
showResults = false;
searchQuery = '';
// Store event in sessionStorage so the event page can use it without re-fetching
sessionStorage.setItem('aitherboard_preloadedEvent', JSON.stringify(event));
goto(`/event/${event.id}`);
}

2
src/lib/components/profile/BookmarksPanel.svelte

@ -20,6 +20,8 @@ @@ -20,6 +20,8 @@
let loading = $state(true);
function navigateToEvent(event: NostrEvent) {
// Store event in sessionStorage so the event page can use it without re-fetching
sessionStorage.setItem('aitherboard_preloadedEvent', JSON.stringify(event));
goto(`/event/${event.id}`);
}

2
src/lib/components/profile/ProfileEventsPanel.svelte

@ -135,6 +135,8 @@ @@ -135,6 +135,8 @@
if (results.success.length > 0) {
setTimeout(() => {
// Store event in sessionStorage so the event page can use it without re-fetching
sessionStorage.setItem('aitherboard_preloadedEvent', JSON.stringify(signedEvent));
goto(`/event/${signedEvent.id}`);
}, 5000);
}

2
src/lib/components/write/CreateEventForm.svelte

@ -389,6 +389,8 @@ @@ -389,6 +389,8 @@
}
await deleteDraft(DRAFT_ID);
setTimeout(() => {
// Store event in sessionStorage so the event page can use it without re-fetching
sessionStorage.setItem('aitherboard_preloadedEvent', JSON.stringify(signedEvent));
goto(`/event/${signedEvent.id}`);
}, 5000);
}

2
src/lib/components/write/EditEventForm.svelte

@ -96,6 +96,8 @@ @@ -96,6 +96,8 @@
// If successful, wait 5 seconds and navigate
if (results.success.length > 0) {
setTimeout(() => {
// Store event in sessionStorage so the event page can use it without re-fetching
sessionStorage.setItem('aitherboard_preloadedEvent', JSON.stringify(signedEvent));
goto(`/event/${signedEvent.id}`);
}, 5000);
}

3
src/lib/components/write/FindEventForm.svelte

@ -6,6 +6,7 @@ @@ -6,6 +6,7 @@
import { nip19 } from 'nostr-tools';
import type { NostrEvent } from '../../types/nostr.js';
import { onMount } from 'svelte';
import { goto } from '$app/navigation';
// @ts-ignore - highlight.js default export works at runtime
import hljs from 'highlight.js';
import 'highlight.js/styles/vs2015.css';
@ -159,7 +160,7 @@ @@ -159,7 +160,7 @@
<div class="found-event">
<div class="event-header">
<h3 class="event-title">Found Event</h3>
<a href="/event/{foundEvent.id}" class="view-link" target="_blank">View in /event page →</a>
<a href="/event/{foundEvent.id}" onclick={(e) => { e.preventDefault(); if (foundEvent) { sessionStorage.setItem('aitherboard_preloadedEvent', JSON.stringify(foundEvent)); goto(`/event/${foundEvent.id}`); } }} class="view-link">View in /event page →</a>
</div>
<div class="event-json">

6
src/lib/modules/comments/Comment.svelte

@ -155,7 +155,11 @@ @@ -155,7 +155,11 @@
icon="eye"
label="View"
size={16}
onclick={() => goto(getEventLink(comment))}
onclick={() => {
// Store event in sessionStorage so the event page can use it without re-fetching
sessionStorage.setItem('aitherboard_preloadedEvent', JSON.stringify(comment));
goto(getEventLink(comment));
}}
/>
{#if sessionManager.isLoggedIn() && onReply}
<IconButton

2
src/lib/modules/discussions/DiscussionCard.svelte

@ -284,7 +284,7 @@ @@ -284,7 +284,7 @@
<article class="thread-card bg-fog-post dark:bg-fog-dark-post border border-fog-border dark:border-fog-dark-border p-4 mb-4 shadow-sm dark:shadow-lg">
{#if !fullView}
<a href="/event/{thread.id}" class="card-link">
<a href="/event/{thread.id}" onclick={(e) => { e.preventDefault(); sessionStorage.setItem('aitherboard_preloadedEvent', JSON.stringify(thread)); goto(`/event/${thread.id}`); }} class="card-link">
<div class="card-content" class:expanded={expanded} bind:this={contentElement}>
<CardHeader
pubkey={thread.pubkey}

2
src/lib/modules/discussions/DiscussionList.svelte

@ -581,6 +581,8 @@ @@ -581,6 +581,8 @@
return;
}
}
// Store event in sessionStorage so the event page can use it without re-fetching
sessionStorage.setItem('aitherboard_preloadedEvent', JSON.stringify(event));
goto(`/event/${event.id}`);
}

48
src/lib/modules/events/EventView.svelte

@ -11,9 +11,10 @@ @@ -11,9 +11,10 @@
interface Props {
eventId: string;
preloadedEvent?: NostrEvent | null; // Optional preloaded event to avoid re-fetching
}
let { eventId }: Props = $props();
let { eventId, preloadedEvent = null }: Props = $props();
// Virtual scrolling for kind 30040 (event indexes with 36k+ events)
let Virtualizer: any = $state(null);
@ -120,8 +121,17 @@ @@ -120,8 +121,17 @@
$effect(() => {
// Only load if eventId changed and we're not already loading
// Also reload if preloadedEvent changes (e.g., from null to an actual event)
if (eventId && eventId !== lastLoadedEventId && !loading) {
loadEvent();
} else if (preloadedEvent && preloadedEvent.id === eventId && !rootEvent && !loading) {
// If we have a preloaded event and haven't loaded yet, use it immediately
rootEvent = preloadedEvent;
loading = false;
if (preloadedEvent.kind === 30040) {
loadEventIndexHierarchy(preloadedEvent);
loadVirtualizer();
}
}
});
@ -139,12 +149,18 @@ @@ -139,12 +149,18 @@
lastLoadedEventId = eventId; // Track that we're loading this eventId
try {
const threadRelays = relayManager.getThreadReadRelays();
const feedRelays = relayManager.getFeedReadRelays();
const allRelays = [...new Set([...threadRelays, ...feedRelays])];
// Use preloaded event if available and matches eventId
let event: NostrEvent | null = null;
if (preloadedEvent && preloadedEvent.id === eventId) {
event = preloadedEvent;
} else {
// Load the event by ID
const threadRelays = relayManager.getThreadReadRelays();
const feedRelays = relayManager.getFeedReadRelays();
const allRelays = [...new Set([...threadRelays, ...feedRelays])];
event = await nostrClient.getEventById(eventId, allRelays);
}
// Load the event by ID
const event = await nostrClient.getEventById(eventId, allRelays);
if (event) {
rootEvent = event;
@ -172,20 +188,20 @@ @@ -172,20 +188,20 @@
indexError = null;
missingEvents = [];
try {
console.log('Loading event index hierarchy for kind 30040...');
// console.log('Loading event index hierarchy for kind 30040...');
const result = await loadEventIndex(opEvent);
eventIndexItems = result.items;
missingEvents = result.missingEvents;
console.log(`Loaded ${result.items.length} events from index hierarchy`);
// console.log(`Loaded ${result.items.length} events from index hierarchy`);
// Debug: log items with children
const itemsWithChildren = result.items.filter(item => item.children && item.children.length > 0);
console.log(`[EventView] Items with children: ${itemsWithChildren.length}`, itemsWithChildren.map(item => ({
id: item.event.id,
kind: item.event.kind,
level: item.level,
childrenCount: item.children?.length || 0,
title: item.event.tags.find(t => t[0] === 'title')?.[1]
})));
// const itemsWithChildren = result.items.filter(item => item.children && item.children.length > 0);
// console.log(`[EventView] Items with children: ${itemsWithChildren.length}`, itemsWithChildren.map(item => ({
// id: item.event.id,
// kind: item.event.kind,
// level: item.level,
// childrenCount: item.children?.length || 0,
// title: item.event.tags.find(t => t[0] === 'title')?.[1]
// })));
if (result.missingEvents.length > 0) {
console.warn(`[EventView] ${result.missingEvents.length} events are missing from the index hierarchy`);
}

14
src/lib/modules/feed/FeedPost.svelte

@ -956,6 +956,8 @@ @@ -956,6 +956,8 @@
} else if (e.key === 'Enter' && !showReplyForm) {
// Enter to view full event
e.preventDefault();
// Store event in sessionStorage so the event page can use it without re-fetching
sessionStorage.setItem('aitherboard_preloadedEvent', JSON.stringify(post));
goto(getEventLink(post));
}
}}
@ -985,7 +987,11 @@ @@ -985,7 +987,11 @@
icon="eye"
label="View"
size={16}
onclick={() => goto(getEventLink(post))}
onclick={() => {
// Store event in sessionStorage so the event page can use it without re-fetching
sessionStorage.setItem('aitherboard_preloadedEvent', JSON.stringify(post));
goto(getEventLink(post));
}}
/>
{#if isLoggedIn}
<IconButton
@ -1069,7 +1075,11 @@ @@ -1069,7 +1075,11 @@
icon="eye"
label="View"
size={16}
onclick={() => goto(getEventLink(post))}
onclick={() => {
// Store event in sessionStorage so the event page can use it without re-fetching
sessionStorage.setItem('aitherboard_preloadedEvent', JSON.stringify(post));
goto(getEventLink(post));
}}
/>
{#if isLoggedIn}
<IconButton

6
src/lib/modules/feed/HighlightCard.svelte

@ -361,7 +361,11 @@ @@ -361,7 +361,11 @@
icon="eye"
label="View"
size={16}
onclick={() => goto(getEventLink(highlight))}
onclick={() => {
// Store event in sessionStorage so the event page can use it without re-fetching
sessionStorage.setItem('aitherboard_preloadedEvent', JSON.stringify(highlight));
goto(getEventLink(highlight));
}}
/>
{#if isLoggedIn}
<IconButton

6
src/lib/modules/feed/Reply.svelte

@ -98,7 +98,11 @@ @@ -98,7 +98,11 @@
icon="eye"
label="View"
size={16}
onclick={() => goto(getEventLink(reply))}
onclick={() => {
// Store event in sessionStorage so the event page can use it without re-fetching
sessionStorage.setItem('aitherboard_preloadedEvent', JSON.stringify(reply));
goto(getEventLink(reply));
}}
/>
{#if sessionManager.isLoggedIn() && onReply}
<IconButton

134
src/lib/modules/profiles/ProfilePage.svelte

@ -27,19 +27,22 @@ @@ -27,19 +27,22 @@
let notifications = $state<NostrEvent[]>([]);
let interactionsWithMe = $state<NostrEvent[]>([]);
let wallComments = $state<NostrEvent[]>([]); // Kind 1111 comments on the wall
let mediaEvents = $state<NostrEvent[]>([]); // Kinds 20, 21, 22
let hasMedia = $state(false); // Whether kinds 20, 21, 22 are available
let loading = $state(true);
let loadingWall = $state(false);
let activeTab = $state<'pins' | 'notifications' | 'interactions' | 'wall'>('pins');
let loadingMedia = $state(false);
let activeTab = $state<'pins' | 'media' | 'notifications' | 'interactions' | 'wall'>('pins');
let nip05Validations = $state<Record<string, boolean | null>>({}); // null = checking, true = valid, false = invalid
// Compute pubkey from route params
let profilePubkey = $derived.by(() => decodePubkey($page.params.pubkey));
// Initialize activeTab from URL parameter
function getTabFromUrl(): 'pins' | 'notifications' | 'interactions' | 'wall' {
function getTabFromUrl(): 'pins' | 'media' | 'notifications' | 'interactions' | 'wall' {
const tabParam = $page.url.searchParams.get('tab');
const validTabs: Array<'pins' | 'notifications' | 'interactions' | 'wall'> = ['pins', 'notifications', 'interactions', 'wall'];
const validTabs: Array<'pins' | 'media' | 'notifications' | 'interactions' | 'wall'> = ['pins', 'media', 'notifications', 'interactions', 'wall'];
if (tabParam && validTabs.includes(tabParam as any)) {
return tabParam as 'pins' | 'notifications' | 'interactions' | 'wall';
return tabParam as 'pins' | 'media' | 'notifications' | 'interactions' | 'wall';
}
return 'pins'; // Default
}
@ -52,12 +55,14 @@ @@ -52,12 +55,14 @@
// Load data for the tab if needed
if (activeTab === 'wall' && profileEvent && wallComments.length === 0 && !loadingWall) {
loadWallComments(profileEvent.id);
} else if (activeTab === 'media' && profilePubkey && mediaEvents.length === 0 && !loadingMedia) {
loadMedia(profilePubkey);
}
}
});
// Function to change tab and update URL
async function setActiveTab(tab: 'pins' | 'notifications' | 'interactions' | 'wall') {
async function setActiveTab(tab: 'pins' | 'media' | 'notifications' | 'interactions' | 'wall') {
activeTab = tab;
const url = new URL($page.url);
url.searchParams.set('tab', tab);
@ -73,6 +78,11 @@ @@ -73,6 +78,11 @@
if (profileEvent && wallComments.length === 0 && !loadingWall) {
await loadWallComments(profileEvent.id);
}
} else if (tab === 'media') {
// Load media if not already loaded
if (profilePubkey && mediaEvents.length === 0 && !loadingMedia) {
await loadMedia(profilePubkey);
}
}
}
@ -163,6 +173,29 @@ @@ -163,6 +173,29 @@
async function loadProfileEvent(pubkey: string) {
if (!isMounted) return;
try {
// Check sessionStorage for a preloaded kind 0 event (from ProfileBadge click)
if (typeof window !== 'undefined') {
const preloadedEventStr = sessionStorage.getItem('aitherboard_preloadedProfileEvent');
if (preloadedEventStr) {
try {
const preloadedEvent = JSON.parse(preloadedEventStr) as NostrEvent;
// Verify the event is a kind 0 event and matches the pubkey
if (preloadedEvent.kind === KIND.METADATA && preloadedEvent.pubkey === pubkey) {
// Use the preloaded event
profileEvent = preloadedEvent;
// Clear it after reading
sessionStorage.removeItem('aitherboard_preloadedProfileEvent');
// Don't load wall comments here - load them when Wall tab is clicked
return;
}
} catch (parseError) {
// Invalid JSON in sessionStorage, continue with normal loading
console.warn('Failed to parse preloaded profile event from sessionStorage:', parseError);
sessionStorage.removeItem('aitherboard_preloadedProfileEvent');
}
}
}
// Try cache first
const cached = await getProfile(pubkey);
if (cached) {
@ -336,6 +369,70 @@ @@ -336,6 +369,70 @@
if (isMounted) pins = [];
}
}
async function checkMediaAvailability(pubkey: string) {
if (!isMounted) return;
try {
const profileRelays = relayManager.getProfileReadRelays();
// Check if any events of kinds 20, 21, 22 exist for this pubkey
const mediaCheck = await nostrClient.fetchEvents(
[{ kinds: [KIND.PICTURE_NOTE, KIND.VIDEO_NOTE, KIND.SHORT_VIDEO_NOTE], authors: [pubkey], limit: 1 }],
profileRelays,
{ useCache: 'cache-first', cacheResults: true, timeout: config.shortTimeout }
);
if (!isMounted) return;
hasMedia = mediaCheck.length > 0;
} catch (error) {
// Failed to check - assume no media
if (isMounted) hasMedia = false;
}
}
async function loadMedia(pubkey: string) {
if (!isMounted) return;
loadingMedia = true;
try {
const profileRelays = relayManager.getFeedReadRelays();
const fetchPromise = nostrClient.fetchEvents(
[{ kinds: [KIND.PICTURE_NOTE, KIND.VIDEO_NOTE, KIND.SHORT_VIDEO_NOTE], authors: [pubkey], limit: config.feedLimit }],
profileRelays,
{
useCache: 'cache-first',
cacheResults: true,
timeout: config.shortTimeout,
onUpdate: (newMedia) => {
if (!isMounted) return;
// Merge with existing media
const mediaMap = new Map(mediaEvents.map(m => [m.id, m]));
for (const media of newMedia) {
mediaMap.set(media.id, media);
}
mediaEvents = Array.from(mediaMap.values()).sort((a, b) => b.created_at - a.created_at);
}
}
);
activeFetchPromises.add(fetchPromise);
const fetchedMedia = await fetchPromise;
activeFetchPromises.delete(fetchPromise);
if (!isMounted) return;
// Sort by created_at descending
mediaEvents = fetchedMedia.sort((a, b) => b.created_at - a.created_at);
hasMedia = mediaEvents.length > 0;
} catch (error) {
// Failed to load media
if (isMounted) {
mediaEvents = [];
hasMedia = false;
}
} finally {
if (isMounted) {
loadingMedia = false;
}
}
}
async function loadNotifications(pubkey: string) {
if (!isMounted) return;
@ -713,6 +810,11 @@ @@ -713,6 +810,11 @@
// Failed to load pins - non-critical
}));
// Check if media (kinds 20, 21, 22) are available
loadPromises.push(checkMediaAvailability(pubkey).catch(() => {
// Failed to check media - non-critical
}));
// Load notifications or interactions based on profile type
if (isOwnProfile) {
loadPromises.push(loadNotifications(pubkey).catch(() => {
@ -783,6 +885,8 @@ @@ -783,6 +885,8 @@
interactionsWithMe = [];
wallComments = [];
pins = [];
mediaEvents = [];
hasMedia = false;
await loadProfile();
}
export { refresh };
@ -877,6 +981,14 @@ @@ -877,6 +981,14 @@
>
Pins ({pins.length})
</button>
{#if hasMedia}
<button
onclick={() => setActiveTab('media')}
class="px-2 sm:px-4 py-2 font-semibold whitespace-nowrap flex-shrink-0 {activeTab === 'media' ? 'border-b-2 border-fog-accent dark:border-fog-dark-accent' : ''}"
>
Media ({mediaEvents.length})
</button>
{/if}
<button
onclick={() => setActiveTab('wall')}
class="px-2 sm:px-4 py-2 font-semibold whitespace-nowrap flex-shrink-0 {activeTab === 'wall' ? 'border-b-2 border-fog-accent dark:border-fog-dark-accent' : ''}"
@ -910,6 +1022,18 @@ @@ -910,6 +1022,18 @@
{/each}
</div>
{/if}
{:else if activeTab === 'media'}
{#if loadingMedia}
<p class="text-fog-text-light dark:text-fog-dark-text-light">Loading media...</p>
{:else if mediaEvents.length === 0}
<p class="text-fog-text-light dark:text-fog-dark-text-light">No media posts yet.</p>
{:else}
<div class="media-list">
{#each mediaEvents as media (media.id)}
<FeedPost post={media} />
{/each}
</div>
{/if}
{:else if activeTab === 'notifications'}
{#if notifications.length === 0}
<p class="text-fog-text-light dark:text-fog-dark-text-light">No notifications yet.</p>

11
src/lib/services/event-links.ts

@ -4,6 +4,7 @@ @@ -4,6 +4,7 @@
import { nip19 } from 'nostr-tools';
import type { NostrEvent } from '../types/nostr.js';
import { goto } from '$app/navigation';
/**
* Generate a link to view an event
@ -44,3 +45,13 @@ export function getEventLink(event: NostrEvent): string { @@ -44,3 +45,13 @@ export function getEventLink(event: NostrEvent): string {
return `/event/${event.id}`;
}
}
/**
* Navigate to an event page with the event preloaded
* Stores the event in sessionStorage so the event page can use it without re-fetching
*/
export function navigateToEvent(event: NostrEvent): void {
// Store event in sessionStorage so the event page can use it without re-fetching
sessionStorage.setItem('aitherboard_preloadedEvent', JSON.stringify(event));
goto(getEventLink(event));
}

10
src/lib/services/nostr/event-index-loader.ts

@ -279,7 +279,7 @@ async function loadEventIndexRecursive( @@ -279,7 +279,7 @@ async function loadEventIndexRecursive(
}
// Process all a-tag results
console.log(`[EventIndex] Processing ${aTags.length} a-tags, found ${aTagResults.size} events`);
// console.log(`[EventIndex] Processing ${aTags.length} a-tags, found ${aTagResults.size} events`);
for (const aTagInfo of aTags) {
const parts = aTagInfo.address.split(':');
if (parts.length === 3) {
@ -291,13 +291,13 @@ async function loadEventIndexRecursive( @@ -291,13 +291,13 @@ async function loadEventIndexRecursive(
const event = aTagResults.get(aTagInfo.address);
if (event) {
console.log(`[EventIndex] Processing event ${event.id}, kind ${event.kind}, level ${level}`);
// console.log(`[EventIndex] Processing event ${event.id}, kind ${event.kind}, level ${level}`);
// Check if this event is also a kind 30040 (nested index)
if (event.kind === 30040) {
console.log(`[EventIndex] Found kind 30040 event ${event.id} at level ${level}, loading children...`);
// console.log(`[EventIndex] Found kind 30040 event ${event.id} at level ${level}, loading children...`);
// Recursively load nested index
const nestedResult = await loadEventIndexRecursive(event, level + 1, maxDepth);
console.log(`[EventIndex] Loaded nested index ${event.id} with ${nestedResult.items.length} children at level ${level + 1}`, nestedResult.items.map(i => ({ id: i.event.id, kind: i.event.kind, level: i.level })));
// console.log(`[EventIndex] Loaded nested index ${event.id} with ${nestedResult.items.length} children at level ${level + 1}`, nestedResult.items.map(i => ({ id: i.event.id, kind: i.event.kind, level: i.level })));
// Create a parent item with children
const parentItem: EventIndexItem = {
event,
@ -525,7 +525,7 @@ async function loadEventIndexRecursive( @@ -525,7 +525,7 @@ async function loadEventIndexRecursive(
missingEvents.sort((a, b) => a.order - b.order);
const itemsWithChildren = items.filter(item => item.children && item.children.length > 0);
console.log(`[EventIndex] Returning ${items.length} items (${itemsWithChildren.length} with children) at level ${level}`);
// console.log(`[EventIndex] Returning ${items.length} items (${itemsWithChildren.length} with children) at level ${level}`);
return { items, missingEvents };
}

4
src/routes/cache/+page.svelte vendored

@ -779,7 +779,7 @@ @@ -779,7 +779,7 @@
{JSON.stringify(event, null, 2)}
</div>
<div class="event-actions-bottom">
<a href="/event/{event.id}" class="action-button" target="_blank">View</a>
<a href="/event/{event.id}" onclick={(e) => { e.preventDefault(); sessionStorage.setItem('aitherboard_preloadedEvent', JSON.stringify(event)); goto(`/event/${event.id}`); }} class="action-button">View</a>
<button class="action-button" onclick={() => copyEventJson(event)} title="Copy event JSON">
Copy JSON
</button>
@ -809,7 +809,7 @@ @@ -809,7 +809,7 @@
<div class="event-preview">
<p class="event-content-preview">{event.content.substring(0, 200)}{event.content.length > 200 ? '...' : ''}</p>
<div class="event-actions-bottom">
<a href="/event/{event.id}" class="action-button" target="_blank">View</a>
<a href="/event/{event.id}" onclick={(e) => { e.preventDefault(); sessionStorage.setItem('aitherboard_preloadedEvent', JSON.stringify(event)); goto(`/event/${event.id}`); }} class="action-button">View</a>
<button class="action-button" onclick={() => copyEventJson(event)} title="Copy event JSON">
Copy JSON
</button>

3
src/routes/discussions/+page.svelte

@ -10,6 +10,7 @@ @@ -10,6 +10,7 @@
import type { NostrEvent } from '../../lib/types/nostr.js';
import { onMount } from 'svelte';
import { page } from '$app/stores';
import { goto } from '$app/navigation';
import Pagination from '../../lib/components/ui/Pagination.svelte';
import { getPaginatedItems, getCurrentPage, ITEMS_PER_PAGE } from '../../lib/utils/pagination.js';
import Icon from '../../lib/components/ui/Icon.svelte';
@ -126,7 +127,7 @@ @@ -126,7 +127,7 @@
<h3>Events ({searchResults.events.length})</h3>
<div class="event-results">
{#each paginatedSearchEvents as event}
<a href="/event/{event.id}" class="event-result-card">
<a href="/event/{event.id}" onclick={(e) => { e.preventDefault(); sessionStorage.setItem('aitherboard_preloadedEvent', JSON.stringify(event)); goto(`/event/${event.id}`); }} class="event-result-card">
<FeedPost post={event} fullView={false} />
</a>
{/each}

112
src/routes/event/[id]/+page.svelte

@ -9,23 +9,26 @@ @@ -9,23 +9,26 @@
import { page } from '$app/stores';
import { nip19 } from 'nostr-tools';
import { KIND } from '../../../lib/types/kind-lookup.js';
import type { NostrEvent } from '../../../lib/types/nostr.js';
let decodedEventId = $state<string | null>(null);
let eventKind = $state<number | null>(null);
let preloadedEvent = $state<NostrEvent | null>(null); // Store event if already loaded (e.g., from naddr)
let loading = $state(false);
let error = $state<string | null>(null);
let lastProcessedParam = $state<string | null>(null); // Track last processed param to prevent loops
/**
* Decode route parameter to event hex ID
* Decode route parameter to event hex ID and optionally return the event
* Supports: hex event id, note, nevent, naddr
* Returns: { eventId: string, event?: NostrEvent } or null
*/
async function decodeEventId(param: string): Promise<string | null> {
async function decodeEventId(param: string): Promise<{ eventId: string; event?: NostrEvent } | null> {
if (!param) return null;
// Check if it's already a hex event ID (64 hex characters)
if (/^[0-9a-f]{64}$/i.test(param)) {
return param.toLowerCase();
return { eventId: param.toLowerCase() };
}
// Check if it's a bech32 encoded format (note, nevent, naddr)
@ -33,11 +36,11 @@ @@ -33,11 +36,11 @@
try {
const decoded = nip19.decode(param);
if (decoded.type === 'note') {
return String(decoded.data);
return { eventId: String(decoded.data) };
} else if (decoded.type === 'nevent') {
// nevent contains event id and optional relays
if (decoded.data && typeof decoded.data === 'object' && 'id' in decoded.data) {
return String(decoded.data.id);
return { eventId: String(decoded.data.id) };
}
} else if (decoded.type === 'naddr') {
// naddr is for parameterized replaceable events (kind + pubkey + d tag)
@ -70,11 +73,12 @@ @@ -70,11 +73,12 @@
const events = await nostrClient.fetchEvents(
filters,
relays,
{ useCache: true, cacheResults: true }
{ useCache: 'cache-first', cacheResults: true, timeout: 10000 }
);
if (events.length > 0) {
return events[0].id;
// Return both eventId and the event itself to avoid re-fetching
return { eventId: events[0].id, event: events[0] };
} else {
// Event not found for naddr
return null;
@ -103,20 +107,49 @@ @@ -103,20 +107,49 @@
error = null;
decodedEventId = null;
eventKind = null;
preloadedEvent = null;
lastProcessedParam = currentParam; // Track that we're processing this param
try {
const eventId = await decodeEventId(currentParam);
if (eventId) {
decodedEventId = eventId;
// Check sessionStorage for a preloaded event (from EventMenu, eye icon button, or any click)
let sessionEvent: NostrEvent | null = null;
if (typeof window !== 'undefined') {
const preloadedEventStr = sessionStorage.getItem('aitherboard_preloadedEvent');
if (preloadedEventStr) {
try {
sessionEvent = JSON.parse(preloadedEventStr) as NostrEvent;
// Clear it after reading (we'll verify it matches first)
sessionStorage.removeItem('aitherboard_preloadedEvent');
} catch (parseError) {
// Invalid JSON in sessionStorage, continue with normal loading
console.warn('Failed to parse preloaded event from sessionStorage:', parseError);
sessionStorage.removeItem('aitherboard_preloadedEvent');
}
}
}
const result = await decodeEventId(currentParam);
if (result) {
decodedEventId = result.eventId;
// Fetch the event to determine its kind for routing
const threadRelays = relayManager.getThreadReadRelays();
const feedRelays = relayManager.getFeedReadRelays();
const allRelays = [...new Set([...threadRelays, ...feedRelays])];
const event = await nostrClient.getEventById(eventId, allRelays);
if (event) {
eventKind = event.kind;
// If we already have the event from naddr decoding, use it (highest priority)
if (result.event) {
preloadedEvent = result.event;
eventKind = result.event.kind;
} else if (sessionEvent && sessionEvent.id === result.eventId) {
// Use session event if it matches the decoded event ID
preloadedEvent = sessionEvent;
eventKind = sessionEvent.kind;
} else {
// Fetch the event to determine its kind for routing
const threadRelays = relayManager.getThreadReadRelays();
const feedRelays = relayManager.getFeedReadRelays();
const allRelays = [...new Set([...threadRelays, ...feedRelays])];
const event = await nostrClient.getEventById(result.eventId, allRelays);
if (event) {
preloadedEvent = event;
eventKind = event.kind;
}
}
} else {
error = 'Event not found or invalid format';
@ -146,24 +179,33 @@ @@ -146,24 +179,33 @@
<Header />
<main class="container mx-auto px-4 py-8">
{#if loading}
<PageHeader title="Loading event..." onRefresh={loadEvent} refreshLoading={loading} />
<p class="text-fog-text dark:text-fog-dark-text">Loading event...</p>
{:else if error}
<PageHeader title="Error" onRefresh={loadEvent} refreshLoading={loading} />
<p class="text-fog-text dark:text-fog-dark-text">{error}</p>
{:else if decodedEventId}
<PageHeader title="Event" onRefresh={loadEvent} refreshLoading={loading} />
{#if eventKind === KIND.DISCUSSION_THREAD}
<!-- Route kind 11 (discussion threads) to DiscussionView -->
<DiscussionView threadId={decodedEventId} />
<div class="event-content-wrapper">
{#if loading}
<PageHeader title="Loading event..." onRefresh={loadEvent} refreshLoading={loading} />
<p class="text-fog-text dark:text-fog-dark-text">Loading event...</p>
{:else if error}
<PageHeader title="Error" onRefresh={loadEvent} refreshLoading={loading} />
<p class="text-fog-text dark:text-fog-dark-text">{error}</p>
{:else if decodedEventId}
<PageHeader title="Event" onRefresh={loadEvent} refreshLoading={loading} />
{#if eventKind === KIND.DISCUSSION_THREAD}
<!-- Route kind 11 (discussion threads) to DiscussionView -->
<DiscussionView threadId={decodedEventId} />
{:else}
<!-- Route all other events (including kind 30040, metadata-only, etc.) to EventView -->
<EventView eventId={decodedEventId} preloadedEvent={preloadedEvent} />
{/if}
{:else if $page.params.id}
<p class="text-fog-text dark:text-fog-dark-text">Invalid event ID format. Supported: hex event ID, note, nevent, or naddr</p>
{:else}
<!-- Route all other events (including kind 30040, metadata-only, etc.) to EventView -->
<EventView eventId={decodedEventId} />
<p class="text-fog-text dark:text-fog-dark-text">Event ID required</p>
{/if}
{:else if $page.params.id}
<p class="text-fog-text dark:text-fog-dark-text">Invalid event ID format. Supported: hex event ID, note, nevent, or naddr</p>
{:else}
<p class="text-fog-text dark:text-fog-dark-text">Event ID required</p>
{/if}
</div>
</main>
<style>
.event-content-wrapper {
max-width: var(--content-width);
margin: 0 auto;
}
</style>

6
src/routes/find/+page.svelte

@ -12,7 +12,7 @@ @@ -12,7 +12,7 @@
import { onMount } from 'svelte';
import { page } from '$app/stores';
import { untrack } from 'svelte';
import { afterNavigate } from '$app/navigation';
import { afterNavigate, goto } from '$app/navigation';
import Pagination from '../../lib/components/ui/Pagination.svelte';
import { getPaginatedItems, getCurrentPage, ITEMS_PER_PAGE } from '../../lib/utils/pagination.js';
@ -156,7 +156,7 @@ @@ -156,7 +156,7 @@
<div class="event-results">
{#each paginatedCacheEvents as event}
<div class="event-result-card">
<a href="/event/{event.id}" class="event-result-link">
<a href="/event/{event.id}" onclick={(e) => { e.preventDefault(); sessionStorage.setItem('aitherboard_preloadedEvent', JSON.stringify(event)); goto(`/event/${event.id}`); }} class="event-result-link">
<FeedPost post={event} fullView={false} />
</a>
<div class="event-relay-badge">
@ -194,7 +194,7 @@ @@ -194,7 +194,7 @@
<div class="event-results">
{#each paginatedSearchEvents as event}
<div class="event-result-card">
<a href="/event/{event.id}" class="event-result-link">
<a href="/event/{event.id}" onclick={(e) => { e.preventDefault(); sessionStorage.setItem('aitherboard_preloadedEvent', JSON.stringify(event)); goto(`/event/${event.id}`); }} class="event-result-link">
<FeedPost post={event} fullView={false} />
</a>
{#if eventRelayMap.has(event.id)}

4
src/routes/highlights/+page.svelte

@ -310,7 +310,7 @@ @@ -310,7 +310,7 @@
{#each searchResults.events as event}
{#if event.kind === KIND.HIGHLIGHTED_ARTICLE}
<div class="event-result-card">
<HighlightCard highlight={event} onOpenEvent={(e) => goto(`/event/${e.id}`)} />
<HighlightCard highlight={event} onOpenEvent={(e) => { sessionStorage.setItem('aitherboard_preloadedEvent', JSON.stringify(e)); goto(`/event/${e.id}`); }} />
</div>
{/if}
{/each}
@ -337,7 +337,7 @@ @@ -337,7 +337,7 @@
<div class="highlights-posts">
{#each paginatedItems as item (item.event.id)}
<div class="highlight-item-wrapper">
<HighlightCard highlight={item.event} onOpenEvent={(event) => goto(`/event/${event.id}`)} />
<HighlightCard highlight={item.event} onOpenEvent={(event) => { sessionStorage.setItem('aitherboard_preloadedEvent', JSON.stringify(event)); goto(`/event/${event.id}`); }} />
</div>
{/each}
</div>

13
src/routes/profile/[pubkey]/+page.svelte

@ -23,6 +23,15 @@ @@ -23,6 +23,15 @@
<Header />
<main class="container mx-auto px-2 sm:px-4 py-4 sm:py-8">
<PageHeader title="Profile" onRefresh={handleRefresh} />
<ProfilePage bind:this={profilePageComponent} />
<div class="profile-content">
<PageHeader title="Profile" onRefresh={handleRefresh} />
<ProfilePage bind:this={profilePageComponent} />
</div>
</main>
<style>
.profile-content {
max-width: var(--content-width);
margin: 0 auto;
}
</style>

2
src/routes/repos/+page.svelte

@ -563,7 +563,7 @@ @@ -563,7 +563,7 @@
<FeedPost post={event} fullView={false} />
</a>
{:else}
<a href="/event/{event.id}" class="event-result-card">
<a href="/event/{event.id}" onclick={(e) => { e.preventDefault(); sessionStorage.setItem('aitherboard_preloadedEvent', JSON.stringify(event)); goto(`/event/${event.id}`); }} class="event-result-card">
<FeedPost post={event} fullView={false} />
</a>
{/if}

2
src/routes/repos/[naddr]/+page.svelte

@ -1468,7 +1468,7 @@ @@ -1468,7 +1468,7 @@
<div class="doc-header">
<div class="doc-meta">
<span class="doc-kind">Kind {docEvent.kind}</span>
<a href="/event/{docEvent.id}" class="doc-event-link">View Event</a>
<a href="/event/{docEvent.id}" onclick={(e) => { e.preventDefault(); sessionStorage.setItem('aitherboard_preloadedEvent', JSON.stringify(docEvent)); goto(`/event/${docEvent.id}`); }} class="doc-event-link">View Event</a>
<EventMenu event={docEvent} showContentActions={true} />
</div>
</div>

2
src/routes/topics/+page.svelte

@ -282,7 +282,7 @@ @@ -282,7 +282,7 @@
<h3>Events ({searchResults.events.length})</h3>
<div class="event-results">
{#each paginatedSearchEvents as event}
<a href="/event/{event.id}" class="event-result-card">
<a href="/event/{event.id}" onclick={(e) => { e.preventDefault(); sessionStorage.setItem('aitherboard_preloadedEvent', JSON.stringify(event)); goto(`/event/${event.id}`); }} class="event-result-card">
<FeedPost post={event} fullView={false} />
</a>
{/each}

Loading…
Cancel
Save