"use client"; /** * The message context menu: reply, copy, star, forward, delete, report. * * Opened by long-press on touch and right-click on desktop, both arbitrated by * MessageGestures. Positioned at the pointer and clamped to the viewport, so a * bubble near an edge doesn't push the menu off-screen. */ import { useEffect, useLayoutEffect, useRef, useState } from "react"; import { Copy, Flag, Forward, Reply, Star, StarOff, Trash2, } from "lucide-react"; import { ReactionRow } from "@/components/chat/reaction-row"; import { cn } from "@/lib/utils"; export type MenuPoint = { x: number; y: number }; export type MessageMenuActions = { onReply: () => void; onCopy: () => void; onStar: () => void; onForward: () => void; onDeleteForMe: () => void; /** Absent when the caller isn't the sender — only they may erase for all. */ onDeleteForEveryone?: () => void; onReport?: () => void; }; const MARGIN = 8; export function MessageMenu({ point, starred, myReaction, onReact, actions, onClose, }: { point: MenuPoint; starred: boolean; /** The viewer's current emoji on this message, if any. */ myReaction?: string; /** Picking the same emoji again is a toggle — the caller decides. */ onReact: (emoji: string) => void; actions: MessageMenuActions; onClose: () => void; }) { const ref = useRef(null); const [pos, setPos] = useState(point); // Clamp after measuring, before paint, so the menu never flashes off-screen. useLayoutEffect(() => { const el = ref.current; if (!el) return; const { width, height } = el.getBoundingClientRect(); setPos({ x: Math.min(Math.max(MARGIN, point.x), window.innerWidth - width - MARGIN), y: Math.min(Math.max(MARGIN, point.y), window.innerHeight - height - MARGIN), }); }, [point.x, point.y]); useEffect(() => { const onKey = (e: KeyboardEvent) => e.key === "Escape" && onClose(); window.addEventListener("keydown", onKey); return () => window.removeEventListener("keydown", onKey); }, [onClose]); const run = (fn?: () => void) => () => { fn?.(); onClose(); }; return ( <> {/* Catches the next click or scroll anywhere and dismisses. */}
{ e.preventDefault(); onClose(); }} />
{/* Reactions lead: they are the most common thing to want from a long press, and putting them in this menu keeps one gesture responsible for one surface. */} { onReact(emoji); onClose(); }} /> } onClick={run(actions.onReply)}> Reply } onClick={run(actions.onCopy)}> Copy : } onClick={run(actions.onStar)} > {starred ? "Remove star" : "Star"} } onClick={run(actions.onForward)} > Forward
} onClick={run(actions.onDeleteForMe)} > Delete for me {actions.onDeleteForEveryone && ( } onClick={run(actions.onDeleteForEveryone)} > Delete for everyone )} {actions.onReport && ( } onClick={run(actions.onReport)} > Report )}
); } function Item({ icon, onClick, danger = false, children, }: { icon: React.ReactNode; onClick: () => void; danger?: boolean; children: React.ReactNode; }) { return ( ); }