From 812b719ea03be34f045a524956be173a23b0fd5b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20G=C3=B3mez?= Date: Thu, 18 Dec 2025 23:32:00 +0100 Subject: [PATCH] feat: debug command, simplify state --- src/components/DebugViewer.tsx | 39 ++++++++++------------- src/components/ReqViewer.tsx | 48 ++++++++++++++++------------ src/components/nostr/user-menu.tsx | 4 +-- src/core/logic.ts | 4 +-- src/core/state.ts | 3 +- src/hooks/useAccountSync.ts | 51 ++++++------------------------ src/lib/migrations.ts | 26 ++++++++++++++- src/types/app.ts | 8 +---- src/types/man.ts | 24 +++++++------- 9 files changed, 97 insertions(+), 110 deletions(-) diff --git a/src/components/DebugViewer.tsx b/src/components/DebugViewer.tsx index f93e6f8..2df35d4 100644 --- a/src/components/DebugViewer.tsx +++ b/src/components/DebugViewer.tsx @@ -1,6 +1,7 @@ import { useGrimoire } from "@/core/state"; -import { Copy, Check } from "lucide-react"; import { useCopy } from "@/hooks/useCopy"; +import { SyntaxHighlight } from "@/components/SyntaxHighlight"; +import { CodeCopyButton } from "@/components/CodeCopyButton"; export function DebugViewer() { const { state } = useGrimoire(); @@ -8,32 +9,26 @@ export function DebugViewer() { const stateJson = JSON.stringify(state, null, 2); + const handleCopy = () => { + copy(stateJson); + }; + return (

Application State

-
-
-
-          {stateJson}
-        
+
+ +
); diff --git a/src/components/ReqViewer.tsx b/src/components/ReqViewer.tsx index eea9b0e..1e7f99e 100644 --- a/src/components/ReqViewer.tsx +++ b/src/components/ReqViewer.tsx @@ -55,7 +55,7 @@ import { AccordionTrigger, } from "./ui/accordion"; import { RelayLink } from "./nostr/RelayLink"; -import type { NostrFilter, NostrEvent } from "@/types/nostr"; +import type { NostrFilter } from "@/types/nostr"; import { formatEventIds, formatDTags, @@ -678,8 +678,10 @@ export default function ReqViewer({ // Memoize fallbackRelays to prevent re-creation on every render const fallbackRelays = useMemo( () => - state.activeAccount?.relays?.inbox.map((r) => r.url) || AGGREGATOR_RELAYS, - [state.activeAccount?.relays?.inbox], + state.activeAccount?.relays + ?.filter((r) => r.read) + .map((r) => r.url) || AGGREGATOR_RELAYS, + [state.activeAccount?.relays], ); // Memoize outbox options to prevent object re-creation @@ -749,34 +751,38 @@ export default function ReqViewer({ // Virtuoso scroll position preservation for prepending events const STARTING_INDEX = 100000; const [firstItemIndex, setFirstItemIndex] = useState(STARTING_INDEX); - const prevEventsRef = useRef([]); + const seenEventIdsRef = useRef>(new Set()); // Adjust firstItemIndex when new events are prepended to preserve scroll position + // Uses Set-based tracking to handle rapid batches correctly useEffect(() => { - const prevEvents = prevEventsRef.current; - const currentEvents = events; - const prevLength = prevEvents.length; - const currentLength = currentEvents.length; - - // Reset on clear/query change (events array shrunk) - if (currentLength < prevLength) { + // Reset on query change (events cleared) + if (events.length === 0) { + seenEventIdsRef.current = new Set(); setFirstItemIndex(STARTING_INDEX); - prevEventsRef.current = currentEvents; return; } - // Detect new events prepended (only in streaming mode after EOSE) - const newEventsCount = currentLength - prevLength; - if (newEventsCount > 0 && prevLength > 0 && stream && eoseReceived) { - // Verify first event changed (events were prepended, not inserted in middle) - const firstIdChanged = currentEvents[0]?.id !== prevEvents[0]?.id; - if (firstIdChanged) { - // Decrement firstItemIndex to maintain scroll position - setFirstItemIndex((prev) => prev - newEventsCount); + // Find new events at the start of the array (prepended) + // This approach is immune to rapid updates because we track ALL seen IDs cumulatively + let prependCount = 0; + for (let i = 0; i < events.length; i++) { + const event = events[i]; + if (!seenEventIdsRef.current.has(event.id)) { + // New event found at position i + prependCount++; + seenEventIdsRef.current.add(event.id); + } else { + // Found first existing event, stop counting + // All events after this are old (already seen) + break; } } - prevEventsRef.current = currentEvents; + // Adjust index only in streaming mode after EOSE + if (prependCount > 0 && stream && eoseReceived) { + setFirstItemIndex((prev) => prev - prependCount); + } }, [events, stream, eoseReceived]); /** diff --git a/src/components/nostr/user-menu.tsx b/src/components/nostr/user-menu.tsx index 3fc7f56..7edff47 100644 --- a/src/components/nostr/user-menu.tsx +++ b/src/components/nostr/user-menu.tsx @@ -107,14 +107,14 @@ export default function UserMenu() { - {relays && relays.all.length > 0 && ( + {relays && relays.length > 0 && ( <> Relays - {relays.all.map((relay) => ( + {relays.map((relay) => ( { if (!state.activeAccount) { return state; diff --git a/src/core/state.ts b/src/core/state.ts index 4936a6d..96ba8c2 100644 --- a/src/core/state.ts +++ b/src/core/state.ts @@ -6,6 +6,7 @@ import { AppId, WindowInstance, LayoutConfig, + RelayInfo, } from "@/types/app"; import { useLocale } from "@/hooks/useLocale"; import * as Logic from "./logic"; @@ -255,7 +256,7 @@ export const useGrimoire = () => { ); const setActiveAccountRelays = useCallback( - (relays: any) => + (relays: RelayInfo[]) => setState((prev) => Logic.setActiveAccountRelays(prev, relays)), [setState], ); diff --git a/src/hooks/useAccountSync.ts b/src/hooks/useAccountSync.ts index c101252..84dd6bc 100644 --- a/src/hooks/useAccountSync.ts +++ b/src/hooks/useAccountSync.ts @@ -2,9 +2,8 @@ import { useEffect } from "react"; import { useEventStore, useObservableMemo } from "applesauce-react/hooks"; import accounts from "@/services/accounts"; import { useGrimoire } from "@/core/state"; -import { getInboxes, getOutboxes } from "applesauce-core/helpers"; import { addressLoader } from "@/services/loaders"; -import type { RelayInfo, UserRelays } from "@/types/app"; +import type { RelayInfo } from "@/types/app"; import { normalizeRelayURL } from "@/lib/relay-url"; /** @@ -48,12 +47,10 @@ export function useAccountSync() { if (relayListEvent.id === lastRelayEventId) return; lastRelayEventId = relayListEvent.id; - // Parse inbox and outbox relays - const inboxRelays = getInboxes(relayListEvent); - const outboxRelays = getOutboxes(relayListEvent); - - // Get all relays from tags - const allRelays: RelayInfo[] = []; + // Parse relays from tags (NIP-65 format) + // Tag format: ["r", "relay-url", "read|write"] + // If no marker, relay is used for both read and write + const relays: RelayInfo[] = []; const seenUrls = new Set(); for (const tag of relayListEvent.tags) { @@ -63,11 +60,11 @@ export function useAccountSync() { if (seenUrls.has(url)) continue; seenUrls.add(url); - const type = tag[2]; - allRelays.push({ + const marker = tag[2]; + relays.push({ url, - read: !type || type === "read", - write: !type || type === "write", + read: !marker || marker === "read", + write: !marker || marker === "write", }); } catch (error) { console.warn( @@ -78,36 +75,6 @@ export function useAccountSync() { } } - const relays: UserRelays = { - inbox: inboxRelays - .map((url) => { - try { - return { - url: normalizeRelayURL(url), - read: true, - write: false, - }; - } catch { - return null; - } - }) - .filter((r): r is RelayInfo => r !== null), - outbox: outboxRelays - .map((url) => { - try { - return { - url: normalizeRelayURL(url), - read: false, - write: true, - }; - } catch { - return null; - } - }) - .filter((r): r is RelayInfo => r !== null), - all: allRelays, - }; - setActiveAccountRelays(relays); }); diff --git a/src/lib/migrations.ts b/src/lib/migrations.ts index d75e931..9b9c37d 100644 --- a/src/lib/migrations.ts +++ b/src/lib/migrations.ts @@ -8,7 +8,7 @@ import { GrimoireState } from "@/types/app"; import { toast } from "sonner"; -export const CURRENT_VERSION = 8; +export const CURRENT_VERSION = 9; /** * Migration function type @@ -81,6 +81,30 @@ const migrations: Record = { }, }; }, + // Migration from v8 to v9 - simplifies relay structure + 8: (state: any) => { + // Simplify activeAccount.relays from {inbox, outbox, all} to just an array + // The 'all' array already has the correct read/write flags per relay + if (state.activeAccount?.relays) { + const oldRelays = state.activeAccount.relays; + // If it has the old structure (with inbox/outbox/all), migrate it + if (oldRelays.all && Array.isArray(oldRelays.all)) { + return { + ...state, + __version: 9, + activeAccount: { + ...state.activeAccount, + relays: oldRelays.all, + }, + }; + } + } + // No relays to migrate, just bump version + return { + ...state, + __version: 9, + }; + }, }; /** diff --git a/src/types/app.ts b/src/types/app.ts index 27c142e..8b38e89 100644 --- a/src/types/app.ts +++ b/src/types/app.ts @@ -72,12 +72,6 @@ export interface RelayInfo { write: boolean; } -export interface UserRelays { - inbox: RelayInfo[]; - outbox: RelayInfo[]; - all: RelayInfo[]; -} - export interface GrimoireState { __version: number; // Schema version for migrations windows: Record; @@ -86,7 +80,7 @@ export interface GrimoireState { layoutConfig: LayoutConfig; // Global configuration for window insertion behavior activeAccount?: { pubkey: string; - relays?: UserRelays; + relays?: RelayInfo[]; }; locale?: { locale: string; diff --git a/src/types/man.ts b/src/types/man.ts index b49fe45..36bea80 100644 --- a/src/types/man.ts +++ b/src/types/man.ts @@ -108,18 +108,18 @@ export const manPages: Record = { category: "Documentation", defaultProps: {}, }, - // debug: { - // name: "debug", - // section: "1", - // synopsis: "debug", - // description: - // "Display the current application state for debugging purposes. Shows windows, workspaces, active account, and other internal state in a formatted view.", - // examples: ["debug View current application state"], - // seeAlso: ["help"], - // appId: "debug", - // category: "System", - // defaultProps: {}, - // }, + debug: { + name: "debug", + section: "1", + synopsis: "debug", + description: + "Display the current application state for debugging purposes. Shows windows, workspaces, active account, and other internal state in a formatted view.", + examples: ["debug View current application state"], + seeAlso: ["help"], + appId: "debug", + category: "System", + defaultProps: {}, + }, man: { name: "man", section: "1",