Browse Source

build fixes

add relay page
master
Silberengel 1 month ago
parent
commit
70bed530af
  1. 11
      ideas.txt
  2. 4
      public/healthz.json
  3. 9
      src/lib/components/content/HighlightOverlay.svelte
  4. 417
      src/lib/components/relay/RelayInfo.svelte
  5. 10
      src/lib/components/write/EditEventForm.svelte
  6. 149
      src/lib/modules/feed/FeedPage.svelte
  7. 1
      src/lib/services/nostr/nip30-emoji.ts
  8. 17
      src/lib/services/nostr/nostr-client.ts
  9. 91
      src/routes/feed/relay/explore/relays/[relay]/+page.svelte

11
ideas.txt

@ -87,3 +87,14 @@ Clicking create should ask them to enter a kind they would like to write: 1, 11, @@ -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.

4
public/healthz.json

@ -2,7 +2,7 @@ @@ -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
}

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

@ -8,9 +8,10 @@ @@ -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<HTMLElement | null>(null);
let drawerOpen = $state(false);
@ -91,7 +92,7 @@ @@ -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 @@ @@ -103,7 +104,7 @@
</script>
<div class="highlight-overlay" bind:this={containerRef}>
<slot />
{@render children()}
{#if hoveredHighlight}
<div
@ -111,7 +112,7 @@ @@ -111,7 +112,7 @@
style="top: {tooltipPosition.top}px; left: {tooltipPosition.left}px;"
>
<ProfileBadge pubkey={hoveredHighlight.pubkey} />
<button class="view-highlight-button" onclick={() => openHighlight(hoveredHighlight)}>
<button class="view-highlight-button" onclick={() => hoveredHighlight && openHighlight(hoveredHighlight)}>
View the highlight
</button>
</div>

417
src/lib/components/relay/RelayInfo.svelte

@ -0,0 +1,417 @@ @@ -0,0 +1,417 @@
<script lang="ts">
import { nostrClient } from '../../services/nostr/nostr-client.js';
import { Relay } from 'nostr-tools';
import { onMount } from 'svelte';
interface Props {
relayUrl: string;
}
let { relayUrl }: Props = $props();
interface RelayMetadata {
name?: string;
description?: string;
pubkey?: string;
contact?: string;
supported_nips?: number[];
software?: string;
version?: string;
limitation?: {
max_message_length?: number;
max_subscriptions?: number;
max_filters?: number;
max_limit?: number;
max_subid_length?: number;
min_prefix?: number;
max_event_tags?: number;
max_content_length?: number;
min_pow_difficulty?: number;
auth_required?: boolean;
payment_required?: boolean;
restricted_writes?: boolean;
};
}
let metadata = $state<RelayMetadata | null>(null);
let loading = $state(true);
let error = $state<string | null>(null);
let connectionStatus = $state<'connecting' | 'connected' | 'disconnected'>('connecting');
let eventCount = $state<number | null>(null);
async function fetchRelayMetadata() {
loading = true;
error = null;
try {
// Get relay connection status from nostr-client
const relay = await nostrClient.getRelay(relayUrl);
if (relay) {
// Check connection status
const status = (relay as any).status;
if (status === 1) {
connectionStatus = 'connected';
} else if (status === 0) {
connectionStatus = 'connecting';
} else {
connectionStatus = 'disconnected';
}
// Fetch NIP-11 metadata via HTTP GET
try {
// Convert ws:// or wss:// to http:// or https://
const httpUrl = relayUrl.replace(/^wss?:\/\//, (match) => {
return match === 'wss://' ? 'https://' : 'http://';
});
const nip11Url = `${httpUrl}/.well-known/nostr.json`;
const response = await fetch(nip11Url, {
method: 'GET',
headers: {
'Accept': 'application/nostr+json'
}
});
if (response.ok) {
const info = await response.json();
if (info) {
metadata = info as RelayMetadata;
}
}
} catch (err) {
console.debug(`[RelayInfo] Could not fetch NIP-11 metadata for ${relayUrl}:`, err);
// Not all relays support NIP-11, so this is not a critical error
}
} else {
connectionStatus = 'disconnected';
error = 'Failed to connect to relay';
}
} catch (err) {
console.error(`[RelayInfo] Error fetching relay info for ${relayUrl}:`, err);
error = err instanceof Error ? err.message : 'Unknown error';
connectionStatus = 'disconnected';
} finally {
loading = false;
}
}
async function getEventCount() {
try {
// Fetch a small sample to estimate activity
const events = await nostrClient.fetchEvents(
[{ kinds: [1], limit: 1 }],
[relayUrl],
{ useCache: false, cacheResults: false, timeout: 5000 }
);
// This is just a connectivity check, not a real count
// Real event count would require a COUNT query which not all relays support
eventCount = events.length > 0 ? -1 : 0; // -1 means "has events" but we don't know the count
} catch (err) {
// Silently fail - this is just informational
eventCount = null;
}
}
onMount(async () => {
await fetchRelayMetadata();
await getEventCount();
});
function getStatusColor(status: string): string {
switch (status) {
case 'connected':
return 'text-green-600 dark:text-green-400';
case 'connecting':
return 'text-yellow-600 dark:text-yellow-400';
case 'disconnected':
return 'text-red-600 dark:text-red-400';
default:
return 'text-gray-600 dark:text-gray-400';
}
}
function getStatusIcon(status: string): string {
switch (status) {
case 'connected':
return '●';
case 'connecting':
return '○';
case 'disconnected':
return '○';
default:
return '○';
}
}
</script>
<div class="relay-info-card">
{#if loading}
<div class="relay-info-loading">
<p class="text-fog-text-light dark:text-fog-dark-text-light">Loading relay information...</p>
</div>
{:else if error}
<div class="relay-info-error">
<p class="text-fog-text dark:text-fog-dark-text">Error: {error}</p>
</div>
{:else}
<div class="relay-info-header">
<div class="relay-info-title-row">
<h3 class="relay-info-title">Relay Information</h3>
<div class="relay-status">
<span class="relay-status-icon {getStatusColor(connectionStatus)}">{getStatusIcon(connectionStatus)}</span>
<span class="relay-status-text {getStatusColor(connectionStatus)}">
{connectionStatus.charAt(0).toUpperCase() + connectionStatus.slice(1)}
</span>
</div>
</div>
<code class="relay-url">{relayUrl}</code>
</div>
{#if metadata}
<div class="relay-info-content">
{#if metadata.name}
<div class="relay-info-item">
<span class="relay-info-label">Name:</span>
<span class="relay-info-value">{metadata.name}</span>
</div>
{/if}
{#if metadata.description}
<div class="relay-info-item">
<span class="relay-info-label">Description:</span>
<span class="relay-info-value">{metadata.description}</span>
</div>
{/if}
{#if metadata.software}
<div class="relay-info-item">
<span class="relay-info-label">Software:</span>
<span class="relay-info-value">{metadata.software}{metadata.version ? ` ${metadata.version}` : ''}</span>
</div>
{/if}
{#if metadata.contact}
<div class="relay-info-item">
<span class="relay-info-label">Contact:</span>
<span class="relay-info-value">
{#if metadata.contact.startsWith('mailto:')}
<a href="{metadata.contact}" class="relay-info-link">{metadata.contact.replace('mailto:', '')}</a>
{:else if metadata.contact.startsWith('http')}
<a href="{metadata.contact}" target="_blank" rel="noopener noreferrer" class="relay-info-link">{metadata.contact}</a>
{:else}
{metadata.contact}
{/if}
</span>
</div>
{/if}
{#if metadata.pubkey}
<div class="relay-info-item">
<span class="relay-info-label">Pubkey:</span>
<code class="relay-info-value relay-pubkey">{metadata.pubkey.substring(0, 16)}...</code>
</div>
{/if}
{#if metadata.supported_nips && metadata.supported_nips.length > 0}
<div class="relay-info-item">
<span class="relay-info-label">Supported NIPs:</span>
<span class="relay-info-value">
{metadata.supported_nips.slice(0, 20).join(', ')}{metadata.supported_nips.length > 20 ? '...' : ''}
</span>
</div>
{/if}
{#if metadata.limitation}
<div class="relay-info-limitations">
<span class="relay-info-label">Limitations:</span>
<div class="relay-limitations-list">
{#if metadata.limitation.max_message_length}
<span class="relay-limitation-item">Max message: {metadata.limitation.max_message_length} bytes</span>
{/if}
{#if metadata.limitation.max_subscriptions}
<span class="relay-limitation-item">Max subscriptions: {metadata.limitation.max_subscriptions}</span>
{/if}
{#if metadata.limitation.max_filters}
<span class="relay-limitation-item">Max filters: {metadata.limitation.max_filters}</span>
{/if}
{#if metadata.limitation.max_limit}
<span class="relay-limitation-item">Max limit: {metadata.limitation.max_limit}</span>
{/if}
{#if metadata.limitation.auth_required}
<span class="relay-limitation-item">Auth required</span>
{/if}
{#if metadata.limitation.payment_required}
<span class="relay-limitation-item">Payment required</span>
{/if}
{#if metadata.limitation.restricted_writes}
<span class="relay-limitation-item">Restricted writes</span>
{/if}
</div>
</div>
{/if}
</div>
{:else}
<div class="relay-info-no-metadata">
<p class="text-fog-text-light dark:text-fog-dark-text-light">No NIP-11 metadata available</p>
</div>
{/if}
{/if}
</div>
<style>
.relay-info-card {
background: var(--fog-post, #ffffff);
border: 1px solid var(--fog-border, #e5e7eb);
border-radius: 0.5rem;
padding: 1.5rem;
margin-bottom: 1.5rem;
}
:global(.dark) .relay-info-card {
background: var(--fog-dark-post, #1f2937);
border-color: var(--fog-dark-border, #374151);
}
.relay-info-header {
margin-bottom: 1rem;
padding-bottom: 1rem;
border-bottom: 1px solid var(--fog-border, #e5e7eb);
}
:global(.dark) .relay-info-header {
border-bottom-color: var(--fog-dark-border, #374151);
}
.relay-info-title-row {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 0.5rem;
}
.relay-info-title {
margin: 0;
font-size: 1.25rem;
font-weight: 600;
color: var(--fog-text, #1f2937);
}
:global(.dark) .relay-info-title {
color: var(--fog-dark-text, #f9fafb);
}
.relay-status {
display: flex;
align-items: center;
gap: 0.5rem;
}
.relay-status-icon {
font-size: 0.75rem;
}
.relay-status-text {
font-size: 0.875rem;
font-weight: 500;
}
.relay-url {
display: block;
font-family: monospace;
font-size: 0.875rem;
color: var(--fog-text-light, #9ca3af);
word-break: break-all;
}
:global(.dark) .relay-url {
color: var(--fog-dark-text-light, #6b7280);
}
.relay-info-content {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.relay-info-item {
display: flex;
flex-direction: column;
gap: 0.25rem;
}
.relay-info-label {
font-size: 0.875rem;
font-weight: 600;
color: var(--fog-text, #1f2937);
}
:global(.dark) .relay-info-label {
color: var(--fog-dark-text, #f9fafb);
}
.relay-info-value {
font-size: 0.875rem;
color: var(--fog-text-light, #6b7280);
word-break: break-word;
}
:global(.dark) .relay-info-value {
color: var(--fog-dark-text-light, #9ca3af);
}
.relay-pubkey {
font-family: monospace;
}
.relay-info-link {
color: var(--fog-accent, #64748b);
text-decoration: underline;
}
.relay-info-link:hover {
color: var(--fog-text, #1f2937);
}
:global(.dark) .relay-info-link {
color: var(--fog-dark-accent, #94a3b8);
}
:global(.dark) .relay-info-link:hover {
color: var(--fog-dark-text, #f9fafb);
}
.relay-info-limitations {
display: flex;
flex-direction: column;
gap: 0.5rem;
margin-top: 0.5rem;
}
.relay-limitations-list {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
}
.relay-limitation-item {
font-size: 0.75rem;
padding: 0.25rem 0.5rem;
background: var(--fog-highlight, #f3f4f6);
border: 1px solid var(--fog-border, #e5e7eb);
border-radius: 0.25rem;
color: var(--fog-text, #1f2937);
}
:global(.dark) .relay-limitation-item {
background: var(--fog-dark-highlight, #374151);
border-color: var(--fog-dark-border, #475569);
color: var(--fog-dark-text, #f9fafb);
}
.relay-info-loading,
.relay-info-error,
.relay-info-no-metadata {
padding: 1rem;
text-align: center;
}
</style>

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

@ -13,12 +13,18 @@ @@ -13,12 +13,18 @@
let { event }: Props = $props();
let content = $state(event.content || '');
let tags = $state<string[][]>([...event.tags]);
let content = $state('');
let tags = $state<string[][]>([]);
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, ['', '']];
}

149
src/lib/modules/feed/FeedPage.svelte

@ -8,6 +8,12 @@ @@ -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<NostrEvent[]>([]);
let allPosts = $state<NostrEvent[]>([]); // Store all posts before filtering
let loading = $state(true);
@ -61,6 +67,11 @@ @@ -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 @@ @@ -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 @@ @@ -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 @@ @@ -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<string, NostrEvent>();
@ -413,7 +442,13 @@ @@ -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 @@ @@ -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 @@ @@ -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 @@ @@ -601,7 +643,7 @@
</script>
<div class="feed-page">
{#if !loading && availableLists.length > 0}
{#if !loading && availableLists.length > 0 && !singleRelay}
<div class="feed-filter">
<label for="list-filter" class="filter-label">Filter by list:</label>
<select
@ -618,6 +660,14 @@ @@ -618,6 +660,14 @@
</div>
{/if}
{#if singleRelay}
<div class="relay-info">
<p class="relay-info-text">
Showing feed from: <code class="relay-url">{singleRelay}</code>
</p>
</div>
{/if}
{#if loading}
<div class="loading-state">
<p class="text-fog-text dark:text-fog-dark-text">Loading feed...</p>
@ -722,4 +772,41 @@ @@ -722,4 +772,41 @@
text-align: center;
min-height: 100px;
}
.relay-info {
margin-bottom: 1.5rem;
padding: 1rem;
background: var(--fog-highlight, #f3f4f6);
border: 1px solid var(--fog-border, #e5e7eb);
border-radius: 0.375rem;
}
:global(.dark) .relay-info {
background: var(--fog-dark-highlight, #374151);
border-color: var(--fog-dark-border, #475569);
}
.relay-info-text {
margin: 0;
font-size: 0.875rem;
color: var(--fog-text, #1f2937);
}
:global(.dark) .relay-info-text {
color: var(--fog-dark-text, #f9fafb);
}
.relay-url {
font-family: monospace;
font-size: 0.875rem;
background: var(--fog-post, #ffffff);
padding: 0.25rem 0.5rem;
border-radius: 0.25rem;
border: 1px solid var(--fog-border, #e5e7eb);
}
:global(.dark) .relay-url {
background: var(--fog-dark-post, #1f2937);
border-color: var(--fog-dark-border, #374151);
}
</style>

1
src/lib/services/nostr/nip30-emoji.ts

@ -14,7 +14,6 @@ import { nostrClient } from './nostr-client.js'; @@ -14,7 +14,6 @@ import { nostrClient } from './nostr-client.js';
import { relayManager } from './relay-manager.js';
import type { NostrEvent } from '../../types/nostr.js';
import { KIND } from '../../types/kind-lookup.js';
import { matchAll } from 'nostr-tools/nip30';
export interface EmojiDefinition {
shortcode: string; // Without colons, e.g., "turtlehappy_sm"

17
src/lib/services/nostr/nostr-client.ts

@ -213,6 +213,23 @@ class NostrClient { @@ -213,6 +213,23 @@ class NostrClient {
return true;
}
/**
* Get a relay instance by URL
* Will connect if not already connected
*/
async getRelay(url: string): Promise<Relay | null> {
// Ensure relay is connected
if (!this.relays.has(url)) {
try {
await this.addRelay(url);
} catch (error) {
console.debug(`[nostr-client] Failed to connect to relay ${url}:`, error);
return null;
}
}
return this.relays.get(url) || null;
}
private shouldFilterZapReceipt(event: NostrEvent): boolean {
if (event.kind !== KIND.ZAP_RECEIPT) return false;
const amountTag = event.tags.find((t) => t[0] === 'amount');

91
src/routes/feed/relay/explore/relays/[relay]/+page.svelte

@ -0,0 +1,91 @@ @@ -0,0 +1,91 @@
<script lang="ts">
import Header from '../../../../../../lib/components/layout/Header.svelte';
import FeedPage from '../../../../../../lib/modules/feed/FeedPage.svelte';
import SearchBox from '../../../../../../lib/components/layout/SearchBox.svelte';
import RelayInfo from '../../../../../../lib/components/relay/RelayInfo.svelte';
import { nostrClient } from '../../../../../../lib/services/nostr/nostr-client.js';
import { onMount } from 'svelte';
import { page } from '$app/stores';
let decodedRelay = $state<string | null>(null);
let error = $state<string | null>(null);
function decodeRelayUrl(encoded: string): string | null {
try {
// Decode the URL-encoded relay URI
const decoded = decodeURIComponent(encoded);
// Validate it's a websocket URI
if (!decoded.startsWith('ws://') && !decoded.startsWith('wss://')) {
return null;
}
return decoded;
} catch (e) {
console.error('Error decoding relay URL:', e);
return null;
}
}
onMount(async () => {
await nostrClient.initialize();
if ($page.params.relay) {
const decoded = decodeRelayUrl($page.params.relay);
if (decoded) {
decodedRelay = decoded;
} else {
error = 'Invalid relay URL. Must be a ws:// or wss:// URI.';
}
} else {
error = 'No relay specified.';
}
});
$effect(() => {
if ($page.params.relay) {
const decoded = decodeRelayUrl($page.params.relay);
if (decoded) {
decodedRelay = decoded;
error = null;
} else {
error = 'Invalid relay URL. Must be a ws:// or wss:// URI.';
decodedRelay = null;
}
}
});
</script>
<Header />
<main class="container mx-auto px-4 py-8">
<div class="search-section mb-6">
<SearchBox />
</div>
{#if error}
<div class="error-state">
<p class="text-fog-text dark:text-fog-dark-text">{error}</p>
</div>
{:else if decodedRelay}
<RelayInfo relayUrl={decodedRelay} />
<FeedPage singleRelay={decodedRelay} />
{:else}
<div class="loading-state">
<p class="text-fog-text dark:text-fog-dark-text">Loading relay feed...</p>
</div>
{/if}
</main>
<style>
main {
max-width: var(--content-width);
margin: 0 auto;
}
.error-state,
.loading-state {
padding: 2rem;
text-align: center;
}
</style>
Loading…
Cancel
Save