import React, { useState, useRef, useEffect } from 'react'; import { parseMarkdown } from '../utils/markdown'; const CODE_EXTENSIONS = ['.js', '.jsx', '.ts', '.tsx', '.py', '.html', '.css', '.json', '.md', '.txt', '.sh', '.yml', '.yaml', '.xml', '.sql', '.rb', '.go', '.rs', '.c', '.cpp', '.h', '.java']; function formatFileSize(bytes) { if (!bytes) return '0 B'; const units = ['B', 'KB', 'MB', 'GB']; let i = 0; let size = bytes; while (size >= 1024 && i < units.length - 1) { size /= 1024; i++; } return `${size.toFixed(i === 0 ? 0 : 1)} ${units[i]}`; } function formatTime(dateStr) { const d = new Date(dateStr); const now = new Date(); const isToday = d.toDateString() === now.toDateString(); const yesterday = new Date(now); yesterday.setDate(yesterday.getDate() - 1); const isYesterday = d.toDateString() === yesterday.toDateString(); const time = d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); if (isToday) return `Today at ${time}`; if (isYesterday) return `Yesterday at ${time}`; return `${d.toLocaleDateString()} ${time}`; } function isCodeFile(fileName) { if (!fileName) return false; return CODE_EXTENSIONS.some(ext => fileName.toLowerCase().endsWith(ext)); } function FilePreview({ message }) { const { file_url, file_name, file_size, file_type } = message; const [codeContent, setCodeContent] = useState(null); const [imageOpen, setImageOpen] = useState(false); useEffect(() => { if (isCodeFile(file_name)) { fetch(file_url).then(r => r.text()).then(text => { const lines = text.split('\n').slice(0, 20); setCodeContent(lines.join('\n') + (text.split('\n').length > 20 ? '\n...' : '')); }).catch(() => {}); } }, [file_url, file_name]); // Image preview if (file_type?.startsWith('image/')) { return ( <> {file_name} setImageOpen(true)} /> {imageOpen && (
setImageOpen(false)}> {file_name}
)} ); } // Code file preview if (isCodeFile(file_name) && codeContent !== null) { return (
{file_name} Download
          {codeContent}
        
); } // Generic file card return (
{file_name} {formatFileSize(file_size)}
); } function ReactionBar({ reactions, userId, token, messageId }) { if (!reactions || reactions.length === 0) return null; const toggleReaction = async (emoji) => { const reaction = reactions.find(r => r.emoji === emoji); const users = reaction?.users ? reaction.users.split(',') : []; const hasReacted = users.includes(String(userId)); try { if (hasReacted) { await fetch(`/api/messages/${messageId}/reactions/${encodeURIComponent(emoji)}`, { method: 'DELETE', headers: { 'Authorization': `Bearer ${token}` }, }); } else { await fetch(`/api/messages/${messageId}/reactions`, { method: 'POST', headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ emoji }), }); } } catch (err) { console.error(err); } }; return (
{reactions.map((r, i) => { const users = r.users ? r.users.split(',') : []; const count = users.length; const hasReacted = users.includes(String(userId)); return ( ); })}
); } const EMOJI_QUICK = ['๐Ÿ‘', 'โค๏ธ', '๐Ÿ˜‚', '๐ŸŽ‰', '๐Ÿ”ฅ', '๐Ÿ‘€']; export default function Message({ message, user, token, onReply, onEdit, onDelete }) { const [hovered, setHovered] = useState(false); const [editing, setEditing] = useState(false); const [editText, setEditText] = useState(message.content || ''); const [showEmojiPicker, setShowEmojiPicker] = useState(false); const editRef = useRef(null); const isOwn = user && String(message.user_id) === String(user.id); useEffect(() => { if (editing && editRef.current) { editRef.current.focus(); editRef.current.selectionStart = editRef.current.value.length; } }, [editing]); const saveEdit = () => { const trimmed = editText.trim(); if (trimmed && trimmed !== message.content) { onEdit(message.id, trimmed); } setEditing(false); }; const handleEditKeyDown = (e) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); saveEdit(); } if (e.key === 'Escape') { setEditing(false); setEditText(message.content || ''); } }; const addReaction = async (emoji) => { setShowEmojiPicker(false); try { await fetch(`/api/messages/${message.id}/reactions`, { method: 'POST', headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ emoji }), }); } catch (err) { console.error(err); } }; return (
setHovered(true)} onMouseLeave={() => { setHovered(false); setShowEmojiPicker(false); }} > {/* Avatar */}
{message.avatar ? ( ) : (
{(message.username || '?')[0].toUpperCase()}
)}
{/* Body */}
{/* Reply reference */} {message.reply_to && (
replied to a message
)} {/* Header */}
{message.username || 'Unknown'} {formatTime(message.created_at)} {message.edited ? (edited) : null}
{/* Content */} {editing ? (