mirror of
https://github.com/purrgrammer/grimoire.git
synced 2026-04-13 00:46:54 +02:00
Add automatic relay selection with NIP-45 filtering for COUNT
- Make relays optional in count-parser (no longer throws if none specified) - Add useOutboxRelays for automatic relay selection based on filter criteria - Filter selected relays by NIP-45 support via NIP-11 before querying - Show "Selecting relays..." and "Filtering by NIP-45..." loading states - Fall back to aggregator relays if no NIP-45 relays found - Update man page: relays now optional, new examples showing auto-selection
This commit is contained in:
@@ -14,8 +14,11 @@ 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";
|
||||
@@ -40,7 +43,7 @@ import type { Filter } from "nostr-tools";
|
||||
|
||||
interface CountViewerProps {
|
||||
filter: NostrFilter;
|
||||
relays: string[];
|
||||
relays?: string[]; // Optional - uses outbox model if not specified
|
||||
needsAccount?: boolean;
|
||||
}
|
||||
|
||||
@@ -280,12 +283,16 @@ function SingleRelayResult({ result }: { result: RelayCountResult }) {
|
||||
|
||||
export default function CountViewer({
|
||||
filter: rawFilter,
|
||||
relays,
|
||||
relays: explicitRelays,
|
||||
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(
|
||||
@@ -317,7 +324,84 @@ export default function CountViewer({
|
||||
[needsAccount, rawFilter, accountPubkey, contacts],
|
||||
);
|
||||
|
||||
const { results, loading, refresh } = useCount(filter, relays);
|
||||
// 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 isSingleRelay = relays.length === 1;
|
||||
const singleResult = isSingleRelay ? results.get(relays[0]) : null;
|
||||
@@ -575,8 +659,22 @@ 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) && (
|
||||
{(!needsAccount || accountPubkey) && !isSelectingRelays && (
|
||||
<div className="flex-1 overflow-auto">
|
||||
{isSingleRelay && singleResult ? (
|
||||
<SingleRelayResult result={singleResult} />
|
||||
|
||||
@@ -335,10 +335,7 @@ export function parseCountCommand(args: string[]): ParsedCountCommand {
|
||||
}
|
||||
}
|
||||
|
||||
// Validate: at least one relay is required
|
||||
if (relays.length === 0) {
|
||||
throw new Error("At least one relay is required for COUNT");
|
||||
}
|
||||
// Relays are optional - will use outbox model if not specified
|
||||
|
||||
// Convert accumulated sets to filter arrays
|
||||
if (kinds.size > 0) filter.kinds = Array.from(kinds);
|
||||
|
||||
@@ -325,14 +325,14 @@ export const manPages: Record<string, ManPageEntry> = {
|
||||
count: {
|
||||
name: "count",
|
||||
section: "1",
|
||||
synopsis: "count [options] <relay...>",
|
||||
synopsis: "count [relay...] [options]",
|
||||
description:
|
||||
"Count events on Nostr relays using the NIP-45 COUNT verb. Returns event counts matching specified filter criteria. At least one relay is required. If querying multiple relays, shows per-relay breakdown. Automatically 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. 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.",
|
||||
options: [
|
||||
{
|
||||
flag: "<relay...>",
|
||||
flag: "[relay...]",
|
||||
description:
|
||||
"Relay URLs to query (required, at least one). Can appear anywhere in the command. Supports wss://relay.com or shorthand: relay.com",
|
||||
"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",
|
||||
},
|
||||
{
|
||||
flag: "-k, --kind <number>",
|
||||
@@ -390,11 +390,12 @@ export const manPages: Record<string, ManPageEntry> = {
|
||||
},
|
||||
],
|
||||
examples: [
|
||||
"count relay.damus.io -k 3 -p npub1... Count followers on one relay",
|
||||
"count nos.lol relay.damus.io -k 1 -a fiatjaf.com Compare post counts across relays",
|
||||
"count relay.nostr.band -k 1 --search bitcoin Count search results",
|
||||
"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",
|
||||
"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",
|
||||
],
|
||||
seeAlso: ["req", "nip"],
|
||||
appId: "count",
|
||||
|
||||
Reference in New Issue
Block a user