diff --git a/src/components/CountViewer.tsx b/src/components/CountViewer.tsx index 4f31041..42a028d 100644 --- a/src/components/CountViewer.tsx +++ b/src/components/CountViewer.tsx @@ -14,11 +14,8 @@ import { import { firstValueFrom, timeout, catchError, of } from "rxjs"; import { useGrimoire } from "@/core/state"; import { useNostrEvent } from "@/hooks/useNostrEvent"; -import { useOutboxRelays } from "@/hooks/useOutboxRelays"; import pool from "@/services/relay-pool"; -import { AGGREGATOR_RELAYS } from "@/services/loaders"; import { getRelayInfo } from "@/lib/nip11"; -import { normalizeRelayURL } from "@/lib/relay-url"; import { RelayLink } from "./nostr/RelayLink"; import { FilterSummaryBadges } from "./nostr/FilterSummaryBadges"; import { KindBadge } from "./KindBadge"; @@ -43,7 +40,7 @@ import type { Filter } from "nostr-tools"; interface CountViewerProps { filter: NostrFilter; - relays?: string[]; // Optional - uses outbox model if not specified + relays: string[]; // Required - at least one relay needsAccount?: boolean; } @@ -283,16 +280,12 @@ function SingleRelayResult({ result }: { result: RelayCountResult }) { export default function CountViewer({ filter: rawFilter, - relays: explicitRelays, + relays, needsAccount, }: CountViewerProps) { const { state } = useGrimoire(); const accountPubkey = state.activeAccount?.pubkey; const { copy: handleCopy, copied } = useCopy(); - const [nip45FilteredRelays, setNip45FilteredRelays] = useState([]); - const [nip45FilterPhase, setNip45FilterPhase] = useState< - "idle" | "filtering" | "ready" - >("idle"); // Create pointer for contact list (kind 3) if we need to resolve $contacts const contactPointer = useMemo( @@ -324,84 +317,7 @@ export default function CountViewer({ [needsAccount, rawFilter, accountPubkey, contacts], ); - // Fallback relays for outbox selection (user's read relays or aggregators) - const fallbackRelays = useMemo( - () => - state.activeAccount?.relays?.filter((r) => r.read).map((r) => r.url) || - AGGREGATOR_RELAYS, - [state.activeAccount?.relays], - ); - - // Outbox options for relay selection - const outboxOptions = useMemo( - () => ({ - fallbackRelays, - timeout: 1000, - maxRelays: 20, // Fewer relays for COUNT since it's one-shot - }), - [fallbackRelays], - ); - - // Use outbox model for relay selection when no explicit relays - const { relays: selectedRelays, phase: relaySelectionPhase } = - useOutboxRelays(explicitRelays ? {} : filter, outboxOptions); - - // Filter relays by NIP-45 support - useEffect(() => { - // Skip if explicit relays provided or selection not ready - if (explicitRelays) { - setNip45FilteredRelays(explicitRelays); - setNip45FilterPhase("ready"); - return; - } - - if (relaySelectionPhase !== "ready" || selectedRelays.length === 0) { - setNip45FilterPhase("idle"); - return; - } - - // Filter by NIP-45 support - setNip45FilterPhase("filtering"); - - const filterByNip45 = async () => { - const results = await Promise.all( - selectedRelays.map(async (url) => { - const supported = await checkNip45Support(url); - // Include if supported or unknown (will error at request time) - return { url, include: supported !== false }; - }), - ); - - const filtered = results - .filter((r) => r.include) - .map((r) => { - try { - return normalizeRelayURL(r.url); - } catch { - return r.url; - } - }); - - // Use fallback aggregators if no NIP-45 relays found - setNip45FilteredRelays( - filtered.length > 0 ? filtered : AGGREGATOR_RELAYS.slice(0, 3), - ); - setNip45FilterPhase("ready"); - }; - - filterByNip45(); - }, [explicitRelays, selectedRelays, relaySelectionPhase]); - - // Final relays to use - const relays = nip45FilteredRelays; - const isSelectingRelays = - !explicitRelays && - (relaySelectionPhase !== "ready" || nip45FilterPhase !== "ready"); - - const { results, loading, refresh } = useCount( - filter, - isSelectingRelays ? [] : relays, - ); + const { results, loading, refresh } = useCount(filter, relays); const isSingleRelay = relays.length === 1; const singleResult = isSingleRelay ? results.get(relays[0]) : null; @@ -659,22 +575,8 @@ export default function CountViewer({ )} - {/* Selecting Relays */} - {(!needsAccount || accountPubkey) && isSelectingRelays && ( -
-
- -

- {nip45FilterPhase === "filtering" - ? "Filtering relays by NIP-45 support..." - : "Selecting relays..."} -

-
-
- )} - {/* Results */} - {(!needsAccount || accountPubkey) && !isSelectingRelays && ( + {(!needsAccount || accountPubkey) && (
{isSingleRelay && singleResult ? ( diff --git a/src/lib/count-parser.ts b/src/lib/count-parser.ts index d4fe512..e8b5c50 100644 --- a/src/lib/count-parser.ts +++ b/src/lib/count-parser.ts @@ -44,7 +44,7 @@ function parseCommaSeparated( /** * Parse COUNT command arguments into a Nostr filter * Similar to REQ but: - * - Requires at least one relay (no automatic relay selection) + * - Requires at least one relay * - No --limit flag (COUNT returns total, not a subset) * - No --close-on-eose flag (COUNT is inherently one-shot) * - No --view flag (COUNT doesn't render events) @@ -335,7 +335,10 @@ export function parseCountCommand(args: string[]): ParsedCountCommand { } } - // Relays are optional - will use outbox model if not specified + // Validate that at least one relay is specified + if (relays.length === 0) { + throw new Error("At least one relay is required for COUNT"); + } // Convert accumulated sets to filter arrays if (kinds.size > 0) filter.kinds = Array.from(kinds); diff --git a/src/types/man.ts b/src/types/man.ts index 7515c32..c566eed 100644 --- a/src/types/man.ts +++ b/src/types/man.ts @@ -325,14 +325,14 @@ export const manPages: Record = { count: { name: "count", section: "1", - synopsis: "count [relay...] [options]", + synopsis: "count [options]", description: - "Count events on Nostr relays using the NIP-45 COUNT verb. Returns event counts matching specified filter criteria. If no relays specified, automatically selects relays using the outbox model (NIP-65) filtered by NIP-45 support. Checks NIP-11 relay info to detect NIP-45 support before querying. Can be saved as a spell for quick access.", + "Count events on Nostr relays using the NIP-45 COUNT verb. Returns event counts matching specified filter criteria. Requires at least one relay. Checks NIP-11 relay info to detect NIP-45 support before querying. Can be saved as a spell for quick access.", options: [ { - flag: "[relay...]", + flag: "", description: - "Relay URLs to query (optional). If omitted, uses outbox model with NIP-45 filtering. Can appear anywhere in the command. Supports wss://relay.com or shorthand: relay.com", + "Relay URLs to query (required). At least one relay must be specified. Can appear anywhere in the command. Supports wss://relay.com or shorthand: relay.com", }, { flag: "-k, --kind ", @@ -390,12 +390,11 @@ export const manPages: Record = { }, ], examples: [ - "count -k 1 -a fiatjaf.com Count posts (auto relay selection)", - "count -k 3 -p $me Count followers (auto relay selection)", - "count relay.damus.io -k 3 -p npub1... Count followers on specific relay", - "count nos.lol relay.damus.io -k 1 -a fiatjaf.com Compare counts across relays", - "count -k 9735 -p $me --since 30d Count zaps received in last month", - "count -t nostr,bitcoin Count events with hashtags", + "count relay.damus.io -k 1 -a fiatjaf.com Count posts from author", + "count nos.lol -k 3 -p npub1... Count followers on specific relay", + "count nos.lol relay.damus.io -k 1 -a npub1... Compare counts across relays", + "count relay.damus.io -k 9735 -p $me --since 30d Count zaps received in last month", + "count nos.lol -t nostr,bitcoin Count events with hashtags", ], seeAlso: ["req", "nip"], appId: "count",