import { useState, useRef, useCallback, useEffect } from 'react'; import { api } from '../utils/api.js'; import './MessageInput.css'; const URL_REGEX = /https?:\/\/[^\s]+/g; export default function MessageInput({ group, replyTo, onCancelReply, onSend, onTyping }) { const [text, setText] = useState(''); const [imageFile, setImageFile] = useState(null); const [imagePreview, setImagePreview] = useState(null); const [mentionSearch, setMentionSearch] = useState(''); const [mentionResults, setMentionResults] = useState([]); const [mentionIndex, setMentionIndex] = useState(-1); const [showMention, setShowMention] = useState(false); const [linkPreview, setLinkPreview] = useState(null); const [loadingPreview, setLoadingPreview] = useState(false); const inputRef = useRef(null); const typingTimer = useRef(null); const wasTyping = useRef(false); const mentionStart = useRef(-1); const fileInput = useRef(null); // Handle typing notification const handleTypingChange = (value) => { if (value && !wasTyping.current) { wasTyping.current = true; onTyping(true); } if (typingTimer.current) clearTimeout(typingTimer.current); typingTimer.current = setTimeout(() => { if (wasTyping.current) { wasTyping.current = false; onTyping(false); } }, 2000); }; // Link preview const fetchPreview = useCallback(async (url) => { setLoadingPreview(true); try { const { preview } = await api.getLinkPreview(url); if (preview) setLinkPreview(preview); } catch {} setLoadingPreview(false); }, []); const handleChange = (e) => { const val = e.target.value; setText(val); handleTypingChange(val); // Detect @mention const cur = e.target.selectionStart; const lastAt = val.lastIndexOf('@', cur - 1); if (lastAt !== -1) { const between = val.slice(lastAt + 1, cur); if (!between.includes(' ') && !between.includes('\n')) { mentionStart.current = lastAt; setMentionSearch(between); setShowMention(true); api.searchUsers(between).then(({ users }) => { setMentionResults(users); setMentionIndex(0); }).catch(() => {}); return; } } setShowMention(false); // Link preview const urls = val.match(URL_REGEX); if (urls && urls[0] !== linkPreview?.url) { fetchPreview(urls[0]); } else if (!urls) { setLinkPreview(null); } }; const insertMention = (user) => { const before = text.slice(0, mentionStart.current); const after = text.slice(inputRef.current.selectionStart); const mention = `@[${user.display_name || user.name}](${user.id}) `; setText(before + mention + after); setShowMention(false); setMentionResults([]); inputRef.current.focus(); }; const handleKeyDown = (e) => { if (showMention && mentionResults.length > 0) { if (e.key === 'ArrowDown') { e.preventDefault(); setMentionIndex(i => Math.min(i + 1, mentionResults.length - 1)); return; } if (e.key === 'ArrowUp') { e.preventDefault(); setMentionIndex(i => Math.max(i - 1, 0)); return; } if (e.key === 'Enter' || e.key === 'Tab') { e.preventDefault(); if (mentionIndex >= 0) insertMention(mentionResults[mentionIndex]); return; } if (e.key === 'Escape') { setShowMention(false); return; } } if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); handleSend(); } }; const handleSend = async () => { const trimmed = text.trim(); if (!trimmed && !imageFile) return; const lp = linkPreview; setText(''); setLinkPreview(null); setImageFile(null); setImagePreview(null); wasTyping.current = false; onTyping(false); await onSend({ content: trimmed || null, imageFile, linkPreview: lp }); }; const compressImage = (file) => new Promise((resolve) => { const MAX_PX = 1920; const QUALITY = 0.82; const img = new Image(); const url = URL.createObjectURL(file); img.onload = () => { URL.revokeObjectURL(url); let { width, height } = img; if (width <= MAX_PX && height <= MAX_PX) { // Already small enough — still re-encode to strip EXIF and reduce size } else { const ratio = Math.min(MAX_PX / width, MAX_PX / height); width = Math.round(width * ratio); height = Math.round(height * ratio); } const canvas = document.createElement('canvas'); canvas.width = width; canvas.height = height; canvas.getContext('2d').drawImage(img, 0, 0, width, height); canvas.toBlob(blob => { resolve(new File([blob], file.name.replace(/\.[^.]+$/, '.jpg'), { type: 'image/jpeg' })); }, 'image/jpeg', QUALITY); }; img.src = url; }); const handleImageSelect = async (e) => { const file = e.target.files?.[0]; if (!file) return; const compressed = await compressImage(file); setImageFile(compressed); const reader = new FileReader(); reader.onload = (e) => setImagePreview(e.target.result); reader.readAsDataURL(compressed); }; const displayText = (t) => { // Convert @[name](id) to @name for display return t.replace(/@\[([^\]]+)\]\(\d+\)/g, '@$1'); }; return (