423 lines
19 KiB
JavaScript
423 lines
19 KiB
JavaScript
// 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; |