diff --git a/package-lock.json b/package-lock.json index 64e0d0bd..623f1ef9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "jumble-imwald", - "version": "19.4.0", + "version": "20.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "jumble-imwald", - "version": "19.4.0", + "version": "20.0.0", "license": "MIT", "dependencies": { "@asciidoctor/core": "^3.0.4", @@ -4892,9 +4892,9 @@ "license": "MIT" }, "node_modules/@rollup/pluginutils/node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, "license": "MIT", "engines": { @@ -12576,9 +12576,9 @@ "license": "ISC" }, "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", "license": "MIT", "engines": { "node": ">=8.6" @@ -15008,9 +15008,9 @@ } }, "node_modules/tinyglobby/node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "license": "MIT", "engines": { "node": ">=12" @@ -15707,9 +15707,9 @@ } }, "node_modules/vite/node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, "license": "MIT", "engines": { @@ -15802,9 +15802,9 @@ } }, "node_modules/vitest/node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, "license": "MIT", "engines": { diff --git a/package.json b/package.json index 1869a25b..63efefd5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "jumble-imwald", - "version": "19.4.0", + "version": "20.0.0", "description": "A user-friendly Nostr client focused on relay feed browsing and relay discovery, forked from Jumble", "private": true, "type": "module", @@ -14,6 +14,7 @@ "homepage": "https://github.com/Silberengel/jumble", "scripts": { "dev": "vite --host", + "dev:refresh": "rm -rf node_modules/.vite && vite --host", "build": "tsc -b && vite build", "lint": "eslint .", "format": "prettier --write .", diff --git a/src/lib/relay-pulse-active-npubs-cache.ts b/src/lib/relay-pulse-active-npubs-cache.ts new file mode 100644 index 00000000..352489d3 --- /dev/null +++ b/src/lib/relay-pulse-active-npubs-cache.ts @@ -0,0 +1,38 @@ +import logger from '@/lib/logger' + +/** One row per browser; overwritten whenever a new active-npub list is fetched for the same relay + viewer scope. */ +export type RelayPulseActiveNpubsCacheRow = { + relayKey: string + viewerPubkey: string | null + orderedPubkeys: string[] + lastFetchedAtMs: number +} + +const STORAGE_KEY = 'jumble.relayPulse.activeNpubs.v1' + +export function readRelayPulseActiveNpubsCache( + relayKey: string, + viewerPubkey: string | null +): Pick | null { + try { + const raw = localStorage.getItem(STORAGE_KEY) + if (!raw) return null + const data = JSON.parse(raw) as unknown + if (!data || typeof data !== 'object') return null + const o = data as Record + if (o.relayKey !== relayKey || o.viewerPubkey !== viewerPubkey) return null + if (!Array.isArray(o.orderedPubkeys) || typeof o.lastFetchedAtMs !== 'number') return null + const orderedPubkeys = o.orderedPubkeys.filter((x): x is string => typeof x === 'string') + return { orderedPubkeys, lastFetchedAtMs: o.lastFetchedAtMs } + } catch { + return null + } +} + +export function writeRelayPulseActiveNpubsCache(row: RelayPulseActiveNpubsCacheRow): void { + try { + localStorage.setItem(STORAGE_KEY, JSON.stringify(row)) + } catch (e) { + logger.debug('[RelayPulseActiveNpubsCache] write failed', { error: e }) + } +} diff --git a/src/providers/FavoriteRelaysActivityProvider.tsx b/src/providers/FavoriteRelaysActivityProvider.tsx index f0df2a8c..38d99089 100644 --- a/src/providers/FavoriteRelaysActivityProvider.tsx +++ b/src/providers/FavoriteRelaysActivityProvider.tsx @@ -1,5 +1,9 @@ import logger from '@/lib/logger' import { getFavoritesFeedRelayUrls } from '@/lib/favorites-feed-relays' +import { + readRelayPulseActiveNpubsCache, + writeRelayPulseActiveNpubsCache +} from '@/lib/relay-pulse-active-npubs-cache' import { hexPubkeysEqual, normalizeHexPubkey, userIdToPubkey } from '@/lib/pubkey' import { getPubkeysFromPTags } from '@/lib/tag' import { useFavoriteRelays } from '@/providers/FavoriteRelaysProvider' @@ -92,8 +96,15 @@ export function FavoriteRelaysActivityProvider({ children }: { children: React.R setLoading(false) setRelayActivityReady(true) const now = Date.now() + setOrderedPubkeys([]) lastCompletedFetchAtRef.current = now setLastFetchedAtMs(now) + writeRelayPulseActiveNpubsCache({ + relayKey, + viewerPubkey: viewerPubkey ?? null, + orderedPubkeys: [], + lastFetchedAtMs: now + }) return } setLoading(true) @@ -109,9 +120,16 @@ export function FavoriteRelaysActivityProvider({ children }: { children: React.R } ) const now = Date.now() - setOrderedPubkeys(aggregatePubkeysByRecency(events)) + const nextPubkeys = aggregatePubkeysByRecency(events) + setOrderedPubkeys(nextPubkeys) lastCompletedFetchAtRef.current = now setLastFetchedAtMs(now) + writeRelayPulseActiveNpubsCache({ + relayKey, + viewerPubkey: viewerPubkey ?? null, + orderedPubkeys: nextPubkeys, + lastFetchedAtMs: now + }) } catch (error) { logger.debug('[FavoriteRelaysActivity] fetch failed', { error, useDefaultRelays }) if (!useDefaultRelays && favoriteRelays.length > 0) { @@ -122,7 +140,7 @@ export function FavoriteRelaysActivityProvider({ children }: { children: React.R setRelayActivityReady(true) } }, - [favoriteRelays, blockedRelays] + [favoriteRelays, blockedRelays, relayKey, viewerPubkey] ) const fetchRef = useRef(fetchActive) @@ -160,6 +178,16 @@ export function FavoriteRelaysActivityProvider({ children }: { children: React.R prevViewerRef.current = viewerPubkey ?? undefined }, [viewerPubkey, resetForRefetch]) + /** Restore last successful relay-pulse author list from localStorage (same relay set + viewer). */ + useEffect(() => { + const row = readRelayPulseActiveNpubsCache(relayKey, viewerPubkey ?? null) + if (!row) return + setOrderedPubkeys(row.orderedPubkeys) + setLastFetchedAtMs(row.lastFetchedAtMs) + setRelayActivityReady(true) + lastCompletedFetchAtRef.current = row.lastFetchedAtMs + }, [relayKey, viewerPubkey]) + /** When follow list from context is empty but we have a logged-in viewer, try IndexedDB cache. * Fixes race where pulse data arrives before NostrProvider has hydrated follow list from cache. */ useEffect(() => {