From c2f6f1bcd2efc1ef8fb247a089f93e3c3f1716ab Mon Sep 17 00:00:00 2001 From: Alejandro Date: Tue, 20 Jan 2026 10:10:18 +0100 Subject: [PATCH] style: make kind label in event menu more subtle (#168) * style: make kind label in event menu more subtle Reduced visual prominence of kind label in generic event menu: - Smaller text size (text-xs) - Reduced gap between elements (gap-2 instead of gap-4) - More muted colors (text-muted-foreground, opacity-60) - Subtler icon (text-muted-foreground/60) The label now appears as a small informational element rather than looking like an interactive dropdown item. * refactor: remove kind label from menu, add context menu to default renderer 1. Removed kind label from EventMenu dropdown - Kind is already shown in EventFooter, making it redundant - Removed DropdownMenuLabel with kind badges - Removed unused KindBadge import 2. Added EventContextMenu component - Same functionality as EventMenu but triggered by right-click - Reuses all the same menu items (Open, Zap, Copy ID, View JSON) - Supports chat option for kind 1 notes 3. Updated DefaultKindRenderer - Wrapped content with EventContextMenu - Generic events now have context menu on right-click - Updated documentation to indicate right-click access This provides a cleaner UI by removing redundant information and gives users a consistent way to interact with all events, including generic ones that don't have custom renderers. * fix: add context menu to all events in BaseEventContainer Moved EventContextMenu from DefaultKindRenderer to BaseEventContainer so all events (not just generic ones) have context menu support. Now all events in feeds have both interaction methods: - Tap/click the menu button (three dots) for dropdown menu - Right-click or long-press anywhere on the event for context menu This provides a consistent experience across all event types and makes the context menu accessible for all custom renderers, not just the default one. --------- Co-authored-by: Claude --- .../nostr/kinds/BaseEventRenderer.tsx | 209 +++++++++++++++--- src/components/nostr/kinds/index.tsx | 2 + 2 files changed, 183 insertions(+), 28 deletions(-) diff --git a/src/components/nostr/kinds/BaseEventRenderer.tsx b/src/components/nostr/kinds/BaseEventRenderer.tsx index d9b59fc..ed425be 100644 --- a/src/components/nostr/kinds/BaseEventRenderer.tsx +++ b/src/components/nostr/kinds/BaseEventRenderer.tsx @@ -1,15 +1,20 @@ import { useState } from "react"; import { NostrEvent } from "@/types/nostr"; import { UserName } from "../UserName"; -import { KindBadge } from "@/components/KindBadge"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, - DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; +import { + ContextMenu, + ContextMenuContent, + ContextMenuItem, + ContextMenuSeparator, + ContextMenuTrigger, +} from "@/components/ui/context-menu"; import { Menu, Copy, @@ -223,18 +228,6 @@ export function EventMenu({ event }: { event: NostrEvent }) { - -
- - -
-
- Open @@ -273,6 +266,164 @@ export function EventMenu({ event }: { event: NostrEvent }) { ); } +/** + * Event context menu - same actions as EventMenu but triggered by right-click + * Used for generic event renderers that don't have a built-in menu button + */ +export function EventContextMenu({ + event, + children, +}: { + event: NostrEvent; + children: React.ReactNode; +}) { + const { addWindow } = useGrimoire(); + const { copy, copied } = useCopy(); + const [jsonDialogOpen, setJsonDialogOpen] = useState(false); + + const openEventDetail = () => { + let pointer; + // For replaceable/parameterized replaceable events, use AddressPointer + if (isAddressableKind(event.kind)) { + // Find d-tag for identifier + const dTag = getTagValue(event, "d") || ""; + pointer = { + kind: event.kind, + pubkey: event.pubkey, + identifier: dTag, + }; + } else { + // For regular events, use EventPointer + pointer = { + id: event.id, + }; + } + + addWindow("open", { pointer }); + }; + + const copyEventId = () => { + // Get relay hints from where the event has been seen + const seenRelaysSet = getSeenRelays(event); + const relays = seenRelaysSet ? Array.from(seenRelaysSet) : []; + + // For replaceable/parameterized replaceable events, encode as naddr + if (isAddressableKind(event.kind)) { + // Find d-tag for identifier + const dTag = getTagValue(event, "d") || ""; + const naddr = nip19.naddrEncode({ + kind: event.kind, + pubkey: event.pubkey, + identifier: dTag, + relays: relays, + }); + copy(naddr); + } else { + // For regular events, encode as nevent + const nevent = nip19.neventEncode({ + id: event.id, + author: event.pubkey, + relays: relays, + }); + copy(nevent); + } + }; + + const viewEventJson = () => { + setJsonDialogOpen(true); + }; + + const zapEvent = () => { + // Create event pointer for the zap + let eventPointer; + if (isAddressableKind(event.kind)) { + const dTag = getTagValue(event, "d") || ""; + eventPointer = { + kind: event.kind, + pubkey: event.pubkey, + identifier: dTag, + }; + } else { + eventPointer = { + id: event.id, + }; + } + + // Get semantic author (e.g., zapper for zaps, host for streams) + const recipientPubkey = getSemanticAuthor(event); + + // Open zap window with event context + addWindow("zap", { + recipientPubkey, + eventPointer, + }); + }; + + const openChatWindow = () => { + // Only kind 1 notes support NIP-10 thread chat + if (event.kind === 1) { + const seenRelaysSet = getSeenRelays(event); + const relays = seenRelaysSet ? Array.from(seenRelaysSet) : []; + + // Open chat with NIP-10 thread protocol + addWindow("chat", { + protocol: "nip-10", + identifier: { + type: "thread", + value: { + id: event.id, + relays, + author: event.pubkey, + kind: event.kind, + }, + relays, + }, + }); + } + }; + + return ( + + {children} + + + + Open + + + + Zap + + {event.kind === 1 && ( + + + Chat + + )} + + + {copied ? ( + + ) : ( + + )} + {copied ? "Copied!" : "Copy ID"} + + + + View JSON + + + + + ); +} + /** * Clickable event title component * Opens the event in a new window when clicked @@ -369,21 +520,23 @@ export function BaseEventContainer({ const displayPubkey = authorOverride?.pubkey || event.pubkey; return ( -
-
-
- - - {relativeTime} - + +
+
+
+ + + {relativeTime} + +
+
- + {children} +
- {children} - -
+ ); } diff --git a/src/components/nostr/kinds/index.tsx b/src/components/nostr/kinds/index.tsx index 1a8396e..d14e79b 100644 --- a/src/components/nostr/kinds/index.tsx +++ b/src/components/nostr/kinds/index.tsx @@ -236,6 +236,7 @@ const kindRenderers: Record> = { /** * Default renderer for kinds without custom implementations * Shows basic event info with raw content + * Right-click or tap menu button to access event menu */ function DefaultKindRenderer({ event }: BaseEventProps) { return ( @@ -355,6 +356,7 @@ export { BaseEventContainer, EventAuthor, EventMenu, + EventContextMenu, } from "./BaseEventRenderer"; export type { BaseEventProps } from "./BaseEventRenderer"; export { Kind1Renderer } from "./NoteRenderer";