release: v4.8.8 file transfer consent fix
CodeQL Analysis / Analyze CodeQL (push) Has been cancelled
Deploy Application / deploy (push) Has been cancelled
Mirror to Codeberg / mirror (push) Has been cancelled
Mirror to PrivacyGuides / mirror (push) Has been cancelled

Complete the mandatory receiver-consent gate that was wired in the
backend but never connected to the UI callback chain:

- Add the missing onIncomingFileRequest (4th) callback to
  setFileTransferCallbacks in app.jsx — its absence caused
  handleFileTransferStart to auto-reject every incoming file.
- Remove independent callback registration from FileTransferComponent;
  the component was overwriting app-level callbacks on mount and
  nulling all four on unmount, silently breaking progress/received/
  error handlers whenever the panel was hidden.
- Lift pendingIncomingFiles state to the root component so consent
  prompts are shown regardless of panel visibility; auto-open the
  panel on incoming request.
- Add getReceivedFileObjectURL / revokeReceivedFileObjectURL on
  EnhancedSecureWebRTCManager for download buttons in the panel.
- Update file-transfer-ui-cleanup regression test to match the new
  single-owner callback architecture.
- All 14 tests pass; clean production build.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
lockbitchat
2026-05-26 22:40:36 -04:00
parent 2468cb495e
commit 498d9a98e9
12 changed files with 200 additions and 209 deletions
+25 -97
View File
@@ -1,12 +1,10 @@
// File Transfer Component for Chat Interface - Fixed Version
const FileTransferComponent = ({ webrtcManager, isConnected }) => {
const FileTransferComponent = ({ webrtcManager, isConnected, pendingIncomingFiles = [], onIncomingDecision }) => {
const [dragOver, setDragOver] = React.useState(false);
const [transfers, setTransfers] = React.useState({ sending: [], receiving: [] });
const [readyFiles, setReadyFiles] = React.useState([]); // файлы, готовые к скачиванию
const [pendingIncomingFiles, setPendingIncomingFiles] = React.useState([]);
const fileInputRef = React.useRef(null);
// Update transfers periodically
// Update transfers periodically via polling — no callback registration needed here
React.useEffect(() => {
if (!isConnected || !webrtcManager) return;
@@ -19,73 +17,12 @@ const FileTransferComponent = ({ webrtcManager, isConnected }) => {
return () => clearInterval(interval);
}, [isConnected, webrtcManager]);
// Clear session-local UI state when the connection ends so reconnect starts clean.
// Clear transfers UI when connection drops
React.useEffect(() => {
if (isConnected) return;
setReadyFiles([]);
setPendingIncomingFiles([]);
setTransfers({ sending: [], receiving: [] });
}, [isConnected]);
// 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 менеджере
},
// Incoming file request callback - user consent is mandatory
(fileRequest) => {
setPendingIncomingFiles(prev => {
if (prev.some(file => file.fileId === fileRequest.fileId)) return prev;
return [...prev, fileRequest];
});
}
);
return () => {
webrtcManager.setFileTransferCallbacks(null, null, null, null);
};
}, [webrtcManager]);
const handleFileSelect = async (files) => {
if (!isConnected || !webrtcManager) {
alert('Соединение не установлено. Сначала установите соединение.');
@@ -199,16 +136,10 @@ const FileTransferComponent = ({ webrtcManager, isConnected }) => {
};
const handleIncomingDecision = async (fileId, accepted) => {
try {
if (accepted) {
await webrtcManager.acceptIncomingFile(fileId);
} else {
await webrtcManager.rejectIncomingFile(fileId);
}
} finally {
setPendingIncomingFiles(prev => prev.filter(file => file.fileId !== fileId));
setTransfers(webrtcManager.getFileTransfers());
if (typeof onIncomingDecision === 'function') {
await onIncomingDecision(fileId, accepted);
}
setTransfers(webrtcManager.getFileTransfers());
};
if (!isConnected) {
@@ -424,29 +355,26 @@ const FileTransferComponent = ({ webrtcManager, isConnected }) => {
}, 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(e.message || 'This file is no longer available for download.');
}
transfer.status === 'completed' ? React.createElement('button', {
key: 'download',
className: 'text-green-400 hover:text-green-300 text-xs flex items-center',
onClick: async () => {
try {
const url = await webrtcManager.getReceivedFileObjectURL(transfer.fileId);
if (!url) { alert('This file is no longer available for download.'); return; }
const a = document.createElement('a');
a.href = url;
a.download = transfer.fileName || 'file';
a.click();
setTimeout(() => webrtcManager.revokeReceivedFileObjectURL(url), 10000);
} catch (e) {
alert(e.message || 'This file is no longer available for download.');
}
}, [
React.createElement('i', { key: 'i', className: 'fas fa-download mr-1' }),
'Download'
]);
})(),
}
}, [
React.createElement('i', { key: 'i', className: 'fas fa-download mr-1' }),
'Download'
]) : null,
React.createElement('button', {
key: 'cancel',
onClick: () => webrtcManager.cancelFileTransfer(transfer.fileId),