import { useState, useRef, useEffect } from 'react'; import Avatar from './Avatar.jsx'; import UserProfilePopup from './UserProfilePopup.jsx'; import ImageLightbox from './ImageLightbox.jsx'; import Picker from '@emoji-mart/react'; import data from '@emoji-mart/data'; import { parseTS } from '../utils/api.js'; import './Message.css'; const QUICK_EMOJIS = ['๐', 'โค๏ธ', '๐', '๐ฎ', '๐ข', '๐']; function formatMsgContent(content) { if (!content) return ''; // First handle @mentions let html = content.replace(/@\[([^\]]+)\]/g, (_, name) => `@${name}`); // Then linkify bare URLs (not already inside a tag) html = html.replace(/(https?:\/\/[^\s<>"]+)/g, (url) => { // Trim trailing punctuation that's unlikely to be part of the URL const trimmed = url.replace(/[.,!?;:)\]]+$/, ''); const trailing = url.slice(trimmed.length); return `${trimmed}${trailing}`; }); return html; } // Detect emoji-only messages for large rendering function isEmojiOnly(str) { if (!str || str.length > 12) return false; const emojiRegex = /^(\p{Emoji_Presentation}|\p{Extended_Pictographic}|\uFE0F|\u200D|[\u{1F1E0}-\u{1F1FF}])+$/u; return emojiRegex.test(str.trim()); } export default function Message({ message: msg, prevMessage, currentUser, onReply, onDelete, onReact, onDirectMessage, isDirect, onlineUserIds = new Set() }) { const [showActions, setShowActions] = useState(false); const [showOptionsMenu, setShowOptionsMenu] = useState(false); const longPressTimer = useRef(null); const optionsMenuRef = useRef(null); const [showEmojiPicker, setShowEmojiPicker] = useState(false); const wrapperRef = useRef(null); const pickerRef = useRef(null); const avatarRef = useRef(null); const [showProfile, setShowProfile] = useState(false); const [lightboxSrc, setLightboxSrc] = useState(null); const [pickerOpensDown, setPickerOpensDown] = useState(false); const isOwn = msg.user_id === currentUser.id; const isDeleted = !!msg.is_deleted; const isSystem = msg.type === 'system'; // These must be computed before any early returns that reference them const showDateSep = !prevMessage || parseTS(msg.created_at).toDateString() !== parseTS(prevMessage.created_at).toDateString(); const prevSameUser = !showDateSep && prevMessage && prevMessage.user_id === msg.user_id && prevMessage.type !== 'system' && msg.type !== 'system'; const canDelete = !msg.is_deleted && ( msg.user_id === currentUser.id || currentUser.role === 'admin' || msg.group_owner_id === currentUser.id ); // Close emoji picker when clicking outside useEffect(() => { if (!showEmojiPicker) return; const handler = (e) => { if (pickerRef.current && !pickerRef.current.contains(e.target)) { setShowEmojiPicker(false); } }; document.addEventListener('mousedown', handler); return () => document.removeEventListener('mousedown', handler); }, [showEmojiPicker]); // Close options menu on outside click useEffect(() => { if (!showOptionsMenu) return; const close = (e) => { if (optionsMenuRef.current && !optionsMenuRef.current.contains(e.target)) { setShowOptionsMenu(false); } }; document.addEventListener('mousedown', close); document.addEventListener('touchstart', close); return () => { document.removeEventListener('mousedown', close); document.removeEventListener('touchstart', close); }; }, [showOptionsMenu]); const handleReact = (emoji) => { onReact(msg.id, emoji); setShowEmojiPicker(false); }; const handleCopy = () => { if (!msg.content) return; navigator.clipboard.writeText(msg.content).catch(() => {}); }; const handleTogglePicker = () => { if (!showEmojiPicker && wrapperRef.current) { const rect = wrapperRef.current.getBoundingClientRect(); setPickerOpensDown(rect.top < 400); } setShowEmojiPicker(p => !p); }; // Long press for mobile action menu (DMs only) const handleTouchStart = () => { if (!isDirect) return; longPressTimer.current = setTimeout(() => setShowOptionsMenu(true), 500); }; const handleTouchEnd = () => { if (longPressTimer.current) clearTimeout(longPressTimer.current); }; // Deleted messages are filtered out by ChatWindow, but guard here too if (isDeleted) return null; // System messages render as a simple centred notice if (isSystem) { return ( <> {showDateSep && (
{msg.content}
: )} {msg.link_preview &&