import { useRef, useState } from 'react'; import { renderRichText } from '../rich-text'; import { PopoverPortal } from './popover-portal'; import type { Attachment } from '../types'; import type { UiMessage } from '../hooks/use-messages'; const REACTION_EMOJIS = ['👍', '❤️', '😂', '🎉', '👀']; const isImage = (mime: string): boolean => mime.startsWith('image/'); /** Slack-style message time: today shows the clock, then "Yesterday", weekday, else a date. */ function messageTime(iso: string): string { const d = new Date(iso); if (Number.isNaN(+d)) return ''; const now = new Date(); const time = d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); if (d.toDateString() === now.toDateString()) return time; const yesterday = new Date(now); yesterday.setDate(now.getDate() - 1); if (d.toDateString() === yesterday.toDateString()) return `Yesterday ${time}`; if (now.getTime() - d.getTime() < 7 * 86400000) return `${d.toLocaleDateString([], { weekday: 'short' })} ${time}`; return `${d.toLocaleDateString([], { month: 'short', day: 'numeric' })} ${time}`; } function AttachmentView({ att }: { att: Attachment }) { if (isImage(att.mime)) { return ( {att.name} ); } return ( 📎 {att.name} ); } /** One rendered message: bubble (with @mention highlighting), attachment, reactions, seen tick, * and — in the main thread only — a thread/reply affordance. */ export function MessageItem({ message, memberNames, canReact, onReact, seen, replyCount, onOpenThread, }: { message: UiMessage; memberNames: string[]; canReact: boolean; onReact: (messageId: string, emoji: string) => void; seen: boolean; /** Present only in the main thread (not inside the pane). undefined => no thread affordance. */ replyCount?: number; onOpenThread?: (messageId: string) => void; }) { const [pickerOpen, setPickerOpen] = useState(false); const reactBtnRef = useRef(null); const m = message; return (
{m.text ? renderRichText(m.text, memberNames) : null} {m.attachment ? : null}
{canReact ? (
{pickerOpen ? ( setPickerOpen(false)}>
{REACTION_EMOJIS.map((e) => ( ))}
) : null}
) : null} {onOpenThread ? ( ) : null}
{m.reactions && m.reactions.length > 0 ? (
{m.reactions.map((r) => ( ))}
) : null} {replyCount !== undefined && replyCount > 0 && onOpenThread ? ( ) : null} {message.mine && seen ? Seen : null}
); }