mirror of
https://github.com/purrgrammer/grimoire.git
synced 2026-04-08 22:47:02 +02:00
perf: deduplicate event menu hooks and memoize kind renderers
Extract shared useEventActions hook so EventMenu and EventContextMenu share a single set of hook subscriptions instead of duplicating them. Wrap callbacks in useCallback and memoize KindRenderer/DetailKindRenderer to reduce unnecessary re-renders in event feeds. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { useState } from "react";
|
||||
import { useState, useCallback } from "react";
|
||||
import { NostrEvent } from "@/types/nostr";
|
||||
import { UserName } from "../UserName";
|
||||
import {
|
||||
@@ -129,17 +129,10 @@ function ReplyPreview({
|
||||
*/
|
||||
|
||||
/**
|
||||
* Event menu - universal actions for any event
|
||||
* Shared event action state — used by both EventMenu and EventContextMenu
|
||||
* to avoid duplicate hook subscriptions when both are rendered together.
|
||||
*/
|
||||
export function EventMenu({
|
||||
event,
|
||||
onReactClick,
|
||||
canSign,
|
||||
}: {
|
||||
event: NostrEvent;
|
||||
onReactClick?: () => void;
|
||||
canSign?: boolean;
|
||||
}) {
|
||||
function useEventActions(event: NostrEvent) {
|
||||
const addWindow = useAddWindow();
|
||||
const { copy, copied } = useCopy();
|
||||
const [jsonDialogOpen, setJsonDialogOpen] = useState(false);
|
||||
@@ -149,11 +142,9 @@ export function EventMenu({
|
||||
);
|
||||
const favorited = favoriteConfig ? isFavorite(event) : false;
|
||||
|
||||
const openEventDetail = () => {
|
||||
const openEventDetail = useCallback(() => {
|
||||
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,
|
||||
@@ -161,51 +152,43 @@ export function EventMenu({
|
||||
identifier: dTag,
|
||||
};
|
||||
} else {
|
||||
// For regular events, use EventPointer
|
||||
pointer = {
|
||||
id: event.id,
|
||||
};
|
||||
pointer = { id: event.id };
|
||||
}
|
||||
|
||||
addWindow("open", { pointer });
|
||||
};
|
||||
}, [event, addWindow]);
|
||||
|
||||
const copyEventId = () => {
|
||||
// Get relay hints from where the event has been seen
|
||||
const copyEventId = useCallback(() => {
|
||||
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);
|
||||
copy(
|
||||
nip19.naddrEncode({
|
||||
kind: event.kind,
|
||||
pubkey: event.pubkey,
|
||||
identifier: dTag,
|
||||
relays,
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
// For regular events, encode as nevent
|
||||
const nevent = nip19.neventEncode({
|
||||
id: event.id,
|
||||
author: event.pubkey,
|
||||
relays: relays,
|
||||
});
|
||||
copy(nevent);
|
||||
copy(
|
||||
nip19.neventEncode({
|
||||
id: event.id,
|
||||
author: event.pubkey,
|
||||
relays,
|
||||
}),
|
||||
);
|
||||
}
|
||||
};
|
||||
}, [event, copy]);
|
||||
|
||||
const viewEventJson = () => {
|
||||
const viewEventJson = useCallback(() => {
|
||||
setJsonDialogOpen(true);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const zapEvent = () => {
|
||||
// Get semantic author (e.g., zapper for zaps, host for streams)
|
||||
const zapEvent = useCallback(() => {
|
||||
const recipientPubkey = getSemanticAuthor(event);
|
||||
|
||||
// For addressable events, use addressPointer; for regular events, use eventPointer
|
||||
if (isAddressableKind(event.kind)) {
|
||||
const dTag = getTagValue(event, "d") || "";
|
||||
addWindow("zap", {
|
||||
@@ -223,15 +206,13 @@ export function EventMenu({
|
||||
eventPointer: { id: event.id },
|
||||
});
|
||||
}
|
||||
};
|
||||
}, [event, addWindow]);
|
||||
|
||||
const openChatWindow = () => {
|
||||
// Only kind 1 notes support NIP-10 thread chat
|
||||
const openChatWindow = useCallback(() => {
|
||||
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: {
|
||||
@@ -246,8 +227,117 @@ export function EventMenu({
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
}, [event, addWindow]);
|
||||
|
||||
const handleToggleFavorite = useCallback(() => {
|
||||
toggleFavorite(event);
|
||||
}, [toggleFavorite, event]);
|
||||
|
||||
return {
|
||||
openEventDetail,
|
||||
copyEventId,
|
||||
viewEventJson,
|
||||
zapEvent,
|
||||
openChatWindow,
|
||||
handleToggleFavorite,
|
||||
copied,
|
||||
jsonDialogOpen,
|
||||
setJsonDialogOpen,
|
||||
favoriteConfig,
|
||||
favorited,
|
||||
isUpdating,
|
||||
};
|
||||
}
|
||||
|
||||
type EventActions = ReturnType<typeof useEventActions>;
|
||||
|
||||
interface EventMenuItemsProps {
|
||||
event: NostrEvent;
|
||||
actions: EventActions;
|
||||
onReactClick?: () => void;
|
||||
canSign?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Shared menu items rendered as either DropdownMenuItems or ContextMenuItems
|
||||
*/
|
||||
function EventMenuItems({
|
||||
Item,
|
||||
Separator,
|
||||
event,
|
||||
actions,
|
||||
onReactClick,
|
||||
canSign,
|
||||
}: EventMenuItemsProps & {
|
||||
Item: typeof DropdownMenuItem;
|
||||
Separator: typeof DropdownMenuSeparator;
|
||||
}) {
|
||||
return (
|
||||
<>
|
||||
<Item onClick={actions.openEventDetail}>
|
||||
<ExternalLink className="size-4 mr-2" />
|
||||
Open
|
||||
</Item>
|
||||
<Item onClick={actions.zapEvent}>
|
||||
<Zap className="size-4 mr-2 text-yellow-500" />
|
||||
Zap
|
||||
</Item>
|
||||
{event.kind === 1 && (
|
||||
<Item onClick={actions.openChatWindow}>
|
||||
<MessageSquare className="size-4 mr-2" />
|
||||
Chat
|
||||
</Item>
|
||||
)}
|
||||
{canSign && onReactClick && (
|
||||
<Item onClick={onReactClick}>
|
||||
<SmilePlus className="size-4 mr-2" />
|
||||
React
|
||||
</Item>
|
||||
)}
|
||||
{canSign && actions.favoriteConfig && (
|
||||
<Item
|
||||
onClick={actions.handleToggleFavorite}
|
||||
disabled={actions.isUpdating}
|
||||
>
|
||||
{actions.isUpdating ? (
|
||||
<Loader2 className="size-4 mr-2 animate-spin" />
|
||||
) : (
|
||||
<Bookmark
|
||||
className={cn(
|
||||
"size-4 mr-2",
|
||||
actions.favorited && "text-yellow-500 fill-current",
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
{actions.favorited ? "Unbookmark" : "Bookmark"}
|
||||
</Item>
|
||||
)}
|
||||
<Separator />
|
||||
<Item onClick={actions.copyEventId}>
|
||||
{actions.copied ? (
|
||||
<CopyCheck className="size-4 mr-2 text-success" />
|
||||
) : (
|
||||
<Copy className="size-4 mr-2" />
|
||||
)}
|
||||
{actions.copied ? "Copied!" : "Copy ID"}
|
||||
</Item>
|
||||
<Item onClick={actions.viewEventJson}>
|
||||
<FileJson className="size-4 mr-2" />
|
||||
View JSON
|
||||
</Item>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Event menu - universal actions for any event (dropdown trigger)
|
||||
*/
|
||||
export function EventMenu({
|
||||
event,
|
||||
actions,
|
||||
onReactClick,
|
||||
canSign,
|
||||
}: EventMenuItemsProps) {
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
@@ -256,251 +346,42 @@ export function EventMenu({
|
||||
</button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="w-56">
|
||||
<DropdownMenuItem onClick={openEventDetail}>
|
||||
<ExternalLink className="size-4 mr-2" />
|
||||
Open
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={zapEvent}>
|
||||
<Zap className="size-4 mr-2 text-yellow-500" />
|
||||
Zap
|
||||
</DropdownMenuItem>
|
||||
{event.kind === 1 && (
|
||||
<DropdownMenuItem onClick={openChatWindow}>
|
||||
<MessageSquare className="size-4 mr-2" />
|
||||
Chat
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
{canSign && onReactClick && (
|
||||
<DropdownMenuItem onClick={onReactClick}>
|
||||
<SmilePlus className="size-4 mr-2" />
|
||||
React
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
{canSign && favoriteConfig && (
|
||||
<DropdownMenuItem
|
||||
onClick={() => toggleFavorite(event)}
|
||||
disabled={isUpdating}
|
||||
>
|
||||
{isUpdating ? (
|
||||
<Loader2 className="size-4 mr-2 animate-spin" />
|
||||
) : (
|
||||
<Bookmark
|
||||
className={cn(
|
||||
"size-4 mr-2",
|
||||
favorited && "text-yellow-500 fill-current",
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
{favorited ? "Unbookmark" : "Bookmark"}
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem onClick={copyEventId}>
|
||||
{copied ? (
|
||||
<CopyCheck className="size-4 mr-2 text-success" />
|
||||
) : (
|
||||
<Copy className="size-4 mr-2" />
|
||||
)}
|
||||
{copied ? "Copied!" : "Copy ID"}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={viewEventJson}>
|
||||
<FileJson className="size-4 mr-2" />
|
||||
View JSON
|
||||
</DropdownMenuItem>
|
||||
<EventMenuItems
|
||||
Item={DropdownMenuItem}
|
||||
Separator={DropdownMenuSeparator}
|
||||
event={event}
|
||||
actions={actions}
|
||||
onReactClick={onReactClick}
|
||||
canSign={canSign}
|
||||
/>
|
||||
</DropdownMenuContent>
|
||||
<EventJsonDialog
|
||||
event={event}
|
||||
open={jsonDialogOpen}
|
||||
onOpenChange={setJsonDialogOpen}
|
||||
/>
|
||||
</DropdownMenu>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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,
|
||||
actions,
|
||||
onReactClick,
|
||||
canSign,
|
||||
}: {
|
||||
event: NostrEvent;
|
||||
children: React.ReactNode;
|
||||
onReactClick?: () => void;
|
||||
canSign?: boolean;
|
||||
}) {
|
||||
const addWindow = useAddWindow();
|
||||
const { copy, copied } = useCopy();
|
||||
const [jsonDialogOpen, setJsonDialogOpen] = useState(false);
|
||||
const favoriteConfig = getFavoriteConfig(event.kind);
|
||||
const { isFavorite, toggleFavorite, isUpdating } = useFavoriteList(
|
||||
favoriteConfig ?? FALLBACK_FAVORITE_CONFIG,
|
||||
);
|
||||
const favorited = favoriteConfig ? isFavorite(event) : 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 = () => {
|
||||
// Get semantic author (e.g., zapper for zaps, host for streams)
|
||||
const recipientPubkey = getSemanticAuthor(event);
|
||||
|
||||
// For addressable events, use addressPointer; for regular events, use eventPointer
|
||||
if (isAddressableKind(event.kind)) {
|
||||
const dTag = getTagValue(event, "d") || "";
|
||||
addWindow("zap", {
|
||||
recipientPubkey,
|
||||
eventPointer: { id: event.id },
|
||||
addressPointer: {
|
||||
kind: event.kind,
|
||||
pubkey: event.pubkey,
|
||||
identifier: dTag,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
addWindow("zap", {
|
||||
recipientPubkey,
|
||||
eventPointer: { id: event.id },
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
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,
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
}: EventMenuItemsProps & { children: React.ReactNode }) {
|
||||
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>
|
||||
)}
|
||||
{canSign && onReactClick && (
|
||||
<ContextMenuItem onClick={onReactClick}>
|
||||
<SmilePlus className="size-4 mr-2" />
|
||||
React
|
||||
</ContextMenuItem>
|
||||
)}
|
||||
{canSign && favoriteConfig && (
|
||||
<ContextMenuItem
|
||||
onClick={() => toggleFavorite(event)}
|
||||
disabled={isUpdating}
|
||||
>
|
||||
{isUpdating ? (
|
||||
<Loader2 className="size-4 mr-2 animate-spin" />
|
||||
) : (
|
||||
<Bookmark
|
||||
className={cn(
|
||||
"size-4 mr-2",
|
||||
favorited && "text-yellow-500 fill-current",
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
{favorited ? "Unbookmark" : "Bookmark"}
|
||||
</ContextMenuItem>
|
||||
)}
|
||||
<ContextMenuSeparator />
|
||||
<ContextMenuItem onClick={copyEventId}>
|
||||
{copied ? (
|
||||
<CopyCheck className="size-4 mr-2 text-success" />
|
||||
) : (
|
||||
<Copy className="size-4 mr-2" />
|
||||
)}
|
||||
{copied ? "Copied!" : "Copy ID"}
|
||||
</ContextMenuItem>
|
||||
<ContextMenuItem onClick={viewEventJson}>
|
||||
<FileJson className="size-4 mr-2" />
|
||||
View JSON
|
||||
</ContextMenuItem>
|
||||
<EventMenuItems
|
||||
Item={ContextMenuItem}
|
||||
Separator={ContextMenuSeparator}
|
||||
event={event}
|
||||
actions={actions}
|
||||
onReactClick={onReactClick}
|
||||
canSign={canSign}
|
||||
/>
|
||||
</ContextMenuContent>
|
||||
<EventJsonDialog
|
||||
event={event}
|
||||
open={jsonDialogOpen}
|
||||
onOpenChange={setJsonDialogOpen}
|
||||
/>
|
||||
</ContextMenu>
|
||||
);
|
||||
}
|
||||
@@ -585,57 +466,55 @@ export function BaseEventContainer({
|
||||
const { canSign, signer, pubkey } = useAccount();
|
||||
const { settings } = useSettings();
|
||||
const [emojiPickerOpen, setEmojiPickerOpen] = useState(false);
|
||||
const actions = useEventActions(event);
|
||||
|
||||
const handleReactClick = () => {
|
||||
const handleReactClick = useCallback(() => {
|
||||
setEmojiPickerOpen(true);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const handleEmojiSelect = async (emoji: string, customEmoji?: EmojiTag) => {
|
||||
if (!signer || !pubkey) return;
|
||||
const handleEmojiSelect = useCallback(
|
||||
async (emoji: string, customEmoji?: EmojiTag) => {
|
||||
if (!signer || !pubkey) return;
|
||||
|
||||
try {
|
||||
const factory = new EventFactory();
|
||||
factory.setSigner(signer);
|
||||
try {
|
||||
const factory = new EventFactory();
|
||||
factory.setSigner(signer);
|
||||
|
||||
const emojiArg = customEmoji
|
||||
? {
|
||||
shortcode: customEmoji.shortcode,
|
||||
url: customEmoji.url,
|
||||
address: customEmoji.address,
|
||||
}
|
||||
: emoji;
|
||||
const emojiArg = customEmoji
|
||||
? {
|
||||
shortcode: customEmoji.shortcode,
|
||||
url: customEmoji.url,
|
||||
address: customEmoji.address,
|
||||
}
|
||||
: emoji;
|
||||
|
||||
const draft = await factory.create(ReactionBlueprint, event, emojiArg);
|
||||
const signed = await factory.sign(draft);
|
||||
const draft = await factory.create(ReactionBlueprint, event, emojiArg);
|
||||
const signed = await factory.sign(draft);
|
||||
|
||||
// Select relays per NIP-65: author's outbox + target's inbox
|
||||
// Use semantic author (e.g., zapper for zaps, host for streams)
|
||||
const targetPubkey = getSemanticAuthor(event);
|
||||
const relays = await selectRelaysForInteraction(pubkey, targetPubkey);
|
||||
await publishEventToRelays(signed, relays);
|
||||
} catch (err) {
|
||||
console.error("[BaseEventContainer] Failed to send reaction:", err);
|
||||
}
|
||||
};
|
||||
const targetPubkey = getSemanticAuthor(event);
|
||||
const relays = await selectRelaysForInteraction(pubkey, targetPubkey);
|
||||
await publishEventToRelays(signed, relays);
|
||||
} catch (err) {
|
||||
console.error("[BaseEventContainer] Failed to send reaction:", err);
|
||||
}
|
||||
},
|
||||
[signer, pubkey, event],
|
||||
);
|
||||
|
||||
// Format relative time for display
|
||||
const relativeTime = formatTimestamp(
|
||||
event.created_at,
|
||||
"relative",
|
||||
locale.locale,
|
||||
);
|
||||
|
||||
// Format absolute timestamp for hover (ISO-8601 style)
|
||||
const absoluteTime = formatTimestamp(
|
||||
event.created_at,
|
||||
"absolute",
|
||||
locale.locale,
|
||||
);
|
||||
|
||||
// Use author override if provided, otherwise use event author
|
||||
const displayPubkey = authorOverride?.pubkey || event.pubkey;
|
||||
|
||||
// Get client tag if present: ["client", "<name>", "<31990:pubkey:d-tag>"]
|
||||
const clientTag = event.tags.find((t) => t[0] === "client");
|
||||
const clientName = clientTag?.[1];
|
||||
const clientAddress = clientTag?.[2];
|
||||
@@ -649,6 +528,7 @@ export function BaseEventContainer({
|
||||
<>
|
||||
<EventContextMenu
|
||||
event={event}
|
||||
actions={actions}
|
||||
onReactClick={handleReactClick}
|
||||
canSign={canSign}
|
||||
>
|
||||
@@ -665,6 +545,7 @@ export function BaseEventContainer({
|
||||
</div>
|
||||
<EventMenu
|
||||
event={event}
|
||||
actions={actions}
|
||||
onReactClick={handleReactClick}
|
||||
canSign={canSign}
|
||||
/>
|
||||
@@ -679,6 +560,11 @@ export function BaseEventContainer({
|
||||
/>
|
||||
</div>
|
||||
</EventContextMenu>
|
||||
<EventJsonDialog
|
||||
event={event}
|
||||
open={actions.jsonDialogOpen}
|
||||
onOpenChange={actions.setJsonDialogOpen}
|
||||
/>
|
||||
<EmojiPickerDialog
|
||||
open={emojiPickerOpen}
|
||||
onOpenChange={setEmojiPickerOpen}
|
||||
|
||||
@@ -152,6 +152,7 @@ import {
|
||||
MediaStarterPackRenderer,
|
||||
MediaStarterPackDetailRenderer,
|
||||
} from "./StarterPackRenderer";
|
||||
import { memo } from "react";
|
||||
import { NostrEvent } from "@/types/nostr";
|
||||
import { BaseEventContainer, type BaseEventProps } from "./BaseEventRenderer";
|
||||
import { P2pOrderRenderer } from "./P2pOrderRenderer";
|
||||
@@ -310,7 +311,9 @@ const kindRenderers: Record<number, React.ComponentType<BaseEventProps>> = {
|
||||
* Shows basic event info with raw content
|
||||
* Right-click or tap menu button to access event menu
|
||||
*/
|
||||
function DefaultKindRenderer({ event }: BaseEventProps) {
|
||||
const DefaultKindRenderer = memo(function DefaultKindRenderer({
|
||||
event,
|
||||
}: BaseEventProps) {
|
||||
return (
|
||||
<BaseEventContainer event={event}>
|
||||
<div className="text-sm text-muted-foreground">
|
||||
@@ -320,13 +323,13 @@ function DefaultKindRenderer({ event }: BaseEventProps) {
|
||||
</div>
|
||||
</BaseEventContainer>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Main KindRenderer component
|
||||
* Automatically selects the appropriate renderer based on event kind
|
||||
*/
|
||||
export function KindRenderer({
|
||||
export const KindRenderer = memo(function KindRenderer({
|
||||
event,
|
||||
depth = 0,
|
||||
}: {
|
||||
@@ -335,7 +338,7 @@ export function KindRenderer({
|
||||
}) {
|
||||
const Renderer = kindRenderers[event.kind] || DefaultKindRenderer;
|
||||
return <Renderer event={event} depth={depth} />;
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Registry of kind-specific detail renderers (for detail views)
|
||||
@@ -426,19 +429,27 @@ const detailRenderers: Record<
|
||||
* Default detail renderer for kinds without custom detail implementations
|
||||
* Falls back to the feed renderer
|
||||
*/
|
||||
function DefaultDetailRenderer({ event }: { event: NostrEvent }) {
|
||||
const DefaultDetailRenderer = memo(function DefaultDetailRenderer({
|
||||
event,
|
||||
}: {
|
||||
event: NostrEvent;
|
||||
}) {
|
||||
return <KindRenderer event={event} depth={0} />;
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Main DetailKindRenderer component
|
||||
* Automatically selects the appropriate detail renderer based on event kind
|
||||
* Falls back to feed renderer if no detail renderer exists
|
||||
*/
|
||||
export function DetailKindRenderer({ event }: { event: NostrEvent }) {
|
||||
export const DetailKindRenderer = memo(function DetailKindRenderer({
|
||||
event,
|
||||
}: {
|
||||
event: NostrEvent;
|
||||
}) {
|
||||
const Renderer = detailRenderers[event.kind] || DefaultDetailRenderer;
|
||||
return <Renderer event={event} />;
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Export kind renderers registry for dynamic kind detection
|
||||
|
||||
Reference in New Issue
Block a user