Revert automatic relay selection for COUNT command

Simplify COUNT by requiring explicit relay specification:
- Restore relay requirement validation in count-parser.ts
- Remove useOutboxRelays and NIP-45 auto-filtering from CountViewer
- Update man page documentation to reflect required relays
- Keep NIP-45 support detection for better error messages

This keeps the feature simpler for now; automatic relay selection
can be added later when the UX is better understood.
This commit is contained in:
Claude
2026-01-15 14:48:16 +00:00
parent 1b40b835ea
commit 391a35193d
3 changed files with 18 additions and 114 deletions

View File

@@ -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<string[]>([]);
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({
</div>
)}
{/* Selecting Relays */}
{(!needsAccount || accountPubkey) && isSelectingRelays && (
<div className="flex-1 flex items-center justify-center">
<div className="text-muted-foreground text-center">
<Loader2 className="size-8 mx-auto mb-3 animate-spin" />
<p className="text-sm">
{nip45FilterPhase === "filtering"
? "Filtering relays by NIP-45 support..."
: "Selecting relays..."}
</p>
</div>
</div>
)}
{/* Results */}
{(!needsAccount || accountPubkey) && !isSelectingRelays && (
{(!needsAccount || accountPubkey) && (
<div className="flex-1 overflow-auto">
{isSingleRelay && singleResult ? (
<SingleRelayResult result={singleResult} />

View File

@@ -44,7 +44,7 @@ function parseCommaSeparated<T>(
/**
* 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);

View File

@@ -325,14 +325,14 @@ export const manPages: Record<string, ManPageEntry> = {
count: {
name: "count",
section: "1",
synopsis: "count [relay...] [options]",
synopsis: "count <relay...> [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: "<relay...>",
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 <number>",
@@ -390,12 +390,11 @@ export const manPages: Record<string, ManPageEntry> = {
},
],
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",