diff --git a/src/components/CountViewer.tsx b/src/components/CountViewer.tsx index 17e315b..cfda9c6 100644 --- a/src/components/CountViewer.tsx +++ b/src/components/CountViewer.tsx @@ -4,34 +4,16 @@ import { AlertCircle, CheckCircle2, RefreshCw, - Filter as FilterIcon, - Hash, User, - Clock, - Search, - FileText, - ChevronDown, - ChevronRight, } from "lucide-react"; import { useGrimoire } from "@/core/state"; import { useNostrEvent } from "@/hooks/useNostrEvent"; import { RelayLink } from "./nostr/RelayLink"; -import { UserName } from "./nostr/UserName"; -import { KindBadge } from "./KindBadge"; +import { FilterSummaryBadges } from "./nostr/FilterSummaryBadges"; import { Button } from "./ui/button"; -import { - Collapsible, - CollapsibleContent, - CollapsibleTrigger, -} from "./ui/collapsible"; import { Tooltip, TooltipContent, TooltipTrigger } from "./ui/tooltip"; import type { NostrFilter } from "@/types/nostr"; import { resolveFilterAliases, getTagValues } from "@/lib/nostr-utils"; -import { - formatTimeRange, - formatHashtags, - formatGenericTag, -} from "@/lib/filter-formatters"; interface CountViewerProps { filter: NostrFilter; @@ -83,11 +65,9 @@ async function sendCountRequest( }, 10000); try { - // Convert wss:// to ws:// if needed for WebSocket constructor ws = new WebSocket(relayUrl); ws.onopen = () => { - // Send COUNT request const countMsg = JSON.stringify(["COUNT", queryId, filter]); ws?.send(countMsg); }; @@ -113,14 +93,12 @@ async function sendCountRequest( resolved = true; clearTimeout(timeout); cleanup(); - // payload is the reason string for CLOSED resolve({ url: relayUrl, status: "error", error: payload || "Request closed by relay", }); } else if (type === "NOTICE") { - // Some relays send NOTICE for unsupported commands if ( payload?.toLowerCase().includes("count") || payload?.toLowerCase().includes("unknown") || @@ -191,14 +169,12 @@ function useCount(filter: NostrFilter, relays: string[]) { const executeCount = useCallback(async () => { setLoading(true); - // Initialize all relays as pending const initialResults = new Map(); for (const url of relays) { initialResults.set(url, { url, status: "loading" }); } setResults(initialResults); - // Send COUNT requests in parallel const promises = relays.map(async (url) => { const result = await sendCountRequest(url, filter); setResults((prev) => { @@ -213,7 +189,6 @@ function useCount(filter: NostrFilter, relays: string[]) { setLoading(false); }, [filter, relays]); - // Execute on mount useEffect(() => { executeCount(); }, [executeCount]); @@ -221,172 +196,6 @@ function useCount(filter: NostrFilter, relays: string[]) { return { results, loading, refresh: executeCount }; } -function FilterSummary({ filter }: { filter: NostrFilter }) { - const [isOpen, setIsOpen] = useState(true); - - const authorPubkeys = filter.authors || []; - const pTagPubkeys = filter["#p"] || []; - const tTags = filter["#t"]; - const dTags = filter["#d"]; - - // Find generic tags - const genericTags = Object.entries(filter) - .filter( - ([key]) => - key.startsWith("#") && - key.length === 2 && - !["#e", "#p", "#t", "#d", "#P"].includes(key), - ) - .map(([key, values]) => ({ letter: key[1], values: values as string[] })); - - const tagCount = - (filter["#e"]?.length || 0) + - (tTags?.length || 0) + - (dTags?.length || 0) + - genericTags.reduce((sum, tag) => sum + tag.values.length, 0); - - return ( - - - {isOpen ? ( - - ) : ( - - )} - - Filter - - {/* Summary badges */} -
- {filter.kinds && filter.kinds.length > 0 && ( - - - {filter.kinds.length} - - )} - {authorPubkeys.length > 0 && ( - - - {authorPubkeys.length} - - )} - {pTagPubkeys.length > 0 && ( - - @{pTagPubkeys.length} - - )} - {(filter.since || filter.until) && } - {filter.search && } - {tagCount > 0 && ( - - - {tagCount} - - )} -
-
- - -
- {/* Kinds */} - {filter.kinds && filter.kinds.length > 0 && ( -
- kinds: -
- {filter.kinds.map((kind) => ( - - ))} -
-
- )} - - {/* Authors */} - {authorPubkeys.length > 0 && ( -
- authors: -
- {authorPubkeys.slice(0, 5).map((pubkey) => ( - - - - ))} - {authorPubkeys.length > 5 && ( - - +{authorPubkeys.length - 5} more - - )} -
-
- )} - - {/* #p tags (mentions) */} - {pTagPubkeys.length > 0 && ( -
- #p: -
- {pTagPubkeys.slice(0, 5).map((pubkey) => ( - - - - ))} - {pTagPubkeys.length > 5 && ( - - +{pTagPubkeys.length - 5} more - - )} -
-
- )} - - {/* Time range */} - {(filter.since || filter.until) && ( -
- time: - - {formatTimeRange(filter.since, filter.until)} - -
- )} - - {/* Search */} - {filter.search && ( -
- search: - - {filter.search} - -
- )} - - {/* Hashtags */} - {tTags && tTags.length > 0 && ( -
- #t: - {formatHashtags(tTags)} -
- )} - - {/* Generic tags */} - {genericTags.map(({ letter, values }) => ( -
- #{letter}: - - {formatGenericTag(letter, values)} - -
- ))} -
-
-
- ); -} - function RelayResultRow({ result }: { result: RelayCountResult }) { const statusIcon = useMemo(() => { switch (result.status) { @@ -546,32 +355,30 @@ export default function CountViewer({ return (
- {/* Header */} -
-
-
- {isSingleRelay ? ( - - ) : ( - - {relays.length} relays - - )} -
+ {/* Compact Header */} +
+ {isSingleRelay ? ( + + ) : ( + + {relays.length} relays + + )} + ยท + +
-
{/* Account Required Message */} diff --git a/src/components/ReqViewer.tsx b/src/components/ReqViewer.tsx index 69e7d01..848962b 100644 --- a/src/components/ReqViewer.tsx +++ b/src/components/ReqViewer.tsx @@ -79,6 +79,7 @@ import { shouldAnimate, } from "@/lib/req-state-machine"; import { resolveFilterAliases, getTagValues } from "@/lib/nostr-utils"; +import { FilterSummaryBadges } from "./nostr/FilterSummaryBadges"; import { useNostrEvent } from "@/hooks/useNostrEvent"; import { MemoizedCompactEventRow } from "./nostr/CompactEventRow"; import type { ViewMode } from "@/lib/req-parser"; @@ -141,8 +142,6 @@ function QueryDropdown({ filter, nip05Authors }: QueryDropdownProps) { (dTags?.length || 0) + genericTags.reduce((sum, tag) => sum + tag.values.length, 0); - const mentionCount = pTagPubkeys.length; - // Determine if we should use accordion for complex queries const isComplexQuery = (filter.kinds?.length || 0) + @@ -154,45 +153,7 @@ function QueryDropdown({ filter, nip05Authors }: QueryDropdownProps) { return (
{/* Summary Header */} -
- {filter.kinds && filter.kinds.length > 0 && ( - - - {filter.kinds.length} kind{filter.kinds.length !== 1 ? "s" : ""} - - )} - {authorPubkeys.length > 0 && ( - - - {authorPubkeys.length} author - {authorPubkeys.length !== 1 ? "s" : ""} - - )} - {mentionCount > 0 && ( - - - {mentionCount} mention{mentionCount !== 1 ? "s" : ""} - - )} - {(filter.since || filter.until) && ( - - - time range - - )} - {filter.search && ( - - - search - - )} - {tagCount > 0 && ( - - - {tagCount} tag{tagCount !== 1 ? "s" : ""} - - )} -
+ {isComplexQuery ? ( /* Accordion for complex queries */ diff --git a/src/components/nostr/FilterSummaryBadges.tsx b/src/components/nostr/FilterSummaryBadges.tsx new file mode 100644 index 0000000..6c3e066 --- /dev/null +++ b/src/components/nostr/FilterSummaryBadges.tsx @@ -0,0 +1,77 @@ +import { FileText, User, Clock, Search, Hash } from "lucide-react"; +import type { NostrFilter } from "@/types/nostr"; + +interface FilterSummaryBadgesProps { + filter: NostrFilter; + className?: string; +} + +/** + * Compact filter summary badges showing icons and counts + * Used by ReqViewer and CountViewer headers + */ +export function FilterSummaryBadges({ + filter, + className = "", +}: FilterSummaryBadgesProps) { + const authorPubkeys = filter.authors || []; + const pTagPubkeys = filter["#p"] || []; + + // Calculate tag count (excluding #p which is shown separately) + const tagCount = + (filter["#e"]?.length || 0) + + (filter["#t"]?.length || 0) + + (filter["#d"]?.length || 0) + + Object.entries(filter) + .filter( + ([key]) => + key.startsWith("#") && + key.length === 2 && + !["#e", "#p", "#t", "#d", "#P"].includes(key), + ) + .reduce((sum, [, values]) => sum + (values as string[]).length, 0); + + return ( +
+ {filter.kinds && filter.kinds.length > 0 && ( + + + {filter.kinds.length} kind{filter.kinds.length !== 1 ? "s" : ""} + + )} + {authorPubkeys.length > 0 && ( + + + {authorPubkeys.length} author + {authorPubkeys.length !== 1 ? "s" : ""} + + )} + {pTagPubkeys.length > 0 && ( + + + {pTagPubkeys.length} mention{pTagPubkeys.length !== 1 ? "s" : ""} + + )} + {(filter.since || filter.until) && ( + + + time range + + )} + {filter.search && ( + + + search + + )} + {tagCount > 0 && ( + + + {tagCount} tag{tagCount !== 1 ? "s" : ""} + + )} +
+ ); +}