From 6ae795e48dba64aeaf87fc1cc1f1024067735c88 Mon Sep 17 00:00:00 2001 From: Jure <44338+hoornet@users.noreply.github.com> Date: Wed, 1 Apr 2026 16:01:38 +0200 Subject: [PATCH] Replace themes (Sepia, Nord Frost), batch bookmark fetch, debounce bookmark publish - Replace Solarized (low contrast) with Sepia (warm coffee tones) - Replace Tokyo Night with Nord Frost (cool blue-grey, brighter) - Bookmark fetch uses single batch filter instead of one-by-one (much faster) - Debounce bookmark publish to prevent race conditions with replaceable events --- src/components/bookmark/BookmarkView.tsx | 26 +++++++--- src/lib/themes.ts | 64 ++++++++++++------------ src/stores/bookmark.ts | 24 ++++++--- 3 files changed, 67 insertions(+), 47 deletions(-) diff --git a/src/components/bookmark/BookmarkView.tsx b/src/components/bookmark/BookmarkView.tsx index 99c8770..ea391c3 100644 --- a/src/components/bookmark/BookmarkView.tsx +++ b/src/components/bookmark/BookmarkView.tsx @@ -1,8 +1,8 @@ import { useEffect, useState } from "react"; -import { NDKEvent } from "@nostr-dev-kit/ndk"; +import { NDKEvent, NDKFilter } from "@nostr-dev-kit/ndk"; import { useBookmarkStore } from "../../stores/bookmark"; import { useUserStore } from "../../stores/user"; -import { fetchNoteById, fetchByAddr, getNDK } from "../../lib/nostr"; +import { fetchNoteById, fetchByAddr, getNDK, fetchWithTimeout } from "../../lib/nostr"; import { dbLoadBookmarkedNotes, dbSaveBookmarkedNotes } from "../../lib/db"; import { NoteCard } from "../feed/NoteCard"; import { ArticleCard } from "../article/ArticleCard"; @@ -83,13 +83,25 @@ export function BookmarkView() { } } - // 2) Background: fetch from relays and merge + // 2) Background: batch fetch from relays (single filter is much faster than one-by-one) try { - const results = await Promise.all( - bookmarkedIds.map((id) => fetchNoteById(id)) - ); + const batchSize = 50; + const allFetched: NDKEvent[] = []; + for (let i = 0; i < bookmarkedIds.length; i += batchSize) { + const batch = bookmarkedIds.slice(i, i + batchSize); + const filter: NDKFilter = { ids: batch }; + const events = await fetchWithTimeout(getNDK(), filter, 10000); + allFetched.push(...Array.from(events)); + } + // Fallback: try individual fetch for any IDs not found in batch + const foundIds = new Set(allFetched.map((e) => e.id)); + const missing = bookmarkedIds.filter((id) => !foundIds.has(id)); + if (missing.length > 0 && missing.length <= 10) { + const fallback = await Promise.all(missing.map((id) => fetchNoteById(id))); + allFetched.push(...fallback.filter((e): e is NDKEvent => e !== null)); + } if (!cancelled) { - const fetched = results.filter((e): e is NDKEvent => e !== null); + const fetched = allFetched; // Separate articles (kind 30023) bookmarked via e-tag from notes const fetchedNotes = fetched.filter((e) => e.kind !== 30023); const articlesFromETag = fetched.filter((e) => e.kind === 30023); diff --git a/src/lib/themes.ts b/src/lib/themes.ts index 10707f2..f78bde4 100644 --- a/src/lib/themes.ts +++ b/src/lib/themes.ts @@ -83,23 +83,23 @@ export const themes: Theme[] = [ }, }, { - id: "tokyo-night", - name: "Tokyo Night", + id: "sepia", + name: "Sepia", colors: { - bg: "#1a1b26", - "bg-raised": "#24283b", - "bg-hover": "#292e42", - border: "#3b4261", - "border-subtle": "#292e42", - text: "#a9b1d6", - "text-muted": "#565f89", - "text-dim": "#3b4261", - accent: "#7aa2f7", - "accent-hover": "#89b4fa", - zap: "#e0af68", - danger: "#f7768e", - warning: "#e0af68", - success: "#9ece6a", + bg: "#2b2018", + "bg-raised": "#382a1f", + "bg-hover": "#453527", + border: "#5a4636", + "border-subtle": "#382a1f", + text: "#e8d5c4", + "text-muted": "#b89c84", + "text-dim": "#7a6452", + accent: "#e09850", + "accent-hover": "#c47f3a", + zap: "#f0c040", + danger: "#d45040", + warning: "#e0a040", + success: "#7ab860", }, }, { @@ -123,23 +123,23 @@ export const themes: Theme[] = [ }, }, { - id: "ethereal", - name: "Ethereal", + id: "nord", + name: "Nord Frost", colors: { - bg: "#1a1a2e", - "bg-raised": "#16213e", - "bg-hover": "#1f2f50", - border: "#2a3a5c", - "border-subtle": "#1f2f50", - text: "#dfe6e9", - "text-muted": "#a0aec0", - "text-dim": "#5a6a8a", - accent: "#a29bfe", - "accent-hover": "#6c5ce7", - zap: "#ffeaa7", - danger: "#ff7675", - warning: "#ffeaa7", - success: "#55efc4", + bg: "#2e3440", + "bg-raised": "#3b4252", + "bg-hover": "#434c5e", + border: "#4c566a", + "border-subtle": "#3b4252", + text: "#eceff4", + "text-muted": "#d8dee9", + "text-dim": "#7b88a1", + accent: "#88c0d0", + "accent-hover": "#81a1c1", + zap: "#ebcb8b", + danger: "#bf616a", + warning: "#ebcb8b", + success: "#a3be8c", }, }, { diff --git a/src/stores/bookmark.ts b/src/stores/bookmark.ts index b356b01..b178fbb 100644 --- a/src/stores/bookmark.ts +++ b/src/stores/bookmark.ts @@ -1,6 +1,16 @@ import { create } from "zustand"; import { fetchBookmarkList, fetchBookmarkListFull, publishBookmarkListFull } from "../lib/nostr"; +// Debounce bookmark publishing to avoid race conditions with replaceable events +let publishTimer: ReturnType | null = null; +function debouncedPublish(get: () => BookmarkState) { + if (publishTimer) clearTimeout(publishTimer); + publishTimer = setTimeout(() => { + const { bookmarkedIds, bookmarkedArticleAddrs } = get(); + publishBookmarkListFull(bookmarkedIds, bookmarkedArticleAddrs).catch(() => {}); + }, 1000); +} + const STORAGE_KEY = "wrystr_bookmarks"; const ARTICLE_STORAGE_KEY = "wrystr_bookmarks_articles"; const READ_STORAGE_KEY = "wrystr_articles_read"; @@ -87,20 +97,19 @@ export const useBookmarkStore = create((set, get) => ({ }, addBookmark: async (eventId: string) => { - const { bookmarkedIds, bookmarkedArticleAddrs } = get(); + const { bookmarkedIds } = get(); if (bookmarkedIds.includes(eventId)) return; const updated = [...bookmarkedIds, eventId]; set({ bookmarkedIds: updated }); saveLocal(updated); - publishBookmarkListFull(updated, bookmarkedArticleAddrs).catch(() => {}); + debouncedPublish(get); }, removeBookmark: async (eventId: string) => { - const { bookmarkedArticleAddrs } = get(); const updated = get().bookmarkedIds.filter((id) => id !== eventId); set({ bookmarkedIds: updated }); saveLocal(updated); - publishBookmarkListFull(updated, bookmarkedArticleAddrs).catch(() => {}); + debouncedPublish(get); }, isBookmarked: (eventId: string) => { @@ -108,20 +117,19 @@ export const useBookmarkStore = create((set, get) => ({ }, addArticleBookmark: async (addr: string) => { - const { bookmarkedIds, bookmarkedArticleAddrs } = get(); + const { bookmarkedArticleAddrs } = get(); if (bookmarkedArticleAddrs.includes(addr)) return; const updated = [...bookmarkedArticleAddrs, addr]; set({ bookmarkedArticleAddrs: updated }); saveArticleAddrs(updated); - publishBookmarkListFull(bookmarkedIds, updated).catch(() => {}); + debouncedPublish(get); }, removeArticleBookmark: async (addr: string) => { - const { bookmarkedIds } = get(); const updated = get().bookmarkedArticleAddrs.filter((a) => a !== addr); set({ bookmarkedArticleAddrs: updated }); saveArticleAddrs(updated); - publishBookmarkListFull(bookmarkedIds, updated).catch(() => {}); + debouncedPublish(get); }, isArticleBookmarked: (addr: string) => {