From f61d455bda7b52348c31520675bae94c2f873016 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 21 Jan 2026 16:28:59 +0000 Subject: [PATCH] feat: add React option to event menu for emoji reactions Add emoji reaction capability to the generic event menu (dropdown and right-click context menu). When logged in with a signing account, users can now react to any event with unicode or custom NIP-30 emoji. - Add React menu item after Chat in EventMenu and EventContextMenu - Integrate EmojiPickerDialog from chat components - Use ReactionBlueprint from applesauce-common for NIP-25 reactions - Publish reactions to user's outbox relays via publishEvent() - Hidden when user cannot sign (read-only or not logged in) --- .../nostr/kinds/BaseEventRenderer.tsx | 133 ++++++++++++++---- 1 file changed, 102 insertions(+), 31 deletions(-) diff --git a/src/components/nostr/kinds/BaseEventRenderer.tsx b/src/components/nostr/kinds/BaseEventRenderer.tsx index 01d413f..e9cd7d3 100644 --- a/src/components/nostr/kinds/BaseEventRenderer.tsx +++ b/src/components/nostr/kinds/BaseEventRenderer.tsx @@ -23,10 +23,13 @@ import { ExternalLink, Zap, MessageSquare, + SmilePlus, } from "lucide-react"; import { useGrimoire } from "@/core/state"; import { useCopy } from "@/hooks/useCopy"; +import { useAccount } from "@/hooks/useAccount"; import { JsonViewer } from "@/components/JsonViewer"; +import { EmojiPickerDialog } from "@/components/chat/EmojiPickerDialog"; import { formatTimestamp } from "@/hooks/useLocale"; import { nip19 } from "nostr-tools"; import { getTagValue } from "applesauce-core/helpers"; @@ -36,6 +39,10 @@ import { EventFooter } from "@/components/EventFooter"; import { cn } from "@/lib/utils"; import { isAddressableKind } from "@/lib/nostr-kinds"; import { getSemanticAuthor } from "@/lib/semantic-author"; +import { EventFactory } from "applesauce-core/event-factory"; +import { ReactionBlueprint } from "applesauce-common/blueprints"; +import { publishEvent } from "@/services/hub"; +import type { EmojiTag } from "@/lib/emoji-helpers"; /** * Universal event properties and utilities shared across all kind renderers @@ -115,7 +122,15 @@ function ReplyPreview({ /** * Event menu - universal actions for any event */ -export function EventMenu({ event }: { event: NostrEvent }) { +export function EventMenu({ + event, + onReactClick, + canSign, +}: { + event: NostrEvent; + onReactClick?: () => void; + canSign?: boolean; +}) { const { addWindow } = useGrimoire(); const { copy, copied } = useCopy(); const [jsonDialogOpen, setJsonDialogOpen] = useState(false); @@ -241,6 +256,12 @@ export function EventMenu({ event }: { event: NostrEvent }) { Chat )} + {canSign && onReactClick && ( + + + React + + )} {copied ? ( @@ -272,9 +293,13 @@ export function EventMenu({ event }: { event: NostrEvent }) { export function EventContextMenu({ event, children, + onReactClick, + canSign, }: { event: NostrEvent; children: React.ReactNode; + onReactClick?: () => void; + canSign?: boolean; }) { const { addWindow } = useGrimoire(); const { copy, copied } = useCopy(); @@ -397,6 +422,12 @@ export function EventContextMenu({ Chat )} + {canSign && onReactClick && ( + + + React + + )} {copied ? ( @@ -498,6 +529,31 @@ export function BaseEventContainer({ }; }) { const { locale, addWindow } = useGrimoire(); + const { canSign, signer } = useAccount(); + const [emojiPickerOpen, setEmojiPickerOpen] = useState(false); + + const handleReactClick = () => { + setEmojiPickerOpen(true); + }; + + const handleEmojiSelect = async (emoji: string, customEmoji?: EmojiTag) => { + if (!signer) return; + + try { + const factory = new EventFactory(); + factory.setSigner(signer); + + const emojiArg = customEmoji + ? { shortcode: customEmoji.shortcode, url: customEmoji.url } + : emoji; + + const draft = await factory.create(ReactionBlueprint, event, emojiArg); + const signed = await factory.sign(draft); + await publishEvent(signed); + } catch (err) { + console.error("[BaseEventContainer] Failed to send reaction:", err); + } + }; // Format relative time for display const relativeTime = formatTimestamp( @@ -534,38 +590,53 @@ export function BaseEventContainer({ }; return ( - -
-
-
- - - {relativeTime} - - {clientName && ( - - via{" "} - {clientAppPointer ? ( - - ) : ( - clientName - )} + <> + +
+
+
+ + + {relativeTime} - )} + {clientName && ( + + via{" "} + {clientAppPointer ? ( + + ) : ( + clientName + )} + + )} +
+
- + {children} +
- {children} - -
- + + + ); }