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
This commit is contained in:
Jure
2026-04-01 16:01:38 +02:00
parent 355f412455
commit 6ae795e48d
3 changed files with 67 additions and 47 deletions

View File

@@ -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);

View File

@@ -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",
},
},
{

View File

@@ -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<typeof setTimeout> | 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<BookmarkState>((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<BookmarkState>((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) => {