'use client'; import React, { useRef, useEffect, useState, useCallback, useMemo } from 'react'; import { Smile, Paperclip, Mic, Send, X, StopCircle, Pause, Play, LayoutTemplate, BookOpen, Plus, Sticker, } from 'lucide-react'; import { motion, AnimatePresence } from 'framer-motion'; // SatΓ©lite: emoji picker simples (evita a dep @emoji-mart, incompatΓ­vel com react 19). const EMOJI_SET = ['πŸ˜€','😁','πŸ˜‚','🀣','😊','😍','😘','😎','πŸ€”','😐','😴','😭','😑','πŸ‘','πŸ‘Ž','πŸ™','πŸ‘','πŸ™Œ','πŸ’ͺ','πŸ”₯','πŸŽ‰','❀️','πŸ’”','βœ…','❌','⚠️','πŸ“Œ','πŸ“Ž','πŸ“ž','πŸ’¬','⏰','πŸ₯³','πŸ˜‡','🀝','πŸ‘€','πŸ’―']; import { groupApi, type GroupParticipant, type MessageTemplate, type TemplateType } from '../services/chatApiService'; import RichMessageComposer from './RichMessageComposer'; import TemplateLibrary from './TemplateLibrary'; import ScheduleMessageModal from './ScheduleMessageModal'; import FilePreviewModal from './FilePreviewModal'; import StickerPanel from './StickerPanel'; import { mediaApi } from '../services/chatApiService'; interface Message { id: number; chat_id: number; message_id: string; text: string; direction: 'in' | 'out'; } interface InputBarProps { newMessage: string; onNewMessageChange: (val: string) => void; onSend: (e?: React.FormEvent) => void; onPresence: () => void; onSendMedia: (file: File, caption?: string) => Promise; onSendAudio: (blob: Blob) => Promise; replyingTo: Message | null; onCancelReply: () => void; isMobile: boolean; selectedChat: any; instanceId?: string | null; mentions?: string[]; onMentionsChange?: (mentions: string[]) => void; } const InputBar: React.FC = ({ newMessage, onNewMessageChange, onSend, onPresence, onSendMedia, onSendAudio, replyingTo, onCancelReply, isMobile, selectedChat, instanceId, mentions = [], onMentionsChange, }) => { const textareaRef = useRef(null); const fileInputRef = useRef(null); const [showEmojiPicker, setShowEmojiPicker] = useState(false); const [showStickerPanel, setShowStickerPanel] = useState(false); const [showAttachMenu, setShowAttachMenu] = useState(false); const stickerPanelRef = useRef(null); const [previewFile, setPreviewFile] = useState(null); const attachMenuRef = useRef(null); const [showRichComposer, setShowRichComposer] = useState(false); const [showTemplateLibrary, setShowTemplateLibrary] = useState(false); const [richInitialPayload, setRichInitialPayload] = useState | undefined>(); const [richInitialType, setRichInitialType] = useState(); const [showScheduleModal, setShowScheduleModal] = useState(false); const [schedulingPayload, setSchedulingPayload] = useState>({}); const [schedulingType, setSchedulingType] = useState('TEXT'); const emojiPickerRef = useRef(null); // ── Mention autocomplete state ────────────────────────────────────── const [mentionQuery, setMentionQuery] = useState(null); const [mentionIndex, setMentionIndex] = useState(0); const [participants, setParticipants] = useState([]); const participantsCache = useRef>({}); const mentionDropdownRef = useRef(null); const isGroupChat = selectedChat?.remote_jid?.endsWith('@g.us') || selectedChat?.jid?.endsWith('@g.us'); const groupJid = selectedChat?.remote_jid || selectedChat?.jid; // Fetch group participants when group chat is selected useEffect(() => { if (!isGroupChat || !instanceId || !groupJid) { setParticipants([]); return; } if (participantsCache.current[groupJid]) { setParticipants(participantsCache.current[groupJid]); return; } groupApi.getParticipants(instanceId, groupJid) .then((data) => { participantsCache.current[groupJid] = data.participants; setParticipants(data.participants); }) .catch(() => setParticipants([])); }, [isGroupChat, instanceId, groupJid]); // Filter participants by mention query const filteredParticipants = useMemo(() => { if (mentionQuery === null || !isGroupChat) return []; const q = mentionQuery.toLowerCase(); // Only show participants that have a displayable name or phone return participants.filter((p) => { const label = p.name || p.phone || ''; if (!label) return false; return q === '' || label.toLowerCase().includes(q); }).slice(0, 8); }, [mentionQuery, participants, isGroupChat]); // Detect @ trigger in text const detectMentionQuery = useCallback((text: string, cursorPos: number) => { if (!isGroupChat) { setMentionQuery(null); return; } const before = text.slice(0, cursorPos); const atMatch = before.match(/@([^\s@]*)$/); if (atMatch) { setMentionQuery(atMatch[1]); setMentionIndex(0); } else { setMentionQuery(null); } }, [isGroupChat]); const selectMention = useCallback((participant: GroupParticipant) => { const textarea = textareaRef.current; if (!textarea) return; const cursorPos = textarea.selectionStart; const before = newMessage.slice(0, cursorPos); const after = newMessage.slice(cursorPos); const atIdx = before.lastIndexOf('@'); if (atIdx === -1) return; const displayName = participant.name || participant.phone; const replacement = `@${displayName} `; const newText = before.slice(0, atIdx) + replacement + after; onNewMessageChange(newText); setMentionQuery(null); // Add JID to mentions list if (onMentionsChange && !mentions.includes(participant.jid)) { onMentionsChange([...mentions, participant.jid]); } // Restore cursor requestAnimationFrame(() => { if (textareaRef.current) { const newCursorPos = atIdx + replacement.length; textareaRef.current.selectionStart = newCursorPos; textareaRef.current.selectionEnd = newCursorPos; textareaRef.current.focus(); } }); }, [newMessage, onNewMessageChange, mentions, onMentionsChange]); // Audio recording states const [isRecording, setIsRecording] = useState(false); const [isPaused, setIsPaused] = useState(false); const [recordingDuration, setRecordingDuration] = useState(0); const [micError, setMicError] = useState(null); const mediaRecorderRef = useRef(null); const audioChunksRef = useRef([]); const timerRef = useRef(null); // navigator.mediaDevices sΓ³ existe em contextos seguros (HTTPS ou localhost) const micAvailable = typeof window !== 'undefined' && !!window.isSecureContext && !!navigator.mediaDevices?.getUserMedia; // Auto-expand textarea useEffect(() => { if (textareaRef.current) { textareaRef.current.style.height = 'auto'; textareaRef.current.style.height = `${Math.min(textareaRef.current.scrollHeight, 150)}px`; } }, [newMessage]); // Close emoji picker, sticker panel and attach menu on click outside useEffect(() => { const handleClickOutside = (event: MouseEvent) => { if (emojiPickerRef.current && !emojiPickerRef.current.contains(event.target as Node)) { setShowEmojiPicker(false); } if (stickerPanelRef.current && !stickerPanelRef.current.contains(event.target as Node)) { setShowStickerPanel(false); } if (attachMenuRef.current && !attachMenuRef.current.contains(event.target as Node)) { setShowAttachMenu(false); } }; document.addEventListener('mousedown', handleClickOutside); return () => document.removeEventListener('mousedown', handleClickOutside); }, []); const handleSendSticker = useCallback(async (blob: Blob) => { if (!instanceId || !selectedChat?.remote_jid) return; setShowStickerPanel(false); await mediaApi.sendSticker(instanceId, selectedChat.remote_jid, blob); }, [instanceId, selectedChat]); const handleKeyDown = (e: React.KeyboardEvent) => { // Mention dropdown navigation if (mentionQuery !== null && filteredParticipants.length > 0) { if (e.key === 'ArrowDown') { e.preventDefault(); setMentionIndex((prev) => (prev + 1) % filteredParticipants.length); return; } if (e.key === 'ArrowUp') { e.preventDefault(); setMentionIndex((prev) => (prev - 1 + filteredParticipants.length) % filteredParticipants.length); return; } if (e.key === 'Enter' || e.key === 'Tab') { e.preventDefault(); selectMention(filteredParticipants[mentionIndex]); return; } if (e.key === 'Escape') { e.preventDefault(); setMentionQuery(null); return; } } if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); onSend(); } }; const onEmojiSelect = (emoji: any) => { onNewMessageChange(newMessage + (emoji.native ?? '')); }; const handleFileChange = (e: React.ChangeEvent) => { const file = e.target.files?.[0]; if (file) { setPreviewFile(file); if (fileInputRef.current) fileInputRef.current.value = ''; } }; const handlePreviewConfirm = async (file: File, caption: string) => { await onSendMedia(file, caption); setPreviewFile(null); }; const handlePreviewCancel = () => setPreviewFile(null); const startRecording = async () => { if (!micAvailable) { setMicError('Áudio requer HTTPS. Acesse via https:// para gravar.'); setTimeout(() => setMicError(null), 4000); return; } setMicError(null); try { const stream = await navigator.mediaDevices.getUserMedia({ audio: true }); const mediaRecorder = new MediaRecorder(stream); mediaRecorderRef.current = mediaRecorder; audioChunksRef.current = []; mediaRecorder.ondataavailable = (event) => { if (event.data.size > 0) { audioChunksRef.current.push(event.data); } }; mediaRecorder.onstop = async () => { const mimeType = mediaRecorder.mimeType || 'audio/webm' const audioBlob = new Blob(audioChunksRef.current, { type: mimeType }); await onSendAudio(audioBlob); stream.getTracks().forEach(track => track.stop()); }; mediaRecorder.start(); setIsRecording(true); setIsPaused(false); setRecordingDuration(0); timerRef.current = setInterval(() => { setRecordingDuration(prev => prev + 1); }, 1000); } catch (err: any) { const denied = err?.name === 'NotAllowedError' || err?.name === 'PermissionDeniedError'; const msg = denied ? 'PermissΓ£o de microfone negada.' : 'NΓ£o foi possΓ­vel acessar o microfone.'; setMicError(msg); setTimeout(() => setMicError(null), 4000); } }; const togglePause = () => { const mr = mediaRecorderRef.current if (!mr) return if (isPaused) { mr.resume() setIsPaused(false) timerRef.current = setInterval(() => setRecordingDuration(prev => prev + 1), 1000) } else { mr.pause() setIsPaused(true) if (timerRef.current) clearInterval(timerRef.current) } } const stopRecording = () => { if (mediaRecorderRef.current && isRecording) { // Se estava pausado, retoma antes de parar para capturar o ΓΊltimo chunk if (isPaused && mediaRecorderRef.current.state === 'paused') { mediaRecorderRef.current.resume() } mediaRecorderRef.current.stop(); setIsRecording(false); setIsPaused(false); if (timerRef.current) clearInterval(timerRef.current); } }; const handleTemplateSelect = (template: MessageTemplate) => { setRichInitialPayload(template.payload) setRichInitialType(template.type) setShowRichComposer(true) } const handleScheduleFromComposer = (type: TemplateType, payload: Record) => { setSchedulingType(type) setSchedulingPayload(payload) setShowRichComposer(false) setShowScheduleModal(true) } const formatDuration = (seconds: number) => { const mins = Math.floor(seconds / 60); const secs = seconds % 60; return `${mins}:${secs.toString().padStart(2, '0')}`; }; return ( <>
{/* Hidden File Input */} {/* Emoji Picker Popover */} {showEmojiPicker && (
{EMOJI_SET.map((e) => ( ))}
)}
{/* Sticker Panel Popover */} {showStickerPanel && instanceId && ( )} {/* Reply Preview */} {replyingTo && (
{replyingTo.direction === 'out' ? 'Respondendo a vocΓͺ' : 'Respondendo ao contato'}
{replyingTo.text || 'Anexo/MΓ­dia'}
)}
{/* Mic error banner */} {micError && ( {micError} )} {/* Main Input Bar */}
{!isRecording ? ( <>
{/* Grupo flutuante: + expande Anexar / Templates / Mensagem Rica */}
{showAttachMenu && ( {/* Anexar arquivo */} {/* Biblioteca de templates */} {/* Mensagem rica */} )}
{/* Emoji β€” mantΓ©m no lugar */} {/* Figurinhas */} {instanceId && ( )}
{/* Mention Autocomplete Dropdown */} {mentionQuery !== null && filteredParticipants.length > 0 && ( {filteredParticipants.map((p, i) => ( ))} )}