"use client"; import { useState, useRef, useEffect, useCallback } from "react"; import type { GraphNode } from "@/lib/types"; import { PREFIX_COLORS } from "@/lib/types"; import type { BeadsComment } from "@/hooks/useBeadsComments"; import { useIsMobile } from "@/hooks/useIsMobile"; interface CommentTooltipProps { node: GraphNode; x: number; y: number; onClose: () => void; onSubmit: (text: string) => Promise; isAuthenticated: boolean; existingComments?: BeadsComment[]; } export function CommentTooltip({ node, x, y, onClose, onSubmit, isAuthenticated, existingComments, }: CommentTooltipProps) { const [text, setText] = useState(""); const [sending, setSending] = useState(false); const tooltipRef = useRef(null); const textareaRef = useRef(null); const [pos, setPos] = useState({ x: 0, y: 0 }); const [visible, setVisible] = useState(false); const isMobile = useIsMobile(); // Position the tooltip after first render (measure dimensions) useEffect(() => { if (!tooltipRef.current) return; const tt = tooltipRef.current.getBoundingClientRect(); const vw = window.innerWidth; const vh = window.innerHeight; let nx = x + 14; let ny = y - tt.height - 14; // Clamp right if (nx + tt.width > vw - 16) nx = vw - tt.width - 16; // Clamp left if (nx < 16) nx = 16; // If overflows top, flip below cursor if (ny < 16) ny = y + 28; // Clamp bottom if (ny + tt.height > vh - 16) ny = vh - tt.height - 16; setPos({ x: nx, y: ny }); setVisible(true); }, [x, y]); // Auto-focus textarea on mount useEffect(() => { if (isAuthenticated) { // Small delay to let position settle const timer = setTimeout(() => textareaRef.current?.focus(), 100); return () => clearTimeout(timer); } }, [isAuthenticated]); // Close on Escape useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { if (e.key === "Escape") onClose(); }; window.addEventListener("keydown", handleKeyDown); return () => window.removeEventListener("keydown", handleKeyDown); }, [onClose]); // Close on click outside useEffect(() => { const handleMouseDown = (e: MouseEvent) => { if ( tooltipRef.current && !tooltipRef.current.contains(e.target as Node) ) { onClose(); } }; // Use a small delay so the right-click that opened the tooltip doesn't // immediately close it const timer = setTimeout(() => { window.addEventListener("mousedown", handleMouseDown); }, 50); return () => { clearTimeout(timer); window.removeEventListener("mousedown", handleMouseDown); }; }, [onClose]); const handleSubmit = useCallback(async () => { if (!text.trim() || sending) return; setSending(true); try { await onSubmit(text.trim()); setText(""); } catch (err) { console.error("Failed to post comment:", err); } finally { setSending(false); } }, [text, sending, onSubmit]); // Handle Ctrl+Enter to submit const handleKeyDown = useCallback( (e: React.KeyboardEvent) => { if ((e.metaKey || e.ctrlKey) && e.key === "Enter") { e.preventDefault(); handleSubmit(); } }, [handleSubmit] ); const prefixColor = PREFIX_COLORS[node.prefix] || "#a1a1aa"; const commentCount = existingComments?.length || 0; // Shared form content used in both mobile and desktop layouts const formContent = ( <> {/* Node info */}
{node.id}

{node.title}

{/* Existing comments preview */} {commentCount > 0 && existingComments && (
{commentCount} comment{commentCount !== 1 ? "s" : ""}
{/* Show most recent 2 comments */}
{existingComments.slice(0, 2).map((comment) => (
{comment.avatar ? ( ) : (
{(comment.handle || comment.did) .charAt(0) .toUpperCase()}
)}
))} {commentCount > 2 && (

+{commentCount - 2} more — see detail panel

)}
)} {/* Compose area */} {isAuthenticated ? (