mirror of
https://github.com/hoornet/vega.git
synced 2026-05-07 20:59:12 -07:00
SQLite-backed caching for bookmarks and articles feed
Bookmarks load instantly from DB cache, then fetch missing notes from relays in background. Articles feed shows cached kind-30023 events immediately on the latest tab. Both persist to SQLite for instant load on next visit.
This commit is contained in:
@@ -1,8 +1,9 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { NDKEvent } from "@nostr-dev-kit/ndk";
|
||||
import { fetchArticleFeed } from "../../lib/nostr";
|
||||
import { fetchArticleFeed, getNDK } from "../../lib/nostr";
|
||||
import { useUserStore } from "../../stores/user";
|
||||
import { useUIStore } from "../../stores/ui";
|
||||
import { dbLoadArticles, dbSaveNotes } from "../../lib/db";
|
||||
import { ArticleCard } from "./ArticleCard";
|
||||
|
||||
type ArticleTab = "latest" | "following";
|
||||
@@ -21,11 +22,42 @@ export function ArticleFeed() {
|
||||
if (tab === "following" && follows.length === 0) return;
|
||||
let cancelled = false;
|
||||
setLoading(true);
|
||||
const authors = tab === "following" ? follows : undefined;
|
||||
fetchArticleFeed(40, authors)
|
||||
.then((result) => { if (!cancelled) setArticles(result); })
|
||||
.catch(() => { if (!cancelled) setArticles([]); })
|
||||
.finally(() => { if (!cancelled) setLoading(false); });
|
||||
|
||||
(async () => {
|
||||
// 1) Instant: load from SQLite cache (latest tab only — following is filtered)
|
||||
if (tab === "latest") {
|
||||
const cached = await dbLoadArticles(40);
|
||||
if (!cancelled && cached.length > 0) {
|
||||
const ndk = getNDK();
|
||||
const events = cached
|
||||
.map((raw) => { try { return new NDKEvent(ndk, JSON.parse(raw)); } catch { return null; } })
|
||||
.filter((e): e is NDKEvent => e !== null)
|
||||
.sort((a, b) => (b.created_at ?? 0) - (a.created_at ?? 0));
|
||||
if (events.length > 0) {
|
||||
setArticles(events);
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2) Background: fetch from relays and merge
|
||||
const authors = tab === "following" ? follows : undefined;
|
||||
try {
|
||||
const result = await fetchArticleFeed(40, authors);
|
||||
if (!cancelled) {
|
||||
setArticles(result);
|
||||
// Save to notes table for next time
|
||||
if (result.length > 0) {
|
||||
dbSaveNotes(result.map((e) => JSON.stringify(e.rawEvent())));
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
if (!cancelled && articles.length === 0) setArticles([]);
|
||||
} finally {
|
||||
if (!cancelled) setLoading(false);
|
||||
}
|
||||
})();
|
||||
|
||||
return () => { cancelled = true; };
|
||||
}, [followsKey]);
|
||||
|
||||
|
||||
@@ -2,7 +2,8 @@ import { useEffect, useState } from "react";
|
||||
import { NDKEvent } from "@nostr-dev-kit/ndk";
|
||||
import { useBookmarkStore } from "../../stores/bookmark";
|
||||
import { useUserStore } from "../../stores/user";
|
||||
import { fetchNoteById, fetchByAddr } from "../../lib/nostr";
|
||||
import { fetchNoteById, fetchByAddr, getNDK } from "../../lib/nostr";
|
||||
import { dbLoadBookmarkedNotes, dbSaveBookmarkedNotes } from "../../lib/db";
|
||||
import { NoteCard } from "../feed/NoteCard";
|
||||
import { ArticleCard } from "../article/ArticleCard";
|
||||
import { SkeletonNoteList } from "../shared/Skeleton";
|
||||
@@ -46,53 +47,84 @@ export function BookmarkView() {
|
||||
if (pubkey) fetchBookmarks(pubkey);
|
||||
}, [pubkey]);
|
||||
|
||||
// Load bookmarked notes: DB cache first (instant), then relay fetch for missing
|
||||
useEffect(() => {
|
||||
if (bookmarkedIds.length === 0) {
|
||||
setNotes([]);
|
||||
return;
|
||||
}
|
||||
loadNotes();
|
||||
let cancelled = false;
|
||||
setLoadingNotes(true);
|
||||
|
||||
(async () => {
|
||||
// 1) Instant: load from SQLite cache
|
||||
if (pubkey) {
|
||||
const cached = await dbLoadBookmarkedNotes(pubkey);
|
||||
if (!cancelled && cached.length > 0) {
|
||||
const ndk = getNDK();
|
||||
const events = cached
|
||||
.map((raw) => { try { return new NDKEvent(ndk, JSON.parse(raw)); } catch { return null; } })
|
||||
.filter((e): e is NDKEvent => e !== null)
|
||||
.filter((e) => bookmarkedIds.includes(e.id))
|
||||
.sort((a, b) => (b.created_at ?? 0) - (a.created_at ?? 0));
|
||||
if (events.length > 0) {
|
||||
setNotes(events);
|
||||
setLoadingNotes(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2) Background: fetch from relays and merge
|
||||
try {
|
||||
const results = await Promise.all(
|
||||
bookmarkedIds.map((id) => fetchNoteById(id))
|
||||
);
|
||||
if (!cancelled) {
|
||||
const fetched = results
|
||||
.filter((e): e is NDKEvent => e !== null)
|
||||
.sort((a, b) => (b.created_at ?? 0) - (a.created_at ?? 0));
|
||||
setNotes(fetched);
|
||||
// Save to DB for next time
|
||||
if (pubkey && fetched.length > 0) {
|
||||
dbSaveBookmarkedNotes(fetched.map((e) => JSON.stringify(e.rawEvent())), pubkey);
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
if (!cancelled) setLoadingNotes(false);
|
||||
}
|
||||
})();
|
||||
|
||||
return () => { cancelled = true; };
|
||||
}, [bookmarkedIds]);
|
||||
|
||||
// Load bookmarked articles (no DB cache yet — articles are fetched by addr)
|
||||
useEffect(() => {
|
||||
if (bookmarkedArticleAddrs.length === 0) {
|
||||
setArticles([]);
|
||||
return;
|
||||
}
|
||||
loadArticles();
|
||||
}, [bookmarkedArticleAddrs]);
|
||||
|
||||
const loadNotes = async () => {
|
||||
setLoadingNotes(true);
|
||||
try {
|
||||
const results = await Promise.all(
|
||||
bookmarkedIds.map((id) => fetchNoteById(id))
|
||||
);
|
||||
setNotes(
|
||||
results
|
||||
.filter((e): e is NDKEvent => e !== null)
|
||||
.sort((a, b) => (b.created_at ?? 0) - (a.created_at ?? 0))
|
||||
);
|
||||
} finally {
|
||||
setLoadingNotes(false);
|
||||
}
|
||||
};
|
||||
|
||||
const loadArticles = async () => {
|
||||
let cancelled = false;
|
||||
setLoadingArticles(true);
|
||||
try {
|
||||
const results = await Promise.all(
|
||||
bookmarkedArticleAddrs.map((addr) => fetchByAddr(addr))
|
||||
);
|
||||
setArticles(
|
||||
results
|
||||
.filter((e): e is NDKEvent => e !== null)
|
||||
.sort((a, b) => (b.created_at ?? 0) - (a.created_at ?? 0))
|
||||
);
|
||||
} finally {
|
||||
setLoadingArticles(false);
|
||||
}
|
||||
};
|
||||
|
||||
(async () => {
|
||||
try {
|
||||
const results = await Promise.all(
|
||||
bookmarkedArticleAddrs.map((addr) => fetchByAddr(addr))
|
||||
);
|
||||
if (!cancelled) {
|
||||
setArticles(
|
||||
results
|
||||
.filter((e): e is NDKEvent => e !== null)
|
||||
.sort((a, b) => (b.created_at ?? 0) - (a.created_at ?? 0))
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
if (!cancelled) setLoadingArticles(false);
|
||||
}
|
||||
})();
|
||||
|
||||
return () => { cancelled = true; };
|
||||
}, [bookmarkedArticleAddrs]);
|
||||
|
||||
const totalCount = bookmarkedIds.length + bookmarkedArticleAddrs.length;
|
||||
const loading = tab === "notes" ? loadingNotes : loadingArticles;
|
||||
|
||||
@@ -62,3 +62,23 @@ export function dbSaveFollowers(followers: string[], ownerPubkey: string): void
|
||||
export async function dbLoadFollowers(ownerPubkey: string): Promise<string[]> {
|
||||
return invoke<string[]>("db_load_followers", { ownerPubkey }).catch(() => []);
|
||||
}
|
||||
|
||||
// ── Bookmarks cache ────────────────────────────────────────────────────────
|
||||
|
||||
/** Save bookmarked note events to SQLite. Fire-and-forget. */
|
||||
export function dbSaveBookmarkedNotes(raws: string[], ownerPubkey: string): void {
|
||||
if (raws.length === 0) return;
|
||||
invoke("db_save_bookmarked_notes", { notes: raws, ownerPubkey }).catch(() => {});
|
||||
}
|
||||
|
||||
/** Load cached bookmarked note JSONs for owner. */
|
||||
export async function dbLoadBookmarkedNotes(ownerPubkey: string): Promise<string[]> {
|
||||
return invoke<string[]>("db_load_bookmarked_notes", { ownerPubkey }).catch(() => []);
|
||||
}
|
||||
|
||||
// ── Articles cache ─────────────────────────────────────────────────────────
|
||||
|
||||
/** Load cached articles (kind 30023) from the notes table. */
|
||||
export async function dbLoadArticles(limit = 100): Promise<string[]> {
|
||||
return invoke<string[]>("db_load_articles", { limit }).catch(() => []);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user