// File Transfer Component for Chat Interface - Fixed Version const FileTransferComponent = ({ webrtcManager, isConnected }) => { const [dragOver, setDragOver] = React.useState(false); const [transfers, setTransfers] = React.useState({ sending: [], receiving: [] }); const [readyFiles, setReadyFiles] = React.useState([]); // файлы, готовые к скачиванию const fileInputRef = React.useRef(null); // Update transfers periodically React.useEffect(() => { if (!isConnected || !webrtcManager) return; const updateTransfers = () => { const currentTransfers = webrtcManager.getFileTransfers(); setTransfers(currentTransfers); }; const interval = setInterval(updateTransfers, 500); return () => clearInterval(interval); }, [isConnected, webrtcManager]); // Setup file transfer callbacks - ИСПРАВЛЕНИЕ: НЕ отправляем промежуточные сообщения в чат React.useEffect(() => { if (!webrtcManager) return; webrtcManager.setFileTransferCallbacks( // Progress callback - ТОЛЬКО обновляем UI, НЕ отправляем в чат (progress) => { // Обновляем только локальное состояние const currentTransfers = webrtcManager.getFileTransfers(); setTransfers(currentTransfers); // НЕ отправляем сообщения в чат! }, // File received callback - добавляем кнопку скачивания в UI (fileData) => { // Добавляем в список готовых к скачиванию setReadyFiles(prev => { // избегаем дублей по fileId if (prev.some(f => f.fileId === fileData.fileId)) return prev; return [...prev, { fileId: fileData.fileId, fileName: fileData.fileName, fileSize: fileData.fileSize, mimeType: fileData.mimeType, getBlob: fileData.getBlob, getObjectURL: fileData.getObjectURL, revokeObjectURL: fileData.revokeObjectURL }]; }); // Обновляем список активных передач const currentTransfers = webrtcManager.getFileTransfers(); setTransfers(currentTransfers); }, // Error callback (error) => { const currentTransfers = webrtcManager.getFileTransfers(); setTransfers(currentTransfers); // ИСПРАВЛЕНИЕ: НЕ дублируем сообщения об ошибках // Уведомления об ошибках уже отправляются в WebRTC менеджере } ); }, [webrtcManager]); const handleFileSelect = async (files) => { if (!isConnected || !webrtcManager) { alert('Соединение не установлено. Сначала установите соединение.'); return; } // Дополнительная проверка состояния соединения if (!webrtcManager.isConnected() || !webrtcManager.isVerified) { alert('Соединение не готово для передачи файлов. Дождитесь завершения установки соединения.'); return; } for (const file of files) { try { // КРИТИЧЕСКОЕ ИСПРАВЛЕНИЕ: Валидация файла перед отправкой const validation = webrtcManager.validateFile(file); if (!validation.isValid) { const errorMessage = validation.errors.join('. '); alert(`Файл ${file.name} не может быть отправлен: ${errorMessage}`); continue; } await webrtcManager.sendFile(file); } catch (error) { // Более мягкая обработка ошибок - не закрываем сессию // Показываем пользователю ошибку, но не закрываем соединение if (error.message.includes('Connection not ready')) { alert(`Файл ${file.name} не может быть отправлен сейчас. Проверьте соединение и попробуйте снова.`); } else if (error.message.includes('File too large') || error.message.includes('exceeds maximum')) { alert(`Файл ${file.name} слишком большой: ${error.message}`); } else if (error.message.includes('Maximum concurrent transfers')) { alert(`Достигнут лимит одновременных передач. Дождитесь завершения текущих передач.`); } else if (error.message.includes('File type not allowed')) { alert(`Тип файла ${file.name} не поддерживается: ${error.message}`); } else { alert(`Ошибка отправки файла ${file.name}: ${error.message}`); } } } }; const handleDrop = (e) => { e.preventDefault(); setDragOver(false); const files = Array.from(e.dataTransfer.files); handleFileSelect(files); }; const handleDragOver = (e) => { e.preventDefault(); setDragOver(true); }; const handleDragLeave = (e) => { e.preventDefault(); setDragOver(false); }; const handleFileInputChange = (e) => { const files = Array.from(e.target.files); handleFileSelect(files); e.target.value = ''; // Reset input }; const formatFileSize = (bytes) => { if (bytes === 0) return '0 B'; const k = 1024; const sizes = ['B', 'KB', 'MB', 'GB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; }; const getStatusIcon = (status) => { switch (status) { case 'metadata_sent': case 'preparing': return 'fas fa-cog fa-spin'; case 'transmitting': case 'receiving': return 'fas fa-exchange-alt fa-pulse'; case 'assembling': return 'fas fa-puzzle-piece fa-pulse'; case 'completed': return 'fas fa-check text-green-400'; case 'failed': return 'fas fa-times text-red-400'; default: return 'fas fa-circle'; } }; const getStatusText = (status) => { switch (status) { case 'metadata_sent': return 'Подготовка...'; case 'transmitting': return 'Отправка...'; case 'receiving': return 'Получение...'; case 'assembling': return 'Сборка файла...'; case 'completed': return 'Завершено'; case 'failed': return 'Ошибка'; default: return status; } }; if (!isConnected) { return React.createElement('div', { className: "p-4 text-center text-muted" }, 'Передача файлов доступна только при установленном соединении'); } // Проверяем дополнительное состояние соединения const isConnectionReady = webrtcManager && webrtcManager.isConnected() && webrtcManager.isVerified; if (!isConnectionReady) { return React.createElement('div', { className: "p-4 text-center text-yellow-600" }, [ React.createElement('i', { key: 'icon', className: 'fas fa-exclamation-triangle mr-2' }), 'Соединение устанавливается... Передача файлов будет доступна после завершения установки.' ]); } return React.createElement('div', { className: "file-transfer-component" }, [ // File Drop Zone React.createElement('div', { key: 'drop-zone', className: `file-drop-zone ${dragOver ? 'drag-over' : ''}`, onDrop: handleDrop, onDragOver: handleDragOver, onDragLeave: handleDragLeave, onClick: () => fileInputRef.current?.click() }, [ React.createElement('div', { key: 'drop-content', className: "drop-content" }, [ React.createElement('i', { key: 'icon', className: 'fas fa-cloud-upload-alt text-2xl mb-2 text-blue-400' }), React.createElement('p', { key: 'text', className: "text-primary font-medium" }, 'Drag files here or click to select'), React.createElement('p', { key: 'subtext', className: "text-muted text-sm" }, 'Maximum size: 100 MB per file') ]) ]), // Hidden file input React.createElement('input', { key: 'file-input', ref: fileInputRef, type: 'file', multiple: true, className: 'hidden', onChange: handleFileInputChange }), // Active Transfers (transfers.sending.length > 0 || transfers.receiving.length > 0) && React.createElement('div', { key: 'transfers', className: "active-transfers mt-4" }, [ React.createElement('h4', { key: 'title', className: "text-primary font-medium mb-3 flex items-center" }, [ React.createElement('i', { key: 'icon', className: 'fas fa-exchange-alt mr-2' }), 'Передача файлов' ]), // Sending files ...transfers.sending.map(transfer => React.createElement('div', { key: `send-${transfer.fileId}`, className: "transfer-item bg-blue-500/10 border border-blue-500/20 rounded-lg p-3 mb-2" }, [ React.createElement('div', { key: 'header', className: "flex items-center justify-between mb-2" }, [ React.createElement('div', { key: 'info', className: "flex items-center" }, [ React.createElement('i', { key: 'icon', className: 'fas fa-upload text-blue-400 mr-2' }), React.createElement('span', { key: 'name', className: "text-primary font-medium text-sm" }, transfer.fileName), React.createElement('span', { key: 'size', className: "text-muted text-xs ml-2" }, formatFileSize(transfer.fileSize)) ]), React.createElement('button', { key: 'cancel', onClick: () => webrtcManager.cancelFileTransfer(transfer.fileId), className: "text-red-400 hover:text-red-300 text-xs" }, [ React.createElement('i', { className: 'fas fa-times' }) ]) ]), React.createElement('div', { key: 'progress', className: "progress-bar" }, [ React.createElement('div', { key: 'fill', className: "progress-fill bg-blue-400", style: { width: `${transfer.progress}%` } }), React.createElement('div', { key: 'text', className: "progress-text text-xs flex items-center justify-between" }, [ React.createElement('span', { key: 'status', className: "flex items-center" }, [ React.createElement('i', { key: 'icon', className: `${getStatusIcon(transfer.status)} mr-1` }), getStatusText(transfer.status) ]), React.createElement('span', { key: 'percent' }, `${transfer.progress.toFixed(1)}%`) ]) ]) ]) ), // Receiving files ...transfers.receiving.map(transfer => React.createElement('div', { key: `recv-${transfer.fileId}`, className: "transfer-item bg-green-500/10 border border-green-500/20 rounded-lg p-3 mb-2" }, [ React.createElement('div', { key: 'header', className: "flex items-center justify-between mb-2" }, [ React.createElement('div', { key: 'info', className: "flex items-center" }, [ React.createElement('i', { key: 'icon', className: 'fas fa-download text-green-400 mr-2' }), React.createElement('span', { key: 'name', className: "text-primary font-medium text-sm" }, transfer.fileName), React.createElement('span', { key: 'size', className: "text-muted text-xs ml-2" }, formatFileSize(transfer.fileSize)) ]), React.createElement('div', { key: 'actions', className: 'flex items-center space-x-2' }, [ (() => { const rf = readyFiles.find(f => f.fileId === transfer.fileId); if (!rf || transfer.status !== 'completed') return null; return React.createElement('button', { key: 'download', className: 'text-green-400 hover:text-green-300 text-xs flex items-center', onClick: async () => { try { const url = await rf.getObjectURL(); const a = document.createElement('a'); a.href = url; a.download = rf.fileName || 'file'; a.click(); rf.revokeObjectURL(url); } catch (e) { alert('Failed to start download: ' + e.message); } } }, [ React.createElement('i', { key: 'i', className: 'fas fa-download mr-1' }), 'Download' ]); })(), React.createElement('button', { key: 'cancel', onClick: () => webrtcManager.cancelFileTransfer(transfer.fileId), className: "text-red-400 hover:text-red-300 text-xs" }, [ React.createElement('i', { className: 'fas fa-times' }) ]) ]) ]), React.createElement('div', { key: 'progress', className: "progress-bar" }, [ React.createElement('div', { key: 'fill', className: "progress-fill bg-green-400", style: { width: `${transfer.progress}%` } }), React.createElement('div', { key: 'text', className: "progress-text text-xs flex items-center justify-between" }, [ React.createElement('span', { key: 'status', className: "flex items-center" }, [ React.createElement('i', { key: 'icon', className: `${getStatusIcon(transfer.status)} mr-1` }), getStatusText(transfer.status) ]), React.createElement('span', { key: 'percent' }, `${transfer.progress.toFixed(1)}%`) ]) ]) ]) ) ]) ]); }; // Export window.FileTransferComponent = FileTransferComponent;