diff --git a/ideas.txt b/ideas.txt index 6b1479b..7fe0bea 100644 --- a/ideas.txt +++ b/ideas.txt @@ -87,3 +87,14 @@ Clicking create should ask them to enter a kind they would like to write: 1, 11, 17. If the user is looking at their own profile page, display a menu item "Adjust profile events" that opens a left-side panel that allows them select one of the following events to create/update: 0, 3, 30315, 10133, 10002, 10432, 10001, 10003, 10895, 10015, 10030, 30030, 10000, 30008. Selecting one should open an appropriate form and preload it with any event found in cache or on the relays. Publish to cache and to the standard write-relays. Publishing should reveal the standard success/failure message for the relays. If none were successful, allow them to attempt to republish from cache. If successful, wait 5 seconds and then, open the event in the /event page. 18. Make sure the /event page can handle metadata-only (no "content") events gracefully, displaying their tag-lists. + +Get rid of the light/dark mode button on the main nav bar. Instead, change the preferences button so that it opens a left-side panel with light/dark mode, text-size and paragraph spacing settings, as well as a new checkbox: Create expiring events (6 months: kinds 7, 1, 30315) + +that adds a 6-month expiration time stamp to those event they create, like: +["expiration", "1600000000"] + +also allow them to determine their preferred media-upload server (see ../jumble for how this is done, in /settings/posts) + +and add a button "Manage Cache" that opens a page /cache that contains a full cache browser and manager + +Add a short "About" section, at the bottom of the panel. \ No newline at end of file diff --git a/public/healthz.json b/public/healthz.json index 40ca042..28bed0c 100644 --- a/public/healthz.json +++ b/public/healthz.json @@ -2,7 +2,7 @@ "status": "ok", "service": "aitherboard", "version": "0.1.0", - "buildTime": "2026-02-04T08:31:34.379Z", + "buildTime": "2026-02-04T09:37:24.895Z", "gitCommit": "unknown", - "timestamp": 1770193894379 + "timestamp": 1770197844895 } \ No newline at end of file diff --git a/src/lib/components/content/HighlightOverlay.svelte b/src/lib/components/content/HighlightOverlay.svelte index 597a9ca..70b7db4 100644 --- a/src/lib/components/content/HighlightOverlay.svelte +++ b/src/lib/components/content/HighlightOverlay.svelte @@ -8,9 +8,10 @@ highlights: Array<{ start: number; end: number; highlight: Highlight }>; content: string; event: NostrEvent; + children: import('svelte').Snippet; } - let { highlights, content, event }: Props = $props(); + let { highlights, content, event, children }: Props = $props(); let containerRef = $state(null); let drawerOpen = $state(false); @@ -91,7 +92,7 @@ node.parentNode?.insertBefore(document.createTextNode(afterText), node); } - node.remove(); + node.parentNode?.removeChild(node); break; // Only wrap first occurrence } } @@ -103,7 +104,7 @@
- + {@render children()} {#if hoveredHighlight}
-
diff --git a/src/lib/components/relay/RelayInfo.svelte b/src/lib/components/relay/RelayInfo.svelte new file mode 100644 index 0000000..00fca93 --- /dev/null +++ b/src/lib/components/relay/RelayInfo.svelte @@ -0,0 +1,417 @@ + + +
+ {#if loading} +
+

Loading relay information...

+
+ {:else if error} +
+

Error: {error}

+
+ {:else} +
+
+

Relay Information

+
+ {getStatusIcon(connectionStatus)} + + {connectionStatus.charAt(0).toUpperCase() + connectionStatus.slice(1)} + +
+
+ {relayUrl} +
+ + {#if metadata} +
+ {#if metadata.name} +
+ Name: + {metadata.name} +
+ {/if} + + {#if metadata.description} +
+ Description: + {metadata.description} +
+ {/if} + + {#if metadata.software} +
+ Software: + {metadata.software}{metadata.version ? ` ${metadata.version}` : ''} +
+ {/if} + + {#if metadata.contact} +
+ Contact: + + {#if metadata.contact.startsWith('mailto:')} + {metadata.contact.replace('mailto:', '')} + {:else if metadata.contact.startsWith('http')} + {metadata.contact} + {:else} + {metadata.contact} + {/if} + +
+ {/if} + + {#if metadata.pubkey} +
+ Pubkey: + {metadata.pubkey.substring(0, 16)}... +
+ {/if} + + {#if metadata.supported_nips && metadata.supported_nips.length > 0} +
+ Supported NIPs: + + {metadata.supported_nips.slice(0, 20).join(', ')}{metadata.supported_nips.length > 20 ? '...' : ''} + +
+ {/if} + + {#if metadata.limitation} +
+ Limitations: +
+ {#if metadata.limitation.max_message_length} + Max message: {metadata.limitation.max_message_length} bytes + {/if} + {#if metadata.limitation.max_subscriptions} + Max subscriptions: {metadata.limitation.max_subscriptions} + {/if} + {#if metadata.limitation.max_filters} + Max filters: {metadata.limitation.max_filters} + {/if} + {#if metadata.limitation.max_limit} + Max limit: {metadata.limitation.max_limit} + {/if} + {#if metadata.limitation.auth_required} + Auth required + {/if} + {#if metadata.limitation.payment_required} + Payment required + {/if} + {#if metadata.limitation.restricted_writes} + Restricted writes + {/if} +
+
+ {/if} +
+ {:else} + + {/if} + {/if} +
+ + diff --git a/src/lib/components/write/EditEventForm.svelte b/src/lib/components/write/EditEventForm.svelte index 85a9fbe..a6a0494 100644 --- a/src/lib/components/write/EditEventForm.svelte +++ b/src/lib/components/write/EditEventForm.svelte @@ -13,12 +13,18 @@ let { event }: Props = $props(); - let content = $state(event.content || ''); - let tags = $state([...event.tags]); + let content = $state(''); + let tags = $state([]); let publishing = $state(false); let publicationModalOpen = $state(false); let publicationResults = $state<{ success: string[]; failed: Array<{ relay: string; error: string }> } | null>(null); + // Sync state when event prop changes + $effect(() => { + content = event.content || ''; + tags = [...event.tags]; + }); + function addTag() { tags = [...tags, ['', '']]; } diff --git a/src/lib/modules/feed/FeedPage.svelte b/src/lib/modules/feed/FeedPage.svelte index cf621eb..88fdfcf 100644 --- a/src/lib/modules/feed/FeedPage.svelte +++ b/src/lib/modules/feed/FeedPage.svelte @@ -8,6 +8,12 @@ import { onMount, tick } from 'svelte'; import { KIND } from '../../types/kind-lookup.js'; + interface Props { + singleRelay?: string; // If provided, use only this relay and disable cache + } + + let { singleRelay }: Props = $props(); + let posts = $state([]); let allPosts = $state([]); // Store all posts before filtering let loading = $state(true); @@ -61,6 +67,11 @@ // Load user lists for filtering async function loadUserLists() { + // Don't load user lists for single relay mode + if (singleRelay) { + return; + } + const session = sessionManager.getSession(); if (!session) return; @@ -241,6 +252,11 @@ return; } + // Don't set up subscription for single relay mode + if (singleRelay) { + return; + } + const relays = relayManager.getFeedReadRelays(); const filters = [{ kinds: [KIND.SHORT_TEXT_NOTE], limit: 20 }]; @@ -269,6 +285,11 @@ return; // Already set up } + // Don't set up periodic refresh for single relay mode + if (singleRelay) { + return; + } + // Refresh every 30 seconds refreshInterval = setInterval(async () => { try { @@ -336,44 +357,52 @@ loading = true; try { const config = nostrClient.getConfig(); - const relays = relayManager.getFeedReadRelays(); - // Load initial feed - use cache for fast initial load + // Use single relay if provided, otherwise use normal relay list + const relays = singleRelay ? [singleRelay] : relayManager.getFeedReadRelays(); + + // For single relay mode, disable cache completely + const useCache = !singleRelay; + const cacheResults = !singleRelay; + + // Load initial feed - use cache for fast initial load (unless single relay mode) const filters = [{ kinds: [KIND.SHORT_TEXT_NOTE], limit: 20 }]; const events = await nostrClient.fetchEvents( filters, relays, { - useCache: true, // Use cache for fast initial display - cacheResults: true, + useCache, // Disable cache for single relay mode + cacheResults, // Don't cache results for single relay mode // Don't use onUpdate here - subscriptions handle updates timeout: 10000 } ); // Also immediately query relays to ensure we get fresh data in background - // This runs in parallel but doesn't use onUpdate to avoid loops - nostrClient.fetchEvents( - filters, - relays, - { - useCache: false, // Force query relays - cacheResults: true, - // Don't use onUpdate - let subscriptions handle it - timeout: 10000 - } - ).then((newEvents) => { - // Only update if we got new events that aren't already in posts - if (newEvents.length > 0) { - const existingIds = new Set(posts.map(p => p.id)); - const trulyNew = newEvents.filter(e => !existingIds.has(e.id)); - if (trulyNew.length > 0) { - handleUpdate(trulyNew); + // Skip this for single relay mode + if (!singleRelay) { + nostrClient.fetchEvents( + filters, + relays, + { + useCache: false, // Force query relays + cacheResults: true, + // Don't use onUpdate - let subscriptions handle it + timeout: 10000 } - } - }).catch(error => { - console.debug('[FeedPage] Background relay query error:', error); - }); + ).then((newEvents) => { + // Only update if we got new events that aren't already in posts + if (newEvents.length > 0) { + const existingIds = new Set(posts.map(p => p.id)); + const trulyNew = newEvents.filter(e => !existingIds.has(e.id)); + if (trulyNew.length > 0) { + handleUpdate(trulyNew); + } + } + }).catch(error => { + console.debug('[FeedPage] Background relay query error:', error); + }); + } // Sort by created_at descending and deduplicate const uniqueMap = new Map(); @@ -413,7 +442,13 @@ loadingMore = true; try { const config = nostrClient.getConfig(); - const relays = relayManager.getFeedReadRelays(); + + // Use single relay if provided, otherwise use normal relay list + const relays = singleRelay ? [singleRelay] : relayManager.getFeedReadRelays(); + + // For single relay mode, disable cache completely + const useCache = !singleRelay; + const cacheResults = !singleRelay; const filters = [{ kinds: [KIND.SHORT_TEXT_NOTE], @@ -425,8 +460,8 @@ filters, relays, { - useCache: true, - cacheResults: true, + useCache, // Disable cache for single relay mode + cacheResults, // Don't cache results for single relay mode timeout: 10000 } ); @@ -560,14 +595,21 @@ const reactionRelays = relayManager.getProfileReadRelays(); const eventIds = postsToLoad.map(p => p.id); + // Use single relay if provided, otherwise use normal reaction relays + const relaysForReactions = singleRelay ? [singleRelay] : reactionRelays; + + // For single relay mode, disable cache completely + const useCache = !singleRelay; + const cacheResults = !singleRelay; + // Batch fetch all reactions for all posts in one query const allReactions = await nostrClient.fetchEvents( [ { kinds: [KIND.REACTION], '#e': eventIds, limit: 1000 }, { kinds: [KIND.REACTION], '#E': eventIds, limit: 1000 } ], - reactionRelays, - { useCache: true, cacheResults: true, timeout: 10000 } + relaysForReactions, + { useCache, cacheResults, timeout: 10000 } ); // Group reactions by event ID @@ -601,7 +643,7 @@
- {#if !loading && availableLists.length > 0} + {#if !loading && availableLists.length > 0 && !singleRelay}