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 <noreply@anthropic.com>
This commit is contained in:
Alejandro
2026-01-20 10:10:18 +01:00
committed by GitHub
parent 83b3b0e416
commit c2f6f1bcd2
2 changed files with 183 additions and 28 deletions

View File

@@ -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 }) {
</button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-56">
<DropdownMenuLabel>
<div className="flex flex-row items-center gap-4">
<KindBadge kind={event.kind} variant="compact" />
<KindBadge
kind={event.kind}
showName
showKindNumber
showIcon={false}
/>
</div>
</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuItem onClick={openEventDetail}>
<ExternalLink className="size-4 mr-2" />
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 (
<ContextMenu>
<ContextMenuTrigger asChild>{children}</ContextMenuTrigger>
<ContextMenuContent className="w-56">
<ContextMenuItem onClick={openEventDetail}>
<ExternalLink className="size-4 mr-2" />
Open
</ContextMenuItem>
<ContextMenuItem onClick={zapEvent}>
<Zap className="size-4 mr-2 text-yellow-500" />
Zap
</ContextMenuItem>
{event.kind === 1 && (
<ContextMenuItem onClick={openChatWindow}>
<MessageSquare className="size-4 mr-2" />
Chat
</ContextMenuItem>
)}
<ContextMenuSeparator />
<ContextMenuItem onClick={copyEventId}>
{copied ? (
<Check className="size-4 mr-2 text-green-500" />
) : (
<Copy className="size-4 mr-2" />
)}
{copied ? "Copied!" : "Copy ID"}
</ContextMenuItem>
<ContextMenuItem onClick={viewEventJson}>
<FileJson className="size-4 mr-2" />
View JSON
</ContextMenuItem>
</ContextMenuContent>
<JsonViewer
data={event}
open={jsonDialogOpen}
onOpenChange={setJsonDialogOpen}
title={`Event ${event.id.slice(0, 8)}... - Raw JSON`}
/>
</ContextMenu>
);
}
/**
* 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 (
<div className="flex flex-col gap-2 p-3 border-b border-border/50 last:border-0">
<div className="flex flex-row justify-between items-center">
<div className="flex flex-row gap-2 items-baseline">
<EventAuthor pubkey={displayPubkey} />
<span
className="text-xs text-muted-foreground cursor-help"
title={absoluteTime}
>
{relativeTime}
</span>
<EventContextMenu event={event}>
<div className="flex flex-col gap-2 p-3 border-b border-border/50 last:border-0">
<div className="flex flex-row justify-between items-center">
<div className="flex flex-row gap-2 items-baseline">
<EventAuthor pubkey={displayPubkey} />
<span
className="text-xs text-muted-foreground cursor-help"
title={absoluteTime}
>
{relativeTime}
</span>
</div>
<EventMenu event={event} />
</div>
<EventMenu event={event} />
{children}
<EventFooter event={event} />
</div>
{children}
<EventFooter event={event} />
</div>
</EventContextMenu>
);
}

View File

@@ -236,6 +236,7 @@ const kindRenderers: Record<number, React.ComponentType<BaseEventProps>> = {
/**
* 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";