Browse Source

bookmark paging

master
Silberengel 1 month ago
parent
commit
bbe4467c00
  1. 18
      src/lib/components/content/MediaAttachments.svelte
  2. 2
      src/lib/components/content/ReplyContext.svelte
  3. 4
      src/lib/components/layout/Header.svelte
  4. 6
      src/lib/components/layout/ProfileBadge.svelte
  5. 22
      src/lib/modules/discussions/DiscussionCard.svelte
  6. 2
      src/lib/modules/discussions/DiscussionVoteButtons.svelte
  7. 28
      src/lib/modules/feed/FeedPage.svelte
  8. 78
      src/lib/modules/feed/FeedPost.svelte
  9. 12
      src/lib/modules/feed/ThreadDrawer.svelte
  10. 26
      src/lib/modules/profiles/ProfilePage.svelte
  11. 2
      src/lib/modules/reactions/FeedReactionButtons.svelte
  12. 2
      src/lib/services/nostr/event-hierarchy.ts
  13. 2
      src/lib/services/nostr/nip30-emoji.ts
  14. 4
      src/routes/repos/[naddr]/+page.svelte
  15. 14
      src/routes/rss/+page.svelte
  16. 4
      src/routes/topics/[name]/+page.svelte

18
src/lib/components/content/MediaAttachments.svelte

@ -6,7 +6,7 @@ @@ -6,7 +6,7 @@
}
let { event }: Props = $props();
interface MediaItem {
url: string;
type: 'image' | 'video' | 'audio' | 'file';
@ -216,8 +216,8 @@ @@ -216,8 +216,8 @@
preload="metadata"
class="max-w-full rounded"
style="max-height: 500px;"
autoplay={false}
muted={false}
autoplay={false}
muted={false}
>
<track kind="captions" />
Your browser does not support the video tag.
@ -225,13 +225,13 @@ @@ -225,13 +225,13 @@
</div>
{:else if item.type === 'audio'}
<div class="media-item">
<audio
src={item.url}
controls
<audio
src={item.url}
controls
preload="metadata"
class="w-full"
autoplay={false}
>
class="w-full"
autoplay={false}
>
Your browser does not support the audio tag.
</audio>
</div>

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

@ -66,7 +66,7 @@ @@ -66,7 +66,7 @@
} finally {
// Only clear loading state if this is still the current load
if (lastLoadAttemptId === eventId) {
loadingParent = false;
loadingParent = false;
}
}
}

4
src/lib/components/layout/Header.svelte

@ -110,7 +110,7 @@ @@ -110,7 +110,7 @@
nav {
min-width: 0; /* Allow flex items to shrink */
}
/* Responsive navigation links */
.nav-links {
font-size: 0.65rem;
@ -129,7 +129,7 @@ @@ -129,7 +129,7 @@
@media (min-width: 640px) {
.nav-brand {
font-size: 1rem;
}
}
}
/* Ensure navigation items don't overflow */

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

@ -59,9 +59,9 @@ @@ -59,9 +59,9 @@
const p = await fetchProfile(currentPubkey);
// Only update if pubkey hasn't changed during load
if (pubkey === currentPubkey) {
if (p) {
profile = p;
}
if (p) {
profile = p;
}
lastLoadedPubkey = currentPubkey;
}
} finally {

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

@ -68,7 +68,7 @@ @@ -68,7 +68,7 @@
function toggleExpanded() {
expanded = !expanded;
}
async function loadStats() {
// Prevent duplicate loads for the same event
if (loadingStats || lastStatsLoadEventId === thread.id) {
@ -204,7 +204,7 @@ @@ -204,7 +204,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}" class="card-link">
<div class="card-content" class:expanded={expanded} bind:this={contentElement}>
<div class="flex justify-between items-start mb-2">
<h3 class="font-semibold text-fog-text dark:text-fog-dark-text">
@ -302,15 +302,15 @@ @@ -302,15 +302,15 @@
/>
{/if}
{#if !fullView}
{#if loadingStats}
<span class="text-fog-text-light dark:text-fog-dark-text-light">Loading stats...</span>
{:else}
<span class="font-medium vote-comment-spacing">{commentCount} {commentCount === 1 ? 'comment' : 'comments'}</span>
{#if latestResponseTime}
<span class="text-fog-text-light dark:text-fog-dark-text-light">Last: {getLatestResponseTime()}</span>
{/if}
{#if zapCount > 0}
<span class="font-medium">{zapTotal.toLocaleString()} sats ({zapCount})</span>
{#if loadingStats}
<span class="text-fog-text-light dark:text-fog-dark-text-light">Loading stats...</span>
{:else}
<span class="font-medium vote-comment-spacing">{commentCount} {commentCount === 1 ? 'comment' : 'comments'}</span>
{#if latestResponseTime}
<span class="text-fog-text-light dark:text-fog-dark-text-light">Last: {getLatestResponseTime()}</span>
{/if}
{#if zapCount > 0}
<span class="font-medium">{zapTotal.toLocaleString()} sats ({zapCount})</span>
{/if}
{/if}
{:else}

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

@ -244,7 +244,7 @@ @@ -244,7 +244,7 @@
// Cache of checked reaction IDs to avoid repeated deletion checks
const checkedReactionIds = new Set<string>();
async function filterDeletedReactions(reactions: NostrEvent[]): Promise<NostrEvent[]> {
if (reactions.length === 0) return reactions;

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

@ -32,7 +32,7 @@ @@ -32,7 +32,7 @@
let isMounted = $state(true);
let initialLoadComplete = $state(false);
let loadInProgress = $state(false);
// Load waiting room events into feed
function loadWaitingRoomEvents() {
if (waitingRoomEvents.length === 0) return;
@ -67,9 +67,9 @@ @@ -67,9 +67,9 @@
filters,
relays,
{
relayFirst: true,
useCache: true,
cacheResults: true,
relayFirst: true,
useCache: true,
cacheResults: true,
timeout: config.standardTimeout
}
);
@ -89,7 +89,7 @@ @@ -89,7 +89,7 @@
const eventMap = new Map(events.map(e => [e.id, e]));
for (const event of filtered) {
eventMap.set(event.id, event);
}
}
const sorted = Array.from(eventMap.values()).sort((a, b) => b.created_at - a.created_at);
events = sorted;
@ -115,10 +115,10 @@ @@ -115,10 +115,10 @@
const feedKinds = getFeedKinds().filter(k => k !== KIND.DISCUSSION_THREAD);
const cached = await getRecentFeedEvents(feedKinds, 15 * 60 * 1000, config.feedLimit);
const filtered = cached.filter(e =>
e.kind !== KIND.DISCUSSION_THREAD &&
getKindInfo(e.kind).showInFeed === true
);
e.kind !== KIND.DISCUSSION_THREAD &&
getKindInfo(e.kind).showInFeed === true
);
if (filtered.length > 0 && isMounted) {
const unique = Array.from(new Map(filtered.map(e => [e.id, e])).values());
events = unique.sort((a, b) => b.created_at - a.created_at);
@ -136,9 +136,9 @@ @@ -136,9 +136,9 @@
relayError = `Relay ${singleRelay} is unavailable.`;
loading = false;
return;
}
}
}
const feedKinds = getFeedKinds().filter(k => k !== KIND.DISCUSSION_THREAD);
const filters = feedKinds.map(k => ({ kinds: [k], limit: config.feedLimit }));
@ -168,8 +168,8 @@ @@ -168,8 +168,8 @@
const eventMap = new Map(events.map(e => [e.id, e]));
for (const event of filtered) {
eventMap.set(event.id, event);
}
}
const sorted = Array.from(eventMap.values()).sort((a, b) => b.created_at - a.created_at);
events = sorted;
@ -206,7 +206,7 @@ @@ -206,7 +206,7 @@
const eventIds = new Set([...events.map(e => e.id), ...waitingRoomEvents.map(e => e.id)]);
if (!eventIds.has(event.id)) {
waitingRoomEvents = [...waitingRoomEvents, event].sort((a, b) => b.created_at - a.created_at);
}
}
},
() => {}
);

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

@ -27,7 +27,7 @@ @@ -27,7 +27,7 @@
}
let { post, fullView = false, preloadedReactions, parentEvent: providedParentEvent, quotedEvent: providedQuotedEvent }: Props = $props();
// Check if this event is bookmarked (async, so we use state)
// Only check if user is logged in
let bookmarked = $state(false);
@ -93,7 +93,7 @@ @@ -93,7 +93,7 @@
if (isLoggedIn) {
isBookmarked(post.id).then(b => {
if (!cancelled) {
bookmarked = b;
bookmarked = b;
}
});
} else {
@ -462,8 +462,8 @@ @@ -462,8 +462,8 @@
const urlWithoutProtocol = normalized.replace(/^https?:\/\//i, '');
if (content.includes(normalized.toLowerCase()) || content.includes(urlWithoutProtocol.toLowerCase())) {
return true;
}
}
// Check if URL appears in markdown image syntax ![alt](url)
const markdownImageRegex = /!\[.*?\]\((.*?)\)/gi;
let match;
@ -471,7 +471,7 @@ @@ -471,7 +471,7 @@
const markdownUrl = normalizeUrl(match[1]);
if (markdownUrl === normalized) {
return true;
}
}
}
// Check if URL appears in HTML img/video/audio tags
@ -480,7 +480,7 @@ @@ -480,7 +480,7 @@
const htmlUrl = normalizeUrl(match[2]);
if (htmlUrl === normalized) {
return true;
}
}
}
return false;
@ -493,14 +493,14 @@ @@ -493,14 +493,14 @@
} catch {
return url;
}
}
}
// Extract media URLs from event tags (image, imeta, file) - for feed view only
// Excludes URLs that are already in the content
function getMediaUrls(): string[] {
const urls: string[] = [];
const seen = new Set<string>();
// 1. Image tag (NIP-23)
const imageTag = post.tags.find((t) => t[0] === 'image');
if (imageTag && imageTag[1]) {
@ -509,7 +509,7 @@ @@ -509,7 +509,7 @@
urls.push(imageTag[1]);
seen.add(normalized);
}
}
}
// 2. imeta tags (NIP-92)
for (const tag of post.tags) {
@ -536,7 +536,7 @@ @@ -536,7 +536,7 @@
if (!seen.has(normalized) && !isUrlInContent(tag[1])) {
urls.push(tag[1]);
seen.add(normalized);
}
}
}
}
@ -590,11 +590,11 @@ @@ -590,11 +590,11 @@
<MetadataCard event={post} hideTitle={true} hideImageIfInMedia={true} />
{@const title = getTitle()}
{#if title && title !== 'Untitled'}
{@const title = getTitle()}
{#if title && title !== 'Untitled'}
<h2 class="post-title font-bold mb-4 text-fog-text dark:text-fog-dark-text" style="font-size: 1.5em;">
{title}
</h2>
{title}
</h2>
{/if}
<div class="post-header flex flex-col gap-2 mb-2">
@ -608,21 +608,21 @@ @@ -608,21 +608,21 @@
</div>
<div class="flex items-center gap-2 flex-nowrap">
<div class="flex-shrink-1 min-w-0">
<ProfileBadge pubkey={post.pubkey} />
<ProfileBadge pubkey={post.pubkey} />
</div>
<span class="text-fog-text-light dark:text-fog-dark-text-light flex-shrink-0 whitespace-nowrap" style="font-size: 0.75em;">{getRelativeTime()}</span>
{#if getClientName()}
{#if getClientName()}
<span class="text-fog-text-light dark:text-fog-dark-text-light flex-shrink-0" style="font-size: 0.75em;">via {getClientName()}</span>
{/if}
{#if post.kind === KIND.DISCUSSION_THREAD}
{/if}
{#if post.kind === KIND.DISCUSSION_THREAD}
{@const topics = getTopics()}
{#if topics.length === 0}
{#if topics.length === 0}
<a href="/topics/General" class="topic-badge px-2 py-0.5 rounded bg-fog-border dark:bg-fog-dark-border text-fog-text-light dark:text-fog-dark-text-light hover:underline" style="font-size: 0.75em;">General</a>
{:else}
{#each topics.slice(0, 3) as topic}
{:else}
{#each topics.slice(0, 3) as topic}
<a href="/topics/{topic}" class="topic-badge px-2 py-0.5 rounded bg-fog-border dark:bg-fog-dark-border text-fog-text-light dark:text-fog-dark-text-light hover:underline" style="font-size: 0.75em;">{topic}</a>
{/each}
{/if}
{/each}
{/if}
{/if}
</div>
<hr class="post-header-divider" />
@ -636,7 +636,7 @@ @@ -636,7 +636,7 @@
<div class="post-actions flex flex-wrap items-center gap-2 sm:gap-4">
<FeedReactionButtons event={post} preloadedReactions={preloadedReactions} />
</div>
{:else}
{:else}
<!-- Feed view: plaintext only, no profile pics, media as URLs -->
<div class="post-header flex flex-col gap-2 mb-2">
<div class="flex items-center gap-2 flex-nowrap">
@ -651,7 +651,7 @@ @@ -651,7 +651,7 @@
{@const topics = getTopics()}
{#if topics.length === 0}
<a href="/topics/General" class="topic-badge px-2 py-0.5 rounded bg-fog-border dark:bg-fog-dark-border text-fog-text-light dark:text-fog-dark-text-light hover:underline" style="font-size: 0.75em;">General</a>
{:else}
{:else}
{#each topics.slice(0, 3) as topic}
<a href="/topics/{topic}" class="topic-badge px-2 py-0.5 rounded bg-fog-border dark:bg-fog-dark-border text-fog-text-light dark:text-fog-dark-text-light hover:underline" style="font-size: 0.75em;">{topic}</a>
{/each}
@ -738,22 +738,22 @@ @@ -738,22 +738,22 @@
{url}
</button>
{/each}
</div>
</div>
{/if}
</div>
</div>
{#if !fullView && shouldCollapse}
<div class="show-more-container">
<button
onclick={toggleExpand}
<button
onclick={toggleExpand}
class="show-more-btn text-fog-accent dark:text-fog-dark-accent hover:underline"
style="font-size: 0.875em;"
>
>
{isExpanded ? 'Show Less' : 'Show More'}
</button>
</button>
</div>
{/if}
{/if}
{#if !fullView}
<div class="feed-card-footer flex items-center justify-between">
<div class="feed-card-actions flex items-center gap-2">
@ -771,10 +771,10 @@ @@ -771,10 +771,10 @@
{/if}
{#if fullView}
<div class="kind-badge">
<span class="kind-number">{getKindInfo(post.kind).number}</span>
<span class="kind-description">{getKindInfo(post.kind).description}</span>
</div>
<div class="kind-badge">
<span class="kind-number">{getKindInfo(post.kind).number}</span>
<span class="kind-description">{getKindInfo(post.kind).description}</span>
</div>
{/if}
</article>

12
src/lib/modules/feed/ThreadDrawer.svelte

@ -206,8 +206,8 @@ @@ -206,8 +206,8 @@
const eventId = opEvent.id;
// Create abort controller to track operation lifecycle
const abortController = new AbortController();
const abortController = new AbortController();
// Load hierarchy with abort signal
loadHierarchy(abortController.signal, eventId).catch((error) => {
// Handle any unhandled promise rejections
@ -255,7 +255,7 @@ @@ -255,7 +255,7 @@
// Load reactions for the single event when drawer opens
if (opEvent) {
batchLoadReactionsForEvents([opEvent.id]);
}
}
}
// Don't auto-load hierarchy - only load when "View thread" is clicked
@ -343,7 +343,7 @@ @@ -343,7 +343,7 @@
</button>
</div>
{:else if hierarchyChain.length > 0}
<!-- Display full event hierarchy (root to leaf) -->
<!-- Display full event hierarchy (root to leaf) -->
{#each hierarchyChain as parentEvent, index (parentEvent.id)}
<div class="hierarchy-post">
{#if index > 0}
@ -371,7 +371,7 @@ @@ -371,7 +371,7 @@
<!-- Display comments/replies only when full thread is loaded -->
{#if loadFullThread}
<div class="comments-section">
<div class="comments-section">
<CommentThread
threadId={opEvent.id}
event={opEvent}
@ -383,7 +383,7 @@ @@ -383,7 +383,7 @@
}
}}
/>
</div>
</div>
{/if}
</div>
{/if}

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

@ -63,7 +63,7 @@ @@ -63,7 +63,7 @@
currentLoadPubkey = null;
};
});
function openProfileEventsPanel() {
profileEventsPanelOpen = true;
}
@ -96,7 +96,7 @@ @@ -96,7 +96,7 @@
if (currentUserPubkey === pubkey) {
// Reload notifications for own profile
loadNotifications(pubkey);
} else {
} else {
// Reload interactions for other user's profile
loadInteractionsWithMe(pubkey, currentUserPubkey);
}
@ -535,7 +535,7 @@ @@ -535,7 +535,7 @@
activeTab = 'notifications';
} else if (pins.length > 0) {
activeTab = 'pins';
}
}
} else {
notifications = [];
// Load interactions if logged in and viewing another user's profile
@ -552,9 +552,9 @@ @@ -552,9 +552,9 @@
} catch (error) {
// Only update state if this load wasn't aborted
if (!abortController.signal.aborted && currentLoadPubkey === pubkey) {
console.error('Error loading profile:', error);
loading = false;
profile = null;
console.error('Error loading profile:', error);
loading = false;
profile = null;
}
} finally {
// Clear load tracking if this was the current load
@ -663,12 +663,12 @@ @@ -663,12 +663,12 @@
Pins ({pins.length})
</button>
{#if isOwnProfile}
<button
<button
onclick={() => activeTab = 'notifications'}
class="px-4 py-2 font-semibold {activeTab === 'notifications' ? 'border-b-2 border-fog-accent dark:border-fog-dark-accent' : ''}"
>
>
Notifications ({notifications.length})
</button>
</button>
{:else if currentUserPubkey && currentUserPubkey !== profilePubkey}
<button
onclick={() => activeTab = 'interactions'}
@ -687,8 +687,8 @@ @@ -687,8 +687,8 @@
{#each pins as pin (pin.id)}
<FeedPost post={pin} />
{/each}
</div>
{/if}
</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>
@ -707,8 +707,8 @@ @@ -707,8 +707,8 @@
{#each interactionsWithMe as interaction (interaction.id)}
<FeedPost post={interaction} />
{/each}
</div>
{/if}
</div>
{/if}
{/if}
</div>
{:else}

2
src/lib/modules/reactions/FeedReactionButtons.svelte

@ -176,7 +176,7 @@ @@ -176,7 +176,7 @@
// Cache of checked reaction IDs to avoid repeated deletion checks
const checkedReactionIds = new Set<string>();
async function filterDeletedReactions(reactions: NostrEvent[]): Promise<NostrEvent[]> {
if (reactions.length === 0) return reactions;

2
src/lib/services/nostr/event-hierarchy.ts

@ -46,7 +46,7 @@ export async function buildEventHierarchy(event: NostrEvent): Promise<EventHiera @@ -46,7 +46,7 @@ export async function buildEventHierarchy(event: NostrEvent): Promise<EventHiera
const parts = aTag[1].split(':');
// Validate a-tag format
if (parts.length === 3 && /^[a-f0-9]{64}$/i.test(parts[1])) {
return { type: 'a', value: aTag[1] };
return { type: 'a', value: aTag[1] };
}
}

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

@ -257,7 +257,7 @@ export async function resolveEmojiShortcode( @@ -257,7 +257,7 @@ export async function resolveEmojiShortcode(
if (searchBroadly) {
// Only check cache if already loaded - don't trigger background fetch
if (allEmojiPacksLoaded && shortcodeCache.has(cleanShortcode)) {
return shortcodeCache.get(cleanShortcode)!;
return shortcodeCache.get(cleanShortcode)!;
}
// If packs aren't loaded yet, return null (don't fetch in background)
return null;

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

@ -48,7 +48,7 @@ @@ -48,7 +48,7 @@
// Track the last naddr we loaded to prevent duplicate loads
let lastLoadedNaddr = $state<string | null>(null);
$effect(() => {
if (naddr && !loadingRepo && naddr !== lastLoadedNaddr) {
// Reset git repo state when naddr changes
@ -218,7 +218,7 @@ @@ -218,7 +218,7 @@
// Only update if it's actually different (prevents unnecessary re-renders)
if (!repoEvent || repoEvent.id !== newRepoEvent.id) {
repoEvent = newRepoEvent;
console.log('Repo event loaded:', repoEvent.id);
console.log('Repo event loaded:', repoEvent.id);
}
// Don't fetch git repo here - wait until user clicks on repository tab

14
src/routes/rss/+page.svelte

@ -131,12 +131,12 @@ @@ -131,12 +131,12 @@
if (feedsToFetch.length > 0) {
const fetchPromises = feedsToFetch.map(async (feedUrl) => {
try {
const items = await fetchRssFeed(feedUrl);
try {
const items = await fetchRssFeed(feedUrl);
// 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')) {
@ -163,11 +163,11 @@ @@ -163,11 +163,11 @@
for (const item of freshItems) {
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;
}
@ -182,7 +182,7 @@ @@ -182,7 +182,7 @@
async function fetchRssFeed(feedUrl: string): Promise<RSSItem[]> {
// Always use a CORS proxy to avoid CORS errors
// Direct fetch will fail for most RSS feeds due to CORS restrictions
const proxyUrl = `https://api.allorigins.win/raw?url=${encodeURIComponent(feedUrl)}`;
const proxyUrl = `https://api.allorigins.win/raw?url=${encodeURIComponent(feedUrl)}`;
const response = await fetch(proxyUrl);
if (!response.ok) {

4
src/routes/topics/[name]/+page.svelte

@ -45,8 +45,8 @@ @@ -45,8 +45,8 @@
onMount(async () => {
await nostrClient.initialize();
if (topicName) {
await loadCachedTopicEvents();
await loadTopicEvents();
await loadCachedTopicEvents();
await loadTopicEvents();
}
});

Loading…
Cancel
Save