Browse Source

more optimizations

fix bugs
master
Silberengel 1 month ago
parent
commit
9a23313c21
  1. 4
      public/healthz.json
  2. 6
      src/lib/components/content/MetadataCard.svelte
  3. 6
      src/lib/modules/feed/FeedPost.svelte
  4. 115
      src/routes/relay/+page.svelte
  5. 205
      src/routes/rss/+page.svelte

4
public/healthz.json

@ -2,7 +2,7 @@ @@ -2,7 +2,7 @@
"status": "ok",
"service": "aitherboard",
"version": "0.2.0",
"buildTime": "2026-02-07T07:05:23.594Z",
"buildTime": "2026-02-07T07:06:24.119Z",
"gitCommit": "unknown",
"timestamp": 1770447923594
"timestamp": 1770447984119
}

6
src/lib/components/content/MetadataCard.svelte

@ -135,11 +135,11 @@ @@ -135,11 +135,11 @@
<p class="metadata-author">Author: {author}</p>
{/if}
{#if !hasContent}
<!-- Show all tags when there's no content -->
{#if !hasContent && !isMediaKind}
<!-- Show all tags when there's no content, but skip for media kinds (MediaAttachments handles those) -->
<div class="metadata-tags">
{#each event.tags as tag}
{#if tag[0] !== 'image' && tag[0] !== 'description' && tag[0] !== 'summary' && tag[0] !== 'author' && tag[0] !== 'title' && tag[0] !== 'd'}
{#if tag[0] !== 'image' && tag[0] !== 'description' && tag[0] !== 'summary' && tag[0] !== 'author' && tag[0] !== 'title' && tag[0] !== 'd' && tag[0] !== 'imeta' && tag[0] !== 'file' && tag[0] !== 'alt' && tag[0] !== 'x' && tag[0] !== 'm'}
<div class="metadata-tag">
<span class="metadata-tag-name">{tag[0]}:</span>
{#each tag.slice(1) as value}

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

@ -890,13 +890,13 @@ @@ -890,13 +890,13 @@
<!-- Show referenced event preview in feed view -->
<ReferencedEventPreview event={post} preloadedReferencedEvent={preloadedReferencedEvent} />
<!-- Show metadata in feed view when content is empty -->
{#if !fullView && (!post.content || !post.content.trim())}
<!-- Show metadata in feed view when content is empty, but skip for media kinds (MediaAttachments handles those) -->
{#if !fullView && (!post.content || !post.content.trim()) && !isMediaKind}
<MetadataCard event={post} hideTitle={true} hideImageIfInMedia={true} />
{/if}
<div bind:this={contentElement} class="post-content mb-2" class:collapsed-content={!fullView && shouldCollapse && !isExpanded}>
{#if shouldAutoRenderMedia && (post.content && post.content.trim())}
{#if shouldAutoRenderMedia}
<MediaAttachments event={post} forceRender={isMediaKind} />
{/if}
<p class="text-fog-text dark:text-fog-dark-text whitespace-pre-wrap word-wrap">

115
src/routes/relay/+page.svelte

@ -7,6 +7,7 @@ @@ -7,6 +7,7 @@
import { goto } from '$app/navigation';
import { sessionManager } from '../../lib/services/auth/session-manager.js';
import { KIND } from '../../lib/types/kind-lookup.js';
import type { NostrEvent } from '../../lib/types/nostr.js';
interface RelayInfo {
url: string;
@ -82,12 +83,54 @@ @@ -82,12 +83,54 @@
relays = relayList;
// Load favorite relays if user is logged in
await loadFavoriteRelays();
// Load favorite relays if user is logged in (non-blocking - show page immediately)
loadFavoriteRelays().catch(err => {
console.debug('Error loading favorite relays in background:', err);
});
loading = false;
}
function processFavoriteRelayEvent(event: NostrEvent): void {
// Extract relay URLs from 'relay' tags (kind 10012 uses 'relay' tags)
const favoriteRelayUrls = new Set<string>();
for (const tag of event.tags) {
if (tag[0] === 'relay' && tag[1]) {
// Normalize URL: trim, remove trailing slash, ensure protocol
let url = tag[1].trim();
// Remove trailing slash
url = url.replace(/\/$/, '');
// If no protocol, assume wss://
if (!url.startsWith('ws://') && !url.startsWith('wss://')) {
url = `wss://${url}`;
}
favoriteRelayUrls.add(url);
}
}
// Get connection status - normalize URLs for comparison
const connectedRelays = nostrClient.getConnectedRelays();
const normalizeUrlForComparison = (url: string): string => {
return url.replace(/\/$/, '').toLowerCase();
};
const favoriteRelayList: RelayInfo[] = Array.from(favoriteRelayUrls).map(url => {
const normalizedUrl = normalizeUrlForComparison(url);
const isConnected = connectedRelays.some(connected =>
normalizeUrlForComparison(connected) === normalizedUrl
);
return {
url,
categories: ['Favorite'],
connected: isConnected
};
});
// Sort by URL
favoriteRelayList.sort((a, b) => a.url.localeCompare(b.url));
favoriteRelays = favoriteRelayList;
}
async function loadFavoriteRelays() {
const currentPubkey = sessionManager.getCurrentPubkey();
if (!currentPubkey) {
@ -95,63 +138,51 @@ @@ -95,63 +138,51 @@
return;
}
// Load from cache first (fast - instant display)
try {
const { getRecentCachedEvents } = await import('../../lib/services/cache/event-cache.js');
const cachedEvents = await getRecentCachedEvents([KIND.FAVORITE_RELAYS], 60 * 60 * 1000, 10); // 1 hour cache
const cachedFavoriteEvent = cachedEvents.find(e => e.pubkey === currentPubkey);
if (cachedFavoriteEvent) {
// Process cached event immediately
processFavoriteRelayEvent(cachedFavoriteEvent);
}
} catch (error) {
console.debug('Error loading cached favorite relays:', error);
}
try {
// Fetch kind 10012 (FAVORITE_RELAYS) event for current user
// Fetch multiple in case there are multiple replaceable events
const favoriteEvents = await nostrClient.fetchEvents(
[{ kinds: [KIND.FAVORITE_RELAYS], authors: [currentPubkey], limit: 10 }],
relayManager.getProfileReadRelays(),
{ useCache: true, cacheResults: true, timeout: 5000 }
{
useCache: 'cache-first', // Already shown cache above, now stream updates
cacheResults: true,
timeout: config.standardTimeout
}
);
if (favoriteEvents.length === 0) {
favoriteRelays = [];
if (favoriteRelays.length === 0) {
favoriteRelays = [];
}
return;
}
// Get the latest event (replaceable event, so should be only one, but take latest just in case)
const latestEvent = favoriteEvents.sort((a, b) => b.created_at - a.created_at)[0];
// Extract relay URLs from 'relay' tags (kind 10012 uses 'relay' tags)
const favoriteRelayUrls = new Set<string>();
for (const tag of latestEvent.tags) {
if (tag[0] === 'relay' && tag[1]) {
// Normalize URL: trim, remove trailing slash, ensure protocol
let url = tag[1].trim();
// Remove trailing slash
url = url.replace(/\/$/, '');
// If no protocol, assume wss://
if (!url.startsWith('ws://') && !url.startsWith('wss://')) {
url = `wss://${url}`;
}
favoriteRelayUrls.add(url);
}
}
// Get connection status - normalize URLs for comparison
const connectedRelays = nostrClient.getConnectedRelays();
const normalizeUrlForComparison = (url: string): string => {
return url.replace(/\/$/, '').toLowerCase();
};
const favoriteRelayList: RelayInfo[] = Array.from(favoriteRelayUrls).map(url => {
const normalizedUrl = normalizeUrlForComparison(url);
const isConnected = connectedRelays.some(connected =>
normalizeUrlForComparison(connected) === normalizedUrl
);
return {
url,
categories: ['Favorite'],
connected: isConnected
};
});
// Sort by URL
favoriteRelayList.sort((a, b) => a.url.localeCompare(b.url));
favoriteRelays = favoriteRelayList;
// Process the event (update favorite relays with fresh data)
processFavoriteRelayEvent(latestEvent);
} catch (err) {
console.error('Error loading favorite relays:', err);
favoriteRelays = [];
// Don't clear favoriteRelays if we have cached data
if (favoriteRelays.length === 0) {
favoriteRelays = [];
}
}
}

205
src/routes/rss/+page.svelte

@ -3,6 +3,7 @@ @@ -3,6 +3,7 @@
import { sessionManager } from '../../lib/services/auth/session-manager.js';
import { nostrClient } from '../../lib/services/nostr/nostr-client.js';
import { relayManager } from '../../lib/services/nostr/relay-manager.js';
import { config } from '../../lib/services/nostr/config.js';
import { cacheRSSFeed, getCachedRSSFeed, type RSSItem } from '../../lib/services/cache/rss-cache.js';
import { onMount } from 'svelte';
import { goto } from '$app/navigation';
@ -22,6 +23,8 @@ @@ -22,6 +23,8 @@
let rssItems = $state<RSSItem[]>([]);
let feedErrors = $state<Map<string, string>>(new Map());
let feedsLoaded = $state(false); // Track if feeds have been loaded to prevent re-triggering
let currentPage = $state(1);
const itemsPerPage = 50;
let subscribedFeeds = $derived.by(() => {
if (!rssEvent) return [];
@ -33,6 +36,16 @@ @@ -33,6 +36,16 @@
// Track the last feeds we loaded to detect changes
let lastLoadedFeeds = $state<string[]>([]);
// Computed: get items for current page
let paginatedItems = $derived.by(() => {
const start = (currentPage - 1) * itemsPerPage;
const end = start + itemsPerPage;
return rssItems.slice(start, end);
});
// Computed: total pages
let totalPages = $derived.by(() => Math.ceil(rssItems.length / itemsPerPage));
onMount(async () => {
await nostrClient.initialize();
const session = sessionManager.getSession();
@ -61,19 +74,53 @@ @@ -61,19 +74,53 @@
rssItems = [];
feedErrors.clear();
lastLoadedFeeds = [];
currentPage = 1;
}
});
// Reset to page 1 when items change significantly
$effect(() => {
if (rssItems.length > 0 && currentPage > totalPages) {
currentPage = 1;
}
});
async function checkRssEvent() {
if (!currentPubkey) return;
loading = true;
// Load from cache first (fast - instant display)
try {
const { getRecentCachedEvents } = await import('../../lib/services/cache/event-cache.js');
const cachedEvents = await getRecentCachedEvents([RSS_FEED_KIND], 60 * 60 * 1000, 1); // 1 hour cache
const cachedRssEvent = cachedEvents.find(e => e.pubkey === currentPubkey);
if (cachedRssEvent) {
// Only update if it's actually different (prevents unnecessary re-renders)
if (!rssEvent || rssEvent.id !== cachedRssEvent.id) {
rssEvent = cachedRssEvent;
// Reset feeds loaded state when event changes
feedsLoaded = false;
lastLoadedFeeds = [];
}
loading = false; // Show cached content immediately
} else {
loading = true; // Only show loading if no cache
}
} catch (error) {
console.debug('Error loading cached RSS event:', error);
loading = true; // Show loading if cache check fails
}
try {
const relays = relayManager.getProfileReadRelays();
const events = await nostrClient.fetchEvents(
[{ kinds: [RSS_FEED_KIND], authors: [currentPubkey], limit: 1 }],
relays,
{ useCache: true, cacheResults: true }
{
useCache: 'cache-first', // Already shown cache above, now stream updates
cacheResults: true,
timeout: config.standardTimeout
}
);
if (events.length > 0) {
@ -84,6 +131,7 @@ @@ -84,6 +131,7 @@
// Reset feeds loaded state when event changes
feedsLoaded = false;
lastLoadedFeeds = [];
currentPage = 1; // Reset to first page when event changes
}
}
} catch (error) {
@ -133,15 +181,24 @@ @@ -133,15 +181,24 @@
if (feedsToFetch.length > 0) {
const fetchPromises = feedsToFetch.map(async (feedUrl) => {
try {
const items = await fetchRssFeed(feedUrl);
try {
// Add timeout to prevent hanging requests
const timeoutPromise = new Promise<never>((_, reject) => {
setTimeout(() => reject(new Error('Request timeout')), 10000); // 10 second timeout
});
const items = await Promise.race([
fetchRssFeed(feedUrl),
timeoutPromise
]);
// Cache the fetched items
await cacheRSSFeed(feedUrl, items);
return { feedUrl, items };
} catch (error) {
} catch (error) {
// Only log non-CORS errors to avoid console spam
const errorMessage = error instanceof Error ? error.message : 'Failed to fetch feed';
if (!errorMessage.includes('CORS') && !errorMessage.includes('Cross-Origin')) {
if (!errorMessage.includes('CORS') && !errorMessage.includes('Cross-Origin') && !errorMessage.includes('timeout')) {
console.error(`Error fetching RSS feed ${feedUrl}:`, error);
}
feedErrors.set(feedUrl, errorMessage);
@ -166,11 +223,12 @@ @@ -166,11 +223,12 @@
itemMap.set(item.link, item);
}
// Sort by date (newest first)
// Sort by date (newest first)
const combinedItems = Array.from(itemMap.values());
combinedItems.sort((a, b) => b.pubDate.getTime() - a.pubDate.getTime());
rssItems = combinedItems;
currentPage = 1; // Reset to first page when items update
}
feedsLoaded = true;
@ -383,8 +441,49 @@ @@ -383,8 +441,49 @@
{:else if rssItems.length === 0}
<p class="text-fog-text-light dark:text-fog-dark-text-light">No RSS items found.</p>
{:else}
<div class="rss-items-info mb-4">
<p class="text-fog-text dark:text-fog-dark-text text-sm">
Showing {paginatedItems.length} of {rssItems.length} items
{#if totalPages > 1}
(Page {currentPage} of {totalPages})
{/if}
</p>
</div>
{#if totalPages > 1}
<div class="pagination pagination-top mb-4">
<button
class="pagination-button"
disabled={currentPage === 1}
onclick={() => {
if (currentPage > 1) currentPage--;
}}
aria-label="Previous page"
>
Previous
</button>
<div class="pagination-info">
<span class="text-fog-text dark:text-fog-dark-text">
Page {currentPage} of {totalPages}
</span>
</div>
<button
class="pagination-button"
disabled={currentPage === totalPages}
onclick={() => {
if (currentPage < totalPages) currentPage++;
}}
aria-label="Next page"
>
Next
</button>
</div>
{/if}
<div class="rss-items">
{#each rssItems as item (item.link + item.pubDate.getTime())}
{#each paginatedItems as item (item.link + item.pubDate.getTime())}
<article class="rss-item">
<div class="rss-item-header">
<div class="rss-item-meta">
@ -435,6 +534,38 @@ @@ -435,6 +534,38 @@
</article>
{/each}
</div>
{#if totalPages > 1}
<div class="pagination pagination-bottom mt-4">
<button
class="pagination-button"
disabled={currentPage === 1}
onclick={() => {
if (currentPage > 1) currentPage--;
}}
aria-label="Previous page"
>
Previous
</button>
<div class="pagination-info">
<span class="text-fog-text dark:text-fog-dark-text">
Page {currentPage} of {totalPages}
</span>
</div>
<button
class="pagination-button"
disabled={currentPage === totalPages}
onclick={() => {
if (currentPage < totalPages) currentPage++;
}}
aria-label="Next page"
>
Next
</button>
</div>
{/if}
{/if}
</div>
{/if}
@ -623,4 +754,62 @@ @@ -623,4 +754,62 @@
color: var(--fog-dark-text, #cbd5e1);
}
.rss-items-info {
padding: 0.5rem 0;
}
.pagination {
display: flex;
justify-content: center;
align-items: center;
gap: 1rem;
margin-top: 2rem;
margin-bottom: 2rem;
}
.pagination-top {
margin-top: 0;
margin-bottom: 1rem;
}
.pagination-bottom {
margin-top: 2rem;
}
.pagination-button {
padding: 0.5rem 1rem;
border: 1px solid var(--fog-border, #e5e7eb);
border-radius: 0.25rem;
background: var(--fog-post, #ffffff);
color: var(--fog-text, #1e293b);
cursor: pointer;
transition: all 0.2s;
}
.pagination-button:hover:not(:disabled) {
background: var(--fog-highlight, #f1f5f9);
border-color: var(--fog-accent, #94a3b8);
}
.pagination-button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
:global(.dark) .pagination-button {
background: var(--fog-dark-post, #1f2937);
border-color: var(--fog-dark-border, #374151);
color: var(--fog-dark-text, #f9fafb);
}
:global(.dark) .pagination-button:hover:not(:disabled) {
background: var(--fog-dark-highlight, #475569);
border-color: var(--fog-dark-accent, #64748b);
}
.pagination-info {
min-width: 120px;
text-align: center;
}
</style>

Loading…
Cancel
Save