mirror of
https://github.com/purrgrammer/grimoire.git
synced 2026-06-06 10:41:21 +02:00
Add NIP-56 Report (Kind 1984) renderer and helpers (#210)
* Add NIP-56 Report renderer (kind 1984) - Add nip56-helpers.ts with report parsing and type definitions - Add ReportRenderer for displaying report events in feeds - Support profile, event, and blob report targets - Display report type with color-coded badges and icons - Show embedded reported event when available https://claude.ai/code/session_012ux81GyM8iZ1GLnKHC7esJ * refactor(nip56): apply applesauce caching and neutral styling - Use getOrComputeCachedValue for report parsing (applesauce pattern) - Rename parseReport to getReportInfo for consistency - Use muted/neutral colors for all report type icons and badges - Use QuotedEvent component for embedding reported events - Remove unnecessary useMemo (helper caches internally) https://claude.ai/code/session_012ux81GyM8iZ1GLnKHC7esJ * refactor(nip56): use collapsed quote and cleaner copy - Use "Reported <username> for <reason>" format - Remove redundant "Event by:" line for event reports - Use depth=2 for QuotedEvent to show collapsed by default - Content may be disturbing, user can expand on demand https://claude.ai/code/session_012ux81GyM8iZ1GLnKHC7esJ * feat(nip56): hide preview and clickable header - Add hidePreview prop to QuotedEvent for sensitive content - Hide text preview when collapsed, show "Click to reveal content" - Make report header clickable to open report detail - UserName stops propagation so clicking username opens profile https://claude.ai/code/session_012ux81GyM8iZ1GLnKHC7esJ * style(nip56): use dotted underline hover, reduce spacing - Remove background highlight on hover - Use underline dotted with cursor crosshair (consistent with app) - Reduce gap between header and quoted event https://claude.ai/code/session_012ux81GyM8iZ1GLnKHC7esJ * refactor(nip56): use RichText for comment and applesauce helpers - Render report comments using RichText like kind 1 notes - Use getTagValue/getTagValues helpers instead of direct tag access - Add explanatory comments where direct tag access is still required https://claude.ai/code/session_012ux81GyM8iZ1GLnKHC7esJ --------- Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -18,6 +18,8 @@ interface QuotedEventProps {
|
||||
depth?: number;
|
||||
/** Optional className for container */
|
||||
className?: string;
|
||||
/** Hide preview text when collapsed (for sensitive content) */
|
||||
hidePreview?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -31,6 +33,7 @@ export function QuotedEvent({
|
||||
onOpen,
|
||||
depth = 1,
|
||||
className,
|
||||
hidePreview = false,
|
||||
}: QuotedEventProps) {
|
||||
const [isExpanded, setIsExpanded] = useState(depth < 2);
|
||||
|
||||
@@ -99,10 +102,17 @@ export function QuotedEvent({
|
||||
>
|
||||
<div className="flex items-center gap-2 min-w-0">
|
||||
<UserName pubkey={event.pubkey} className="text-xs font-medium" />
|
||||
<span className="text-xs text-muted-foreground truncate">
|
||||
{previewText}
|
||||
{hasMore && "..."}
|
||||
</span>
|
||||
{!hidePreview && (
|
||||
<span className="text-xs text-muted-foreground truncate">
|
||||
{previewText}
|
||||
{hasMore && "..."}
|
||||
</span>
|
||||
)}
|
||||
{hidePreview && !isExpanded && (
|
||||
<span className="text-xs text-muted-foreground italic">
|
||||
Click to reveal content
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{isExpanded ? (
|
||||
<ChevronUp className="size-3 flex-shrink-0" />
|
||||
|
||||
134
src/components/nostr/kinds/ReportRenderer.tsx
Normal file
134
src/components/nostr/kinds/ReportRenderer.tsx
Normal file
@@ -0,0 +1,134 @@
|
||||
/**
|
||||
* NIP-56: Report Renderer (Kind 1984)
|
||||
*
|
||||
* Displays report events that signal objectionable content.
|
||||
* Reports can target profiles, events, or blobs.
|
||||
*/
|
||||
|
||||
import {
|
||||
Flag,
|
||||
AlertTriangle,
|
||||
Bug,
|
||||
MessageSquareWarning,
|
||||
Gavel,
|
||||
Mail,
|
||||
UserX,
|
||||
HelpCircle,
|
||||
} from "lucide-react";
|
||||
import {
|
||||
BaseEventProps,
|
||||
BaseEventContainer,
|
||||
ClickableEventTitle,
|
||||
} from "./BaseEventRenderer";
|
||||
import { QuotedEvent } from "@/components/nostr/QuotedEvent";
|
||||
import { UserName } from "@/components/nostr/UserName";
|
||||
import { RichText } from "../RichText";
|
||||
import {
|
||||
getReportInfo,
|
||||
type ReportType,
|
||||
REPORT_TYPE_LABELS,
|
||||
} from "@/lib/nip56-helpers";
|
||||
|
||||
/**
|
||||
* Get icon for report type (all muted/neutral colors)
|
||||
*/
|
||||
function getReportTypeIcon(reportType: ReportType) {
|
||||
const className = "size-3.5 text-muted-foreground";
|
||||
switch (reportType) {
|
||||
case "nudity":
|
||||
return <AlertTriangle className={className} />;
|
||||
case "malware":
|
||||
return <Bug className={className} />;
|
||||
case "profanity":
|
||||
return <MessageSquareWarning className={className} />;
|
||||
case "illegal":
|
||||
return <Gavel className={className} />;
|
||||
case "spam":
|
||||
return <Mail className={className} />;
|
||||
case "impersonation":
|
||||
return <UserX className={className} />;
|
||||
case "other":
|
||||
default:
|
||||
return <HelpCircle className={className} />;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Renderer for Kind 1984 - Reports (NIP-56)
|
||||
*/
|
||||
export function ReportRenderer({ event }: BaseEventProps) {
|
||||
// Parse report using cached helper (no useMemo needed - applesauce caches internally)
|
||||
const report = getReportInfo(event);
|
||||
|
||||
if (!report) {
|
||||
return (
|
||||
<BaseEventContainer event={event}>
|
||||
<div className="text-sm text-muted-foreground">
|
||||
Invalid report event (missing required tags)
|
||||
</div>
|
||||
</BaseEventContainer>
|
||||
);
|
||||
}
|
||||
|
||||
const reasonLabel = REPORT_TYPE_LABELS[report.reportType].toLowerCase();
|
||||
|
||||
return (
|
||||
<BaseEventContainer event={event}>
|
||||
<div className="flex flex-col gap-2">
|
||||
{/* Report header: "Reported <username> for <reason>" */}
|
||||
<ClickableEventTitle
|
||||
event={event}
|
||||
as="div"
|
||||
className="flex items-center gap-1.5 flex-wrap text-sm"
|
||||
>
|
||||
<Flag className="size-4 text-muted-foreground flex-shrink-0" />
|
||||
<span className="text-muted-foreground">Reported</span>
|
||||
<UserName pubkey={report.reportedPubkey} />
|
||||
<span className="inline-flex items-center gap-1 px-1.5 py-0.5 rounded text-xs font-medium bg-muted text-muted-foreground">
|
||||
for {getReportTypeIcon(report.reportType)} {reasonLabel}
|
||||
</span>
|
||||
</ClickableEventTitle>
|
||||
|
||||
{/* Reported event - collapsed with hidden preview (depth=2, hidePreview) */}
|
||||
{report.targetType === "event" && report.reportedEventId && (
|
||||
<QuotedEvent
|
||||
eventPointer={{ id: report.reportedEventId }}
|
||||
depth={2}
|
||||
hidePreview
|
||||
className="mt-0"
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Blob details */}
|
||||
{report.targetType === "blob" && (
|
||||
<div className="flex flex-col gap-1 text-sm">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-muted-foreground">Hash:</span>
|
||||
<code className="text-xs bg-muted px-1.5 py-0.5 rounded font-mono">
|
||||
{report.reportedBlobHash?.slice(0, 16)}...
|
||||
</code>
|
||||
</div>
|
||||
{report.serverUrls && report.serverUrls.length > 0 && (
|
||||
<div className="text-xs text-muted-foreground">
|
||||
Server: {report.serverUrls[0]}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Report comment - rendered like a kind 1 note */}
|
||||
{event.content && <RichText event={event} className="text-sm" />}
|
||||
</div>
|
||||
</BaseEventContainer>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Detail renderer for Kind 1984 - Reports
|
||||
* Shows full report details with raw data
|
||||
*/
|
||||
export function ReportDetailRenderer({ event }: BaseEventProps) {
|
||||
// For now, use the same renderer for detail view
|
||||
// Could be enhanced later with more detailed info
|
||||
return <ReportRenderer event={event} />;
|
||||
}
|
||||
@@ -157,6 +157,7 @@ import { GoalDetailRenderer } from "./GoalDetailRenderer";
|
||||
import { PollRenderer } from "./PollRenderer";
|
||||
import { PollDetailRenderer } from "./PollDetailRenderer";
|
||||
import { PollResponseRenderer } from "./PollResponseRenderer";
|
||||
import { ReportRenderer, ReportDetailRenderer } from "./ReportRenderer";
|
||||
|
||||
/**
|
||||
* Registry of kind-specific renderers
|
||||
@@ -187,6 +188,7 @@ const kindRenderers: Record<number, React.ComponentType<BaseEventProps>> = {
|
||||
1617: PatchRenderer, // Patch (NIP-34)
|
||||
1618: PullRequestRenderer, // Pull Request (NIP-34)
|
||||
1621: IssueRenderer, // Issue (NIP-34)
|
||||
1984: ReportRenderer, // Report (NIP-56)
|
||||
9041: GoalRenderer, // Zap Goal (NIP-75)
|
||||
9735: Kind9735Renderer, // Zap Receipt
|
||||
9802: Kind9802Renderer, // Highlight
|
||||
@@ -296,6 +298,7 @@ const detailRenderers: Record<
|
||||
1617: PatchDetailRenderer, // Patch Detail (NIP-34)
|
||||
1618: PullRequestDetailRenderer, // Pull Request Detail (NIP-34)
|
||||
1621: IssueDetailRenderer, // Issue Detail (NIP-34)
|
||||
1984: ReportDetailRenderer, // Report Detail (NIP-56)
|
||||
9041: GoalDetailRenderer, // Zap Goal Detail (NIP-75)
|
||||
9802: Kind9802DetailRenderer, // Highlight Detail
|
||||
8000: AddUserDetailRenderer, // Add User Detail (NIP-43)
|
||||
|
||||
Reference in New Issue
Block a user