mirror of
https://github.com/purrgrammer/grimoire.git
synced 2026-04-17 19:07:06 +02:00
ui: friendlier req viewer
This commit is contained in:
@@ -123,14 +123,6 @@ export default function CommandLauncher({
|
||||
className="command-input"
|
||||
/>
|
||||
|
||||
{recognizedCommand && parsed.args.length > 0 && (
|
||||
<div className="command-hint">
|
||||
<span className="command-hint-label">Parsed:</span>
|
||||
<span className="command-hint-command">{commandName}</span>
|
||||
<span className="command-hint-args">{parsed.args.join(" ")}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Command.List className="command-list">
|
||||
<Command.Empty className="command-empty">
|
||||
{commandName
|
||||
|
||||
@@ -13,6 +13,12 @@ import type { EventPointer, AddressPointer } from "nostr-tools/nip19";
|
||||
import type { LucideIcon } from "lucide-react";
|
||||
import { kinds, nip19 } from "nostr-tools";
|
||||
import { ProfileContent } from "applesauce-core/helpers";
|
||||
import {
|
||||
formatEventIds,
|
||||
formatDTags,
|
||||
formatTimeRangeCompact,
|
||||
formatGenericTag,
|
||||
} from "@/lib/filter-formatters";
|
||||
|
||||
export interface WindowTitleData {
|
||||
title: string;
|
||||
@@ -273,8 +279,8 @@ function useDynamicTitle(window: WindowInstance): WindowTitleData {
|
||||
// Generate a descriptive title from the filter
|
||||
const parts: string[] = [];
|
||||
|
||||
// 1. Kinds
|
||||
if (filter.kinds && filter.kinds.length > 0) {
|
||||
// Show actual kind names
|
||||
const kindNames = filter.kinds.map((k: number) => getKindName(k));
|
||||
if (kindNames.length <= 3) {
|
||||
parts.push(kindNames.join(", "));
|
||||
@@ -285,13 +291,13 @@ function useDynamicTitle(window: WindowInstance): WindowTitleData {
|
||||
}
|
||||
}
|
||||
|
||||
// Format hashtags with # prefix
|
||||
// 2. Hashtags (#t)
|
||||
if (filter["#t"] && filter["#t"].length > 0) {
|
||||
const hashtagText = formatHashtags("#", reqHashtags);
|
||||
if (hashtagText) parts.push(hashtagText);
|
||||
}
|
||||
|
||||
// Format tagged users with @ prefix
|
||||
// 3. Mentions (#p)
|
||||
if (filter["#p"] && filter["#p"].length > 0) {
|
||||
const taggedText = formatProfileNames("@", reqTagged, [
|
||||
tagged1Profile,
|
||||
@@ -300,7 +306,19 @@ function useDynamicTitle(window: WindowInstance): WindowTitleData {
|
||||
if (taggedText) parts.push(taggedText);
|
||||
}
|
||||
|
||||
// Format authors with "by " prefix
|
||||
// 4. Event References (#e) - NEW
|
||||
if (filter["#e"] && filter["#e"].length > 0) {
|
||||
const eventIdsText = formatEventIds(filter["#e"], 2);
|
||||
if (eventIdsText) parts.push(`→ ${eventIdsText}`);
|
||||
}
|
||||
|
||||
// 5. D-Tags (#d) - NEW
|
||||
if (filter["#d"] && filter["#d"].length > 0) {
|
||||
const dTagsText = formatDTags(filter["#d"], 2);
|
||||
if (dTagsText) parts.push(`📝 ${dTagsText}`);
|
||||
}
|
||||
|
||||
// 6. Authors
|
||||
if (filter.authors && filter.authors.length > 0) {
|
||||
const authorsText = formatProfileNames("by ", reqAuthors, [
|
||||
author1Profile,
|
||||
@@ -309,6 +327,27 @@ function useDynamicTitle(window: WindowInstance): WindowTitleData {
|
||||
if (authorsText) parts.push(authorsText);
|
||||
}
|
||||
|
||||
// 7. Time Range - NEW
|
||||
if (filter.since || filter.until) {
|
||||
const timeRangeText = formatTimeRangeCompact(filter.since, filter.until);
|
||||
if (timeRangeText) parts.push(`📅 ${timeRangeText}`);
|
||||
}
|
||||
|
||||
// 8. Generic Tags - NEW (a-z, A-Z filters excluding e, p, t, d)
|
||||
const genericTags = Object.entries(filter)
|
||||
.filter(([key]) => key.startsWith("#") && key.length === 2 && !["#e", "#p", "#t", "#d"].includes(key))
|
||||
.map(([key, values]) => ({ letter: key[1], values: values as string[] }));
|
||||
|
||||
if (genericTags.length > 0) {
|
||||
genericTags.slice(0, 2).forEach((tag) => {
|
||||
const tagText = formatGenericTag(tag.letter, tag.values, 1);
|
||||
if (tagText) parts.push(tagText);
|
||||
});
|
||||
if (genericTags.length > 2) {
|
||||
parts.push(`+${genericTags.length - 2} more tags`);
|
||||
}
|
||||
}
|
||||
|
||||
return parts.length > 0 ? parts.join(" • ") : "REQ";
|
||||
}, [
|
||||
appId,
|
||||
|
||||
@@ -11,9 +11,18 @@ import {
|
||||
import { Virtuoso } from "react-virtuoso";
|
||||
import { useReqTimeline } from "@/hooks/useReqTimeline";
|
||||
import { useGrimoire } from "@/core/state";
|
||||
import { useProfile } from "@/hooks/useProfile";
|
||||
import { FeedEvent } from "./nostr/Feed";
|
||||
import { KindBadge } from "./KindBadge";
|
||||
import type { NostrFilter } from "@/types/nostr";
|
||||
import {
|
||||
formatEventIds,
|
||||
formatDTags,
|
||||
formatTimeRange,
|
||||
formatGenericTag,
|
||||
formatPubkeysWithProfiles,
|
||||
formatHashtags,
|
||||
} from "@/lib/filter-formatters";
|
||||
|
||||
// Memoized FeedEvent to prevent unnecessary re-renders during scroll
|
||||
const MemoizedFeedEvent = memo(
|
||||
@@ -29,6 +38,180 @@ interface ReqViewerProps {
|
||||
nip05PTags?: string[];
|
||||
}
|
||||
|
||||
interface QueryDropdownProps {
|
||||
filter: NostrFilter;
|
||||
nip05Authors?: string[];
|
||||
nip05PTags?: string[];
|
||||
}
|
||||
|
||||
function QueryDropdown({
|
||||
filter,
|
||||
nip05Authors,
|
||||
nip05PTags,
|
||||
}: QueryDropdownProps) {
|
||||
// Load profiles for authors and #p tags
|
||||
const authorPubkeys = filter.authors || [];
|
||||
const authorProfiles = authorPubkeys
|
||||
.slice(0, 10)
|
||||
.map((pubkey) => useProfile(pubkey));
|
||||
|
||||
const pTagPubkeys = filter["#p"] || [];
|
||||
const pTagProfiles = pTagPubkeys
|
||||
.slice(0, 10)
|
||||
.map((pubkey) => useProfile(pubkey));
|
||||
|
||||
// Extract tag filters
|
||||
const eTags = filter["#e"];
|
||||
const tTags = filter["#t"];
|
||||
const dTags = filter["#d"];
|
||||
|
||||
// Find generic tags (exclude #e, #p, #t, #d)
|
||||
const genericTags = Object.entries(filter)
|
||||
.filter(
|
||||
([key]) =>
|
||||
key.startsWith("#") &&
|
||||
key.length === 2 &&
|
||||
!["#e", "#p", "#t", "#d"].includes(key),
|
||||
)
|
||||
.map(([key, values]) => ({ letter: key[1], values: values as string[] }));
|
||||
|
||||
return (
|
||||
<div className="border-b border-border px-4 py-2 bg-muted space-y-2">
|
||||
{/* Kinds */}
|
||||
{filter.kinds && filter.kinds.length > 0 && (
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<span className="text-xs font-semibold text-foreground">Kinds:</span>
|
||||
{filter.kinds.map((kind) => (
|
||||
<KindBadge
|
||||
key={kind}
|
||||
kind={kind}
|
||||
iconClassname="size-3"
|
||||
className="text-xs"
|
||||
clickable
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Time Range */}
|
||||
{(filter.since || filter.until) && (
|
||||
<div className="flex flex-col gap-1">
|
||||
<span className="text-xs font-semibold text-foreground">
|
||||
Time Range:
|
||||
</span>
|
||||
<span className="text-xs ml-2">
|
||||
{formatTimeRange(filter.since, filter.until)}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Search */}
|
||||
{filter.search && (
|
||||
<div className="flex flex-col gap-1">
|
||||
<span className="text-xs font-semibold text-foreground">Search:</span>
|
||||
<span className="text-xs ml-2">"{filter.search}"</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Authors */}
|
||||
{authorPubkeys.length > 0 && (
|
||||
<div className="flex flex-col gap-1">
|
||||
<span className="text-xs font-semibold text-foreground">
|
||||
Authors: {authorPubkeys.length}
|
||||
</span>
|
||||
<div className="text-xs ml-2">
|
||||
{formatPubkeysWithProfiles(authorPubkeys, authorProfiles, 3)}
|
||||
</div>
|
||||
{nip05Authors && nip05Authors.length > 0 && (
|
||||
<div className="text-xs ml-2 mt-1 space-y-0.5">
|
||||
{nip05Authors.map((nip05) => (
|
||||
<div key={nip05}>→ {nip05}</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Tag Filters Section */}
|
||||
{(eTags ||
|
||||
pTagPubkeys.length > 0 ||
|
||||
tTags ||
|
||||
dTags ||
|
||||
genericTags.length > 0) && (
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<span className="text-xs font-semibold text-foreground">
|
||||
Tag Filters:
|
||||
</span>
|
||||
|
||||
{/* Event References (#e) */}
|
||||
{eTags && eTags.length > 0 && (
|
||||
<div className="flex flex-col">
|
||||
<span className="text-xs">#e ({eTags.length}):</span>
|
||||
<span className="text-xs">{formatEventIds(eTags, 3)}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Mentions (#p) */}
|
||||
{pTagPubkeys.length > 0 && (
|
||||
<div className="flex flex-col">
|
||||
<span className="text-xs">#p ({pTagPubkeys.length}):</span>
|
||||
<span className="text-xs">
|
||||
{formatPubkeysWithProfiles(pTagPubkeys, pTagProfiles, 3)}
|
||||
</span>
|
||||
{nip05PTags && nip05PTags.length > 0 && (
|
||||
<div className="text-xs space-y-0.5">
|
||||
{nip05PTags.map((nip05) => (
|
||||
<div key={nip05}>→ {nip05}</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Hashtags (#t) */}
|
||||
{tTags && tTags.length > 0 && (
|
||||
<div className="flex flex-col">
|
||||
<span className="text-xs">#t ({tTags.length}):</span>
|
||||
<span className="text-xs">{formatHashtags(tTags, 3)}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* D-Tags (#d) */}
|
||||
{dTags && dTags.length > 0 && (
|
||||
<div className="flex flex-col">
|
||||
<span className="text-xs">#d ({dTags.length}):</span>
|
||||
<span className="text-xs">{formatDTags(dTags, 3)}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Generic Tags */}
|
||||
{genericTags.map((tag) => (
|
||||
<div key={tag.letter} className="flex flex-col">
|
||||
<span className="text-xs">
|
||||
#{tag.letter} ({tag.values.length}):
|
||||
</span>
|
||||
<span className="text-xs">
|
||||
{formatGenericTag(tag.letter, tag.values, 3).replace(
|
||||
`#${tag.letter}: `,
|
||||
"",
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Raw Query */}
|
||||
<details className="text-xs">
|
||||
<summary className="cursor-pointer">Show raw query</summary>
|
||||
<pre className="mt-2 text-xs font-mono bg-background p-2 border border-border overflow-x-auto">
|
||||
{JSON.stringify(filter, null, 2)}
|
||||
</pre>
|
||||
</details>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function ReqViewer({
|
||||
filter,
|
||||
relays,
|
||||
@@ -155,77 +338,11 @@ export default function ReqViewer({
|
||||
|
||||
{/* Expandable Query */}
|
||||
{showQuery && (
|
||||
<div className="border-b border-border px-4 py-2 bg-muted space-y-2">
|
||||
{/* Kind Badges */}
|
||||
{filter.kinds && filter.kinds.length > 0 && (
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<span className="text-xs text-muted-foreground">Kinds:</span>
|
||||
{filter.kinds.map((kind) => (
|
||||
<KindBadge key={kind} kind={kind} variant="full" />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Authors with NIP-05 info */}
|
||||
{filter.authors && filter.authors.length > 0 && (
|
||||
<div className="flex flex-col gap-1">
|
||||
<span className="text-xs text-muted-foreground">
|
||||
Authors: {filter.authors.length}
|
||||
</span>
|
||||
{nip05Authors && nip05Authors.length > 0 && (
|
||||
<div className="text-xs text-muted-foreground ml-2">
|
||||
{nip05Authors.map((nip05) => (
|
||||
<div key={nip05}>→ {nip05}</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* #p Tags with NIP-05 info */}
|
||||
{filter["#p"] && filter["#p"].length > 0 && (
|
||||
<div className="flex flex-col gap-1">
|
||||
<span className="text-xs text-muted-foreground">
|
||||
#p Tags: {filter["#p"].length}
|
||||
</span>
|
||||
{nip05PTags && nip05PTags.length > 0 && (
|
||||
<div className="text-xs text-muted-foreground ml-2">
|
||||
{nip05PTags.map((nip05) => (
|
||||
<div key={nip05}>→ {nip05}</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Limit */}
|
||||
{filter.limit && (
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xs text-muted-foreground">
|
||||
Limit: {filter.limit}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Stream Mode */}
|
||||
{stream && (
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xs text-green-500">
|
||||
● Streaming mode enabled
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Raw Query */}
|
||||
<details className="text-xs">
|
||||
<summary className="cursor-crosshair text-muted-foreground hover:text-foreground">
|
||||
Query Filter
|
||||
</summary>
|
||||
<pre className="mt-2 text-xs font-mono text-muted-foreground bg-background p-2 overflow-x-auto">
|
||||
{JSON.stringify(filter, null, 2)}
|
||||
</pre>
|
||||
</details>
|
||||
</div>
|
||||
<QueryDropdown
|
||||
filter={filter}
|
||||
nip05Authors={nip05Authors}
|
||||
nip05PTags={nip05PTags}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Error Display */}
|
||||
|
||||
Reference in New Issue
Block a user