mirror of
https://github.com/hoornet/vega.git
synced 2026-05-07 04:39:12 -07:00
Add relay status badge, toast notifications, per-tab feed timestamps, relay UX improvements
- Relay status badge in feed header: shows connected/total relay count with color coding (green >75%, yellow 25-75%, red <25%), hover tooltip with per-relay status - Toast notification system: transient messages for connection lost, reconnecting, relay reset, and back-online events - Per-tab "last updated" relative timestamp in feed header (global, following, trending tracked independently) - Consolidated all relay management into RelaysView (removed duplicate relay section from Settings); per-relay remove button on health cards - Show all supported NIP badges on relay cards (was filtering to 11 notable) - Tooltips on relay status dots explaining green/yellow/red/gray meaning - Fix relay removal with trailing-slash URL normalization - Fix stale health results lingering after relay removal - Add acknowledgements section to README
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import { create } from "zustand";
|
||||
import { NDKEvent, NDKFilter, NDKKind, NDKSubscription, NDKSubscriptionCacheUsage } from "@nostr-dev-kit/ndk";
|
||||
import { connectToRelays, ensureConnected, resetNDK, fetchGlobalFeed, fetchBatchEngagement, fetchTrendingCandidates, getNDK } from "../lib/nostr";
|
||||
import { useToastStore } from "./toast";
|
||||
import { dbLoadFeed, dbSaveNotes } from "../lib/db";
|
||||
import { diagWrapFetch, logDiag, startRelaySnapshots, getRelayStates } from "../lib/feedDiagnostics";
|
||||
|
||||
@@ -18,6 +19,7 @@ interface FeedState {
|
||||
connected: boolean;
|
||||
error: string | null;
|
||||
focusedNoteIndex: number;
|
||||
lastUpdated: Record<string, number>;
|
||||
trendingNotes: NDKEvent[];
|
||||
trendingLoading: boolean;
|
||||
connect: () => Promise<void>;
|
||||
@@ -34,6 +36,7 @@ export const useFeedStore = create<FeedState>((set, get) => ({
|
||||
connected: false,
|
||||
error: null,
|
||||
focusedNoteIndex: -1,
|
||||
lastUpdated: {},
|
||||
trendingNotes: [],
|
||||
trendingLoading: false,
|
||||
setFocusedNoteIndex: (n: number) => set({ focusedNoteIndex: n }),
|
||||
@@ -64,6 +67,9 @@ export const useFeedStore = create<FeedState>((set, get) => ({
|
||||
const hasConnected = relays.some((r) => r.connected);
|
||||
|
||||
if (hasConnected) {
|
||||
if (offlineStreak > 0) {
|
||||
useToastStore.getState().addToast("Back online", "success");
|
||||
}
|
||||
offlineStreak = 0;
|
||||
if (!get().connected) set({ connected: true });
|
||||
} else {
|
||||
@@ -72,14 +78,17 @@ export const useFeedStore = create<FeedState>((set, get) => ({
|
||||
if (offlineStreak >= 3 && get().connected) {
|
||||
set({ connected: false });
|
||||
logDiag({ ts: new Date().toISOString(), action: "connection_lost", details: `No relays connected after ${offlineStreak} checks` });
|
||||
useToastStore.getState().addToast("Connection lost \u2014 reconnecting\u2026", "warning");
|
||||
// Nuclear reset after 6 consecutive failures (30s)
|
||||
if (offlineStreak >= 6) {
|
||||
offlineStreak = 0;
|
||||
useToastStore.getState().addToast("Resetting relay connections\u2026", "info");
|
||||
resetNDK().then(() => {
|
||||
if (getNDK().pool?.relays) {
|
||||
const after = Array.from(getNDK().pool.relays.values());
|
||||
if (after.some((r) => r.connected)) {
|
||||
set({ connected: true });
|
||||
useToastStore.getState().addToast("Relays reconnected", "success");
|
||||
// Restart live sub after NDK reset
|
||||
get().startLiveFeed();
|
||||
}
|
||||
@@ -126,7 +135,7 @@ export const useFeedStore = create<FeedState>((set, get) => ({
|
||||
.sort((a, b) => (b.created_at ?? 0) - (a.created_at ?? 0))
|
||||
.slice(0, MAX_FEED_SIZE);
|
||||
|
||||
set({ notes: merged, loading: false, focusedNoteIndex: -1 });
|
||||
set({ notes: merged, loading: false, focusedNoteIndex: -1, lastUpdated: { ...get().lastUpdated, global: Date.now() } });
|
||||
|
||||
// Persist fresh notes to SQLite (fire-and-forget)
|
||||
dbSaveNotes(fresh.map((e) => JSON.stringify(e.rawEvent())));
|
||||
@@ -167,7 +176,7 @@ export const useFeedStore = create<FeedState>((set, get) => ({
|
||||
const updated = [event, ...current]
|
||||
.sort((a, b) => (b.created_at ?? 0) - (a.created_at ?? 0))
|
||||
.slice(0, MAX_FEED_SIZE);
|
||||
set({ notes: updated });
|
||||
set({ notes: updated, lastUpdated: { ...get().lastUpdated, global: Date.now() } });
|
||||
|
||||
// Debounced save to SQLite — batch saves every 5s
|
||||
if (!saveTimer) {
|
||||
@@ -233,7 +242,7 @@ export const useFeedStore = create<FeedState>((set, get) => ({
|
||||
.slice(0, 50)
|
||||
.map((s) => s.note);
|
||||
|
||||
set({ trendingNotes: scored, trendingLoading: false });
|
||||
set({ trendingNotes: scored, trendingLoading: false, lastUpdated: { ...get().lastUpdated, trending: Date.now() } });
|
||||
|
||||
// Cache note IDs + timestamp
|
||||
localStorage.setItem(TRENDING_CACHE_KEY, JSON.stringify({
|
||||
|
||||
@@ -16,9 +16,12 @@ export const useRelayHealthStore = create<RelayHealthState>((set, get) => ({
|
||||
|
||||
checkAll: async () => {
|
||||
if (get().checking) return;
|
||||
set({ checking: true });
|
||||
// Immediately prune stale results for relays no longer stored
|
||||
const currentUrls = new Set(getStoredRelayUrls());
|
||||
const pruned = get().results.filter((r) => currentUrls.has(r.url));
|
||||
set({ checking: true, results: pruned });
|
||||
try {
|
||||
const urls = getStoredRelayUrls();
|
||||
const urls = Array.from(currentUrls);
|
||||
const results = await checkAllRelays(urls);
|
||||
set({ results, lastChecked: Date.now(), checking: false });
|
||||
} catch {
|
||||
|
||||
41
src/stores/toast.ts
Normal file
41
src/stores/toast.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { create } from "zustand";
|
||||
|
||||
interface Toast {
|
||||
id: string;
|
||||
message: string;
|
||||
type: "info" | "warning" | "success";
|
||||
autoCloseMs: number;
|
||||
}
|
||||
|
||||
interface ToastState {
|
||||
toasts: Toast[];
|
||||
addToast: (message: string, type: Toast["type"], autoCloseMs?: number) => void;
|
||||
removeToast: (id: string) => void;
|
||||
}
|
||||
|
||||
const MAX_TOASTS = 5;
|
||||
|
||||
export const useToastStore = create<ToastState>((set, get) => ({
|
||||
toasts: [],
|
||||
|
||||
addToast: (message, type, autoCloseMs = 4000) => {
|
||||
// Deduplicate — don't add if same message already visible
|
||||
if (get().toasts.some((t) => t.message === message)) return;
|
||||
|
||||
const id = crypto.randomUUID();
|
||||
const toast: Toast = { id, message, type, autoCloseMs };
|
||||
|
||||
set((state) => {
|
||||
const toasts = [...state.toasts, toast];
|
||||
// Drop oldest if over limit
|
||||
if (toasts.length > MAX_TOASTS) toasts.shift();
|
||||
return { toasts };
|
||||
});
|
||||
|
||||
setTimeout(() => get().removeToast(id), autoCloseMs);
|
||||
},
|
||||
|
||||
removeToast: (id) => {
|
||||
set((state) => ({ toasts: state.toasts.filter((t) => t.id !== id) }));
|
||||
},
|
||||
}));
|
||||
Reference in New Issue
Block a user