import { useState } from 'react'; import { Edit2, Trash2, Reply, MessageSquare, Pin } from 'lucide-react'; import { IrRenderer } from '@/lib/ir/renderer'; import { extractIrNodes } from '@/lib/ir/parser'; import type { Message } from '@/contexts/room'; import { formatRelativeTime } from '@/contexts/room'; import { Avatar } from './Avatar'; import { useRoom } from '@/contexts/room'; import { MESSAGE_ITEM } from '@/css/channel/styles'; interface Props { msg: Message; isCompact: boolean; isFirst: boolean; emojiPickerMessageId: string | null; onReaction: (msgId: string, emoji: string) => void; onEdit: (msg: Message) => void; onDelete: (msgId: string) => void; onReply: (msg: Message) => void; onOpenThread: (msg: Message) => void; onSetEmojiPicker: (msgId: string | null) => void; onShowEditHistory: (msgId: string) => void; roomId: string; readOnly?: boolean; } const COMMON_EMOJIS = ['👍', '❤️', '😄', '😭', '😡', '🎉', '🚀', '👀']; export function MessageItem({ msg, isCompact, isFirst, emojiPickerMessageId, onReaction, onEdit, onDelete, onReply, onOpenThread, onSetEmojiPicker, onShowEditHistory, readOnly = false, }: Props) { const [isHovered, setIsHovered] = useState(false); const isRevoked = !!msg.revoked; const { addPin, removePin, pinnedMessages } = useRoom(); const isPinned = pinnedMessages.some((p) => p.message === msg.id); const handleTogglePin = () => { if (readOnly) return; const action = isPinned ? removePin(msg.id) : addPin(msg.id); action.catch((err) => console.error('[MessageItem] failed to toggle pin:', err)); }; const isEdited = !!msg.edited_at; const isStreaming = msg.is_streaming === true; const reactions = msg._localReactions ?? []; const showPicker = emojiPickerMessageId === msg.id; return (
setIsHovered(true)} onMouseLeave={() => setIsHovered(false)} >
{/* Avatar + spacer: first message shows avatar, compact uses spacer for alignment */} {isFirst ? ( ) : isCompact ? (
) : null} {/* Gap between avatar/spacer and content */} {isFirst || isCompact ?
: null}
{isFirst && (
{msg.display_name ?? msg.sender_id ?? 'Unknown'} {formatRelativeTime(msg.send_at)} {isEdited && ( )} {isStreaming && ( )}
)}
{isRevoked ? ( This message was deleted ) : ( <> {msg.thinking_content && (
Thinking...
                      {msg.thinking_content}
                    
)} )}
{/* Reactions */} {reactions.length > 0 && (
{reactions.map((reaction) => ( ))}
)} {/* Quick emoji picker */} {showPicker && (
{COMMON_EMOJIS.map((e) => ( ))}
)} {/* Thread badge */} {msg.thread && !readOnly && ( )}
{/* Hover actions — Discord-style: button center aligns with message top edge */} {isHovered && !isRevoked && !readOnly && (
{msg.sender_type !== 'ai' && ( <> )}
)}
); }