mirror of
https://github.com/purrgrammer/grimoire.git
synced 2026-04-10 15:36:53 +02:00
* feat(settings): add relay lists management section - Fetch additional relay list kinds (10006, 10007, 10050) on login - Add "Relays" tab to Settings with accordion UI for each relay list kind - Support NIP-65 relay list (kind 10002) with read/write markers - Support blocked relays (10006), search relays (10007), DM relays (10050) - Add/remove relays with URL sanitization and normalization - Explicit save button publishes only modified lists as replaceable events https://claude.ai/code/session_01JHirYU56sKDKYhRx6aCQ54 * docs: add plan for honoring blocked & search relay lists Detailed implementation plan for: - Kind 10006: filter blocked relays from all connection paths - Kind 10007: use search relays for NIP-50 queries https://claude.ai/code/session_01JHirYU56sKDKYhRx6aCQ54 * refactor(settings): extract relay list logic into tested lib, fix UX issues Extract parseRelayEntries, buildRelayListTags, sanitizeRelayInput, and comparison/mode helpers into src/lib/relay-list-utils.ts with 52 tests covering roundtrips, normalization, edge cases, and mode conversions. UX fixes: - Replace RelayLink (navigates away on click) with static RelaySettingsRow - Remove redundant inbox/outbox icons (mode dropdown is sufficient) - Always-visible delete button instead of hover-only opacity - Per-accordion dirty indicator (CircleDot icon) for modified lists - Discard button to reset all changes - Read-only account explanation text - Human-friendly descriptions (no NIP references or kind numbers) - Separator between relay list and add input - Larger relay icons and text for readability https://claude.ai/code/session_01JHirYU56sKDKYhRx6aCQ54 * feat(settings): use KindBadge and NIPBadge in relay list accordions Replace plain text kind names with KindBadge (full variant showing icon, name, and kind number) and add NIPBadge next to each list description. This gives power users the protocol context they expect. Also document KindBadge and NIPBadge as shared components in CLAUDE.md. https://claude.ai/code/session_01JHirYU56sKDKYhRx6aCQ54 * feat(settings): add favorite relays list (kind 10012) to relay settings Add kind 10012 (Favorite Relays / Relay Feeds) to the settings UI and account sync fetching. Uses "relay" tags like other NIP-51 lists. https://claude.ai/code/session_01JHirYU56sKDKYhRx6aCQ54 * fix(kinds): use semantic icons for blocked and search relay lists - Kind 10006 (Blocked Relays): Radio → ShieldBan - Kind 10007 (Search Relays): Radio → Search These icons propagate to KindBadge, settings accordions, and event renderers via getKindInfo(). Generic relay kinds (10002, 30002, etc.) keep the Radio icon. https://claude.ai/code/session_01JHirYU56sKDKYhRx6aCQ54 --------- Co-authored-by: Claude <noreply@anthropic.com>
148 lines
4.4 KiB
TypeScript
148 lines
4.4 KiB
TypeScript
import { useEffect } from "react";
|
|
import { useEventStore, use$ } from "applesauce-react/hooks";
|
|
import accounts from "@/services/accounts";
|
|
import { useGrimoire } from "@/core/state";
|
|
import { addressLoader } from "@/services/loaders";
|
|
import type { RelayInfo } from "@/types/app";
|
|
import { normalizeRelayURL } from "@/lib/relay-url";
|
|
import { getServersFromEvent } from "@/services/blossom";
|
|
|
|
/**
|
|
* Hook that syncs active account with Grimoire state and fetches relay lists and blossom servers
|
|
*/
|
|
export function useAccountSync() {
|
|
const {
|
|
setActiveAccount,
|
|
setActiveAccountRelays,
|
|
setActiveAccountBlossomServers,
|
|
} = useGrimoire();
|
|
const eventStore = useEventStore();
|
|
|
|
// Watch active account from accounts service
|
|
const activeAccount = use$(accounts.active$);
|
|
|
|
// Sync active account pubkey to state
|
|
useEffect(() => {
|
|
setActiveAccount(activeAccount?.pubkey);
|
|
}, [activeAccount?.pubkey, setActiveAccount]);
|
|
|
|
// Fetch and watch relay list (kind 10002) when account changes
|
|
useEffect(() => {
|
|
if (!activeAccount?.pubkey) {
|
|
return;
|
|
}
|
|
|
|
const pubkey = activeAccount.pubkey;
|
|
let lastRelayEventId: string | undefined;
|
|
|
|
// Subscribe to kind 10002 (relay list)
|
|
const subscription = addressLoader({
|
|
kind: 10002,
|
|
pubkey,
|
|
identifier: "",
|
|
}).subscribe();
|
|
|
|
// Watch for relay list event in store
|
|
const storeSubscription = eventStore
|
|
.replaceable(10002, pubkey, "")
|
|
.subscribe((relayListEvent) => {
|
|
if (!relayListEvent) return;
|
|
|
|
// Only process if this is a new event
|
|
if (relayListEvent.id === lastRelayEventId) return;
|
|
lastRelayEventId = relayListEvent.id;
|
|
|
|
// 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<string>();
|
|
|
|
for (const tag of relayListEvent.tags) {
|
|
if (tag[0] === "r" && tag[1]) {
|
|
try {
|
|
const url = normalizeRelayURL(tag[1]);
|
|
if (seenUrls.has(url)) continue;
|
|
seenUrls.add(url);
|
|
|
|
const marker = tag[2];
|
|
relays.push({
|
|
url,
|
|
read: !marker || marker === "read",
|
|
write: !marker || marker === "write",
|
|
});
|
|
} catch (error) {
|
|
console.warn(
|
|
`Skipping invalid relay URL in Kind 10002 event: ${tag[1]}`,
|
|
error,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
setActiveAccountRelays(relays);
|
|
});
|
|
|
|
return () => {
|
|
subscription.unsubscribe();
|
|
storeSubscription.unsubscribe();
|
|
};
|
|
}, [activeAccount?.pubkey, eventStore, setActiveAccountRelays]);
|
|
|
|
// Fetch other replaceable relay lists when account changes
|
|
// These are read directly from EventStore in the settings UI, we just need to trigger fetching
|
|
useEffect(() => {
|
|
if (!activeAccount?.pubkey) {
|
|
return;
|
|
}
|
|
|
|
const pubkey = activeAccount.pubkey;
|
|
const relayListKinds = [10006, 10007, 10012, 10050];
|
|
|
|
const subscriptions = relayListKinds.map((kind) =>
|
|
addressLoader({ kind, pubkey, identifier: "" }).subscribe(),
|
|
);
|
|
|
|
return () => {
|
|
subscriptions.forEach((s) => s.unsubscribe());
|
|
};
|
|
}, [activeAccount?.pubkey]);
|
|
|
|
// Fetch and watch blossom server list (kind 10063) when account changes
|
|
useEffect(() => {
|
|
if (!activeAccount?.pubkey) {
|
|
return;
|
|
}
|
|
|
|
const pubkey = activeAccount.pubkey;
|
|
let lastBlossomEventId: string | undefined;
|
|
|
|
// Subscribe to kind 10063 (blossom server list)
|
|
const subscription = addressLoader({
|
|
kind: 10063,
|
|
pubkey,
|
|
identifier: "",
|
|
}).subscribe();
|
|
|
|
// Watch for blossom server list event in store
|
|
const storeSubscription = eventStore
|
|
.replaceable(10063, pubkey, "")
|
|
.subscribe((blossomListEvent) => {
|
|
if (!blossomListEvent) return;
|
|
|
|
// Only process if this is a new event
|
|
if (blossomListEvent.id === lastBlossomEventId) return;
|
|
lastBlossomEventId = blossomListEvent.id;
|
|
|
|
// Parse servers from event
|
|
const servers = getServersFromEvent(blossomListEvent);
|
|
setActiveAccountBlossomServers(servers);
|
|
});
|
|
|
|
return () => {
|
|
subscription.unsubscribe();
|
|
storeSubscription.unsubscribe();
|
|
};
|
|
}, [activeAccount?.pubkey, eventStore, setActiveAccountBlossomServers]);
|
|
}
|