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
+22
View File
@@ -1,5 +1,27 @@
# Changelog # Changelog
## v4.8.8 — File transfer consent fix
This patch completes the mandatory receiver-consent gate for incoming file transfers and resolves a callback ownership conflict that caused every incoming file request to be silently auto-rejected.
### Fixed
- Wired up the missing fourth `onIncomingFileRequest` callback in the main `setFileTransferCallbacks` call. Without it, `handleFileTransferStart` always saw `null` for the consent handler and auto-rejected every incoming file silently.
- Removed independent callback registration from `FileTransferComponent`. The component was overwriting the application-level callbacks on mount and nulling all four on unmount, which destroyed the progress, received, and error handlers whenever the panel was hidden.
- Centralized incoming-consent state (`pendingIncomingFiles`) in the root application component so consent prompts appear regardless of whether the file-transfer panel is currently visible.
- Auto-opens the file-transfer panel when an incoming request arrives so the user sees the Accept / Reject prompt immediately.
- Added `getReceivedFileObjectURL` / `revokeReceivedFileObjectURL` helpers to `EnhancedSecureWebRTCManager` so the panel can offer a download button for completed transfers without relying on captured callback closures.
- Updated `file-transfer-ui-cleanup` regression test to match the new single-owner callback architecture.
### Security
No change to the cryptographic or transport-level security model. Sender chunks are still gated behind an explicit `file_transfer_response` from the receiver before any data is transmitted.
### Verification
- `npm test` — all 14 tests pass.
- `npm run build` — clean production build.
## v4.8.7 — WebRTC manual join reliability patch ## v4.8.7 — WebRTC manual join reliability patch
This patch improves manual WebRTC setup across separate devices and restrictive local networks. This patch improves manual WebRTC setup across separate devices and restrictive local networks.
+33 -77
View File
@@ -15281,6 +15281,14 @@ var EnhancedSecureWebRTCManager = class _EnhancedSecureWebRTCManager {
if (!this.fileTransferSystem) return false; if (!this.fileTransferSystem) return false;
return this.fileTransferSystem.rejectIncomingFile(fileId); return this.fileTransferSystem.rejectIncomingFile(fileId);
} }
async getReceivedFileObjectURL(fileId) {
if (!this.fileTransferSystem) return null;
return this.fileTransferSystem.getObjectURL(fileId);
}
revokeReceivedFileObjectURL(url) {
if (!this.fileTransferSystem) return;
this.fileTransferSystem.revokeObjectURL(url);
}
// ============================================ // ============================================
// SESSION ACTIVATION HANDLING // SESSION ACTIVATION HANDLING
// ============================================ // ============================================
@@ -18358,11 +18366,9 @@ function Roadmap() {
window.Roadmap = Roadmap; window.Roadmap = Roadmap;
// src/components/ui/FileTransfer.jsx // src/components/ui/FileTransfer.jsx
var FileTransferComponent = ({ webrtcManager, isConnected }) => { var FileTransferComponent = ({ webrtcManager, isConnected, pendingIncomingFiles = [], onIncomingDecision }) => {
const [dragOver, setDragOver] = React.useState(false); const [dragOver, setDragOver] = React.useState(false);
const [transfers, setTransfers] = React.useState({ sending: [], receiving: [] }); const [transfers, setTransfers] = React.useState({ sending: [], receiving: [] });
const [readyFiles, setReadyFiles] = React.useState([]);
const [pendingIncomingFiles, setPendingIncomingFiles] = React.useState([]);
const fileInputRef = React.useRef(null); const fileInputRef = React.useRef(null);
React.useEffect(() => { React.useEffect(() => {
if (!isConnected || !webrtcManager) return; if (!isConnected || !webrtcManager) return;
@@ -18375,52 +18381,8 @@ var FileTransferComponent = ({ webrtcManager, isConnected }) => {
}, [isConnected, webrtcManager]); }, [isConnected, webrtcManager]);
React.useEffect(() => { React.useEffect(() => {
if (isConnected) return; if (isConnected) return;
setReadyFiles([]);
setPendingIncomingFiles([]);
setTransfers({ sending: [], receiving: [] }); setTransfers({ sending: [], receiving: [] });
}, [isConnected]); }, [isConnected]);
React.useEffect(() => {
if (!webrtcManager) return;
webrtcManager.setFileTransferCallbacks(
// Progress callback - ТОЛЬКО обновляем UI, НЕ отправляем в чат
(progress) => {
const currentTransfers = webrtcManager.getFileTransfers();
setTransfers(currentTransfers);
},
// File received callback - добавляем кнопку скачивания в UI
(fileData) => {
setReadyFiles((prev) => {
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);
},
// 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) => { const handleFileSelect = async (files) => {
if (!isConnected || !webrtcManager) { if (!isConnected || !webrtcManager) {
alert("\u0421\u043E\u0435\u0434\u0438\u043D\u0435\u043D\u0438\u0435 \u043D\u0435 \u0443\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D\u043E. \u0421\u043D\u0430\u0447\u0430\u043B\u0430 \u0443\u0441\u0442\u0430\u043D\u043E\u0432\u0438\u0442\u0435 \u0441\u043E\u0435\u0434\u0438\u043D\u0435\u043D\u0438\u0435."); alert("\u0421\u043E\u0435\u0434\u0438\u043D\u0435\u043D\u0438\u0435 \u043D\u0435 \u0443\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D\u043E. \u0421\u043D\u0430\u0447\u0430\u043B\u0430 \u0443\u0441\u0442\u0430\u043D\u043E\u0432\u0438\u0442\u0435 \u0441\u043E\u0435\u0434\u0438\u043D\u0435\u043D\u0438\u0435.");
@@ -18517,16 +18479,10 @@ var FileTransferComponent = ({ webrtcManager, isConnected }) => {
} }
}; };
const handleIncomingDecision = async (fileId, accepted) => { const handleIncomingDecision = async (fileId, accepted) => {
try { if (typeof onIncomingDecision === "function") {
if (accepted) { await onIncomingDecision(fileId, accepted);
await webrtcManager.acceptIncomingFile(fileId);
} else {
await webrtcManager.rejectIncomingFile(fileId);
}
} finally {
setPendingIncomingFiles((prev) => prev.filter((file) => file.fileId !== fileId));
setTransfers(webrtcManager.getFileTransfers());
} }
setTransfers(webrtcManager.getFileTransfers());
}; };
if (!isConnected) { if (!isConnected) {
return React.createElement("div", { return React.createElement("div", {
@@ -18732,29 +18688,29 @@ var FileTransferComponent = ({ webrtcManager, isConnected }) => {
}, formatFileSize(transfer.fileSize)) }, formatFileSize(transfer.fileSize))
]), ]),
React.createElement("div", { key: "actions", className: "flex items-center space-x-2" }, [ React.createElement("div", { key: "actions", className: "flex items-center space-x-2" }, [
(() => { transfer.status === "completed" ? React.createElement("button", {
const rf = readyFiles.find((f) => f.fileId === transfer.fileId); key: "download",
if (!rf || transfer.status !== "completed") return null; className: "text-green-400 hover:text-green-300 text-xs flex items-center",
return React.createElement("button", { onClick: async () => {
key: "download", try {
className: "text-green-400 hover:text-green-300 text-xs flex items-center", const url = await webrtcManager.getReceivedFileObjectURL(transfer.fileId);
onClick: async () => { if (!url) {
try { alert("This file is no longer available for download.");
const url = await rf.getObjectURL(); return;
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.");
} }
const a = document.createElement("a");
a.href = url;
a.download = transfer.fileName || "file";
a.click();
setTimeout(() => webrtcManager.revokeReceivedFileObjectURL(url), 1e4);
} 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", { React.createElement("button", {
key: "cancel", key: "cancel",
onClick: () => webrtcManager.cancelFileTransfer(transfer.fileId), onClick: () => webrtcManager.cancelFileTransfer(transfer.fileId),
+2 -2
View File
File diff suppressed because one or more lines are too long
Vendored
+34 -3
View File
@@ -1186,10 +1186,17 @@ var EnhancedChatInterface = ({
isVerified, isVerified,
chatMessagesRef, chatMessagesRef,
scrollToBottom, scrollToBottom,
webrtcManager webrtcManager,
pendingIncomingFiles = [],
onIncomingDecision
}) => { }) => {
const [showScrollButton, setShowScrollButton] = React.useState(false); const [showScrollButton, setShowScrollButton] = React.useState(false);
const [showFileTransfer, setShowFileTransfer] = React.useState(false); const [showFileTransfer, setShowFileTransfer] = React.useState(false);
React.useEffect(() => {
if (pendingIncomingFiles.length > 0) {
setShowFileTransfer(true);
}
}, [pendingIncomingFiles.length]);
React.useEffect(() => { React.useEffect(() => {
if (chatMessagesRef.current && messages.length > 0) { if (chatMessagesRef.current && messages.length > 0) {
const { scrollTop, scrollHeight, clientHeight } = chatMessagesRef.current; const { scrollTop, scrollHeight, clientHeight } = chatMessagesRef.current;
@@ -1398,7 +1405,9 @@ var EnhancedChatInterface = ({
className: "p-4 text-center text-red-400" className: "p-4 text-center text-red-400"
}, "FileTransferComponent not loaded")), { }, "FileTransferComponent not loaded")), {
webrtcManager, webrtcManager,
isConnected: isFileTransferReady() isConnected: isFileTransferReady(),
pendingIncomingFiles,
onIncomingDecision
}) })
] ]
) )
@@ -1494,6 +1503,7 @@ var EnhancedSecureP2PChat = () => {
const [remoteVerificationConfirmed, setRemoteVerificationConfirmed] = React.useState(false); const [remoteVerificationConfirmed, setRemoteVerificationConfirmed] = React.useState(false);
const [bothVerificationsConfirmed, setBothVerificationsConfirmed] = React.useState(false); const [bothVerificationsConfirmed, setBothVerificationsConfirmed] = React.useState(false);
const [pendingSession, setPendingSession] = React.useState(null); const [pendingSession, setPendingSession] = React.useState(null);
const [pendingIncomingFiles, setPendingIncomingFiles] = React.useState([]);
const [connectionState, setConnectionState] = React.useState({ const [connectionState, setConnectionState] = React.useState({
status: "disconnected", status: "disconnected",
hasActiveAnswer: false, hasActiveAnswer: false,
@@ -1899,6 +1909,13 @@ var EnhancedSecureP2PChat = () => {
} else { } else {
addMessageWithAutoScroll(` File transfer error: ${error}`, "system"); addMessageWithAutoScroll(` File transfer error: ${error}`, "system");
} }
},
// Incoming file request callback — receiver must explicitly accept before any data is sent
(fileRequest) => {
setPendingIncomingFiles((prev) => {
if (prev.some((f) => f.fileId === fileRequest.fileId)) return prev;
return [...prev, fileRequest];
});
} }
); );
} }
@@ -3044,6 +3061,17 @@ var EnhancedSecureP2PChat = () => {
setPendingSession(null); setPendingSession(null);
document.dispatchEvent(new CustomEvent("peer-disconnect")); document.dispatchEvent(new CustomEvent("peer-disconnect"));
}; };
const handleIncomingDecision = React.useCallback(async (fileId, accepted) => {
try {
if (accepted) {
await webrtcManagerRef.current?.acceptIncomingFile(fileId);
} else {
await webrtcManagerRef.current?.rejectIncomingFile(fileId);
}
} finally {
setPendingIncomingFiles((prev) => prev.filter((f) => f.fileId !== fileId));
}
}, []);
const handleDisconnect = () => { const handleDisconnect = () => {
try { try {
setSessionTimeLeft(0); setSessionTimeLeft(0);
@@ -3079,6 +3107,7 @@ var EnhancedSecureP2PChat = () => {
setShowQRScanner(false); setShowQRScanner(false);
setShowQRScannerModal(false); setShowQRScannerModal(false);
setMessages([]); setMessages([]);
setPendingIncomingFiles([]);
if (typeof console.clear === "function") { if (typeof console.clear === "function") {
console.clear(); console.clear();
} }
@@ -3236,7 +3265,9 @@ var EnhancedSecureP2PChat = () => {
isVerified, isVerified,
chatMessagesRef, chatMessagesRef,
scrollToBottom, scrollToBottom,
webrtcManager: webrtcManagerRef.current webrtcManager: webrtcManagerRef.current,
pendingIncomingFiles,
onIncomingDecision: handleIncomingDecision
}); });
})() : React.createElement(EnhancedConnectionSetup, { })() : React.createElement(EnhancedConnectionSetup, {
onCreateOffer: handleCreateOffer, onCreateOffer: handleCreateOffer,
+2 -2
View File
File diff suppressed because one or more lines are too long
+4 -4
View File
@@ -148,13 +148,13 @@
<!-- Update Manager - система принудительного обновления --> <!-- Update Manager - система принудительного обновления -->
<script src="src/utils/updateManager.js"></script> <script src="src/utils/updateManager.js"></script>
<script type="module" src="src/components/UpdateChecker.jsx"></script> <script type="module" src="src/components/UpdateChecker.jsx"></script>
<script type="module" src="dist/qr-local.js?v=1779198538664"></script> <script type="module" src="dist/qr-local.js?v=1779848383991"></script>
<script type="module" src="src/components/QRScanner.js?v=1779198538664"></script> <script type="module" src="src/components/QRScanner.js?v=1779848383991"></script>
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>
<script type="module" src="dist/app-boot.js?v=1779198538664"></script> <script type="module" src="dist/app-boot.js?v=1779848383991"></script>
<script type="module" src="dist/app.js?v=1779198538664"></script> <script type="module" src="dist/app.js?v=1779848383991"></script>
<script src="src/scripts/pwa-register.js"></script> <script src="src/scripts/pwa-register.js"></script>
<script src="./src/pwa/install-prompt.js" type="module"></script> <script src="./src/pwa/install-prompt.js" type="module"></script>
+7 -7
View File
@@ -1,10 +1,10 @@
{ {
"version": "1779198538664", "version": "1779848383991",
"buildVersion": "1779198538664", "buildVersion": "1779848383991",
"appVersion": "4.8.7", "appVersion": "4.8.8",
"buildTime": "2026-05-19T13:48:58.703Z", "buildTime": "2026-05-27T02:19:44.033Z",
"buildId": "1779198538664-1cc8732", "buildId": "1779848383991-2468cb4",
"gitHash": "1cc8732", "gitHash": "2468cb4",
"generated": true, "generated": true,
"generatedAt": "2026-05-19T13:48:58.704Z" "generatedAt": "2026-05-27T02:19:44.035Z"
} }
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "securebit-chat", "name": "securebit-chat",
"version": "4.8.7", "version": "4.8.8",
"description": "Secure P2P Communication Application with End-to-End Encryption", "description": "Secure P2P Communication Application with End-to-End Encryption",
"main": "index.html", "main": "index.html",
"scripts": { "scripts": {
+39 -3
View File
@@ -1241,11 +1241,20 @@ import { installDebugWindowHooks } from './utils/debugWindowHooks.js';
isVerified, isVerified,
chatMessagesRef, chatMessagesRef,
scrollToBottom, scrollToBottom,
webrtcManager webrtcManager,
pendingIncomingFiles = [],
onIncomingDecision
}) => { }) => {
const [showScrollButton, setShowScrollButton] = React.useState(false); const [showScrollButton, setShowScrollButton] = React.useState(false);
const [showFileTransfer, setShowFileTransfer] = React.useState(false); const [showFileTransfer, setShowFileTransfer] = React.useState(false);
// Auto-open the file transfer panel when an incoming request arrives
React.useEffect(() => {
if (pendingIncomingFiles.length > 0) {
setShowFileTransfer(true);
}
}, [pendingIncomingFiles.length]);
React.useEffect(() => { React.useEffect(() => {
if (chatMessagesRef.current && messages.length > 0) { if (chatMessagesRef.current && messages.length > 0) {
const { scrollTop, scrollHeight, clientHeight } = chatMessagesRef.current; const { scrollTop, scrollHeight, clientHeight } = chatMessagesRef.current;
@@ -1475,7 +1484,9 @@ import { installDebugWindowHooks } from './utils/debugWindowHooks.js';
}, 'FileTransferComponent not loaded') }, 'FileTransferComponent not loaded')
), { ), {
webrtcManager: webrtcManager, webrtcManager: webrtcManager,
isConnected: isFileTransferReady() isConnected: isFileTransferReady(),
pendingIncomingFiles: pendingIncomingFiles,
onIncomingDecision: onIncomingDecision
}) })
] ]
) )
@@ -1585,6 +1596,8 @@ import { installDebugWindowHooks } from './utils/debugWindowHooks.js';
// All security features are enabled by default - no payment required // All security features are enabled by default - no payment required
const [pendingIncomingFiles, setPendingIncomingFiles] = React.useState([]);
// ============================================ // ============================================
@@ -2111,6 +2124,14 @@ import { installDebugWindowHooks } from './utils/debugWindowHooks.js';
} else { } else {
addMessageWithAutoScroll(` File transfer error: ${error}`, 'system'); addMessageWithAutoScroll(` File transfer error: ${error}`, 'system');
} }
},
// Incoming file request callback receiver must explicitly accept before any data is sent
(fileRequest) => {
setPendingIncomingFiles(prev => {
if (prev.some(f => f.fileId === fileRequest.fileId)) return prev;
return [...prev, fileRequest];
});
} }
); );
} }
@@ -3464,6 +3485,18 @@ import { installDebugWindowHooks } from './utils/debugWindowHooks.js';
// Session manager removed - all features enabled by default // Session manager removed - all features enabled by default
}; };
const handleIncomingDecision = React.useCallback(async (fileId, accepted) => {
try {
if (accepted) {
await webrtcManagerRef.current?.acceptIncomingFile(fileId);
} else {
await webrtcManagerRef.current?.rejectIncomingFile(fileId);
}
} finally {
setPendingIncomingFiles(prev => prev.filter(f => f.fileId !== fileId));
}
}, []);
const handleDisconnect = () => { const handleDisconnect = () => {
try { try {
setSessionTimeLeft(0); setSessionTimeLeft(0);
@@ -3513,6 +3546,7 @@ import { installDebugWindowHooks } from './utils/debugWindowHooks.js';
// Clear messages // Clear messages
setMessages([]); setMessages([]);
setPendingIncomingFiles([]);
// Clear console // Clear console
if (typeof console.clear === 'function') { if (typeof console.clear === 'function') {
@@ -3705,7 +3739,9 @@ import { installDebugWindowHooks } from './utils/debugWindowHooks.js';
isVerified: isVerified, isVerified: isVerified,
chatMessagesRef: chatMessagesRef, chatMessagesRef: chatMessagesRef,
scrollToBottom: scrollToBottom, scrollToBottom: scrollToBottom,
webrtcManager: webrtcManagerRef.current webrtcManager: webrtcManagerRef.current,
pendingIncomingFiles: pendingIncomingFiles,
onIncomingDecision: handleIncomingDecision
}); });
})() })()
: React.createElement(EnhancedConnectionSetup, { : React.createElement(EnhancedConnectionSetup, {
+25 -97
View File
@@ -1,12 +1,10 @@
// File Transfer Component for Chat Interface - Fixed Version // 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 [dragOver, setDragOver] = React.useState(false);
const [transfers, setTransfers] = React.useState({ sending: [], receiving: [] }); const [transfers, setTransfers] = React.useState({ sending: [], receiving: [] });
const [readyFiles, setReadyFiles] = React.useState([]); // файлы, готовые к скачиванию
const [pendingIncomingFiles, setPendingIncomingFiles] = React.useState([]);
const fileInputRef = React.useRef(null); const fileInputRef = React.useRef(null);
// Update transfers periodically // Update transfers periodically via polling no callback registration needed here
React.useEffect(() => { React.useEffect(() => {
if (!isConnected || !webrtcManager) return; if (!isConnected || !webrtcManager) return;
@@ -19,73 +17,12 @@ const FileTransferComponent = ({ webrtcManager, isConnected }) => {
return () => clearInterval(interval); return () => clearInterval(interval);
}, [isConnected, webrtcManager]); }, [isConnected, webrtcManager]);
// Clear session-local UI state when the connection ends so reconnect starts clean. // Clear transfers UI when connection drops
React.useEffect(() => { React.useEffect(() => {
if (isConnected) return; if (isConnected) return;
setReadyFiles([]);
setPendingIncomingFiles([]);
setTransfers({ sending: [], receiving: [] }); setTransfers({ sending: [], receiving: [] });
}, [isConnected]); }, [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) => { const handleFileSelect = async (files) => {
if (!isConnected || !webrtcManager) { if (!isConnected || !webrtcManager) {
alert('Соединение не установлено. Сначала установите соединение.'); alert('Соединение не установлено. Сначала установите соединение.');
@@ -199,16 +136,10 @@ const FileTransferComponent = ({ webrtcManager, isConnected }) => {
}; };
const handleIncomingDecision = async (fileId, accepted) => { const handleIncomingDecision = async (fileId, accepted) => {
try { if (typeof onIncomingDecision === 'function') {
if (accepted) { await onIncomingDecision(fileId, accepted);
await webrtcManager.acceptIncomingFile(fileId);
} else {
await webrtcManager.rejectIncomingFile(fileId);
}
} finally {
setPendingIncomingFiles(prev => prev.filter(file => file.fileId !== fileId));
setTransfers(webrtcManager.getFileTransfers());
} }
setTransfers(webrtcManager.getFileTransfers());
}; };
if (!isConnected) { if (!isConnected) {
@@ -424,29 +355,26 @@ const FileTransferComponent = ({ webrtcManager, isConnected }) => {
}, formatFileSize(transfer.fileSize)) }, formatFileSize(transfer.fileSize))
]), ]),
React.createElement('div', { key: 'actions', className: 'flex items-center space-x-2' }, [ React.createElement('div', { key: 'actions', className: 'flex items-center space-x-2' }, [
(() => { transfer.status === 'completed' ? React.createElement('button', {
const rf = readyFiles.find(f => f.fileId === transfer.fileId); key: 'download',
if (!rf || transfer.status !== 'completed') return null; className: 'text-green-400 hover:text-green-300 text-xs flex items-center',
return React.createElement('button', { onClick: async () => {
key: 'download', try {
className: 'text-green-400 hover:text-green-300 text-xs flex items-center', const url = await webrtcManager.getReceivedFileObjectURL(transfer.fileId);
onClick: async () => { if (!url) { alert('This file is no longer available for download.'); return; }
try { const a = document.createElement('a');
const url = await rf.getObjectURL(); a.href = url;
const a = document.createElement('a'); a.download = transfer.fileName || 'file';
a.href = url; a.click();
a.download = rf.fileName || 'file'; setTimeout(() => webrtcManager.revokeReceivedFileObjectURL(url), 10000);
a.click(); } catch (e) {
rf.revokeObjectURL(url); alert(e.message || 'This file is no longer available for download.');
} 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', { React.createElement('button', {
key: 'cancel', key: 'cancel',
onClick: () => webrtcManager.cancelFileTransfer(transfer.fileId), onClick: () => webrtcManager.cancelFileTransfer(transfer.fileId),
@@ -12389,6 +12389,16 @@ async processMessage(data) {
return this.fileTransferSystem.rejectIncomingFile(fileId); return this.fileTransferSystem.rejectIncomingFile(fileId);
} }
async getReceivedFileObjectURL(fileId) {
if (!this.fileTransferSystem) return null;
return this.fileTransferSystem.getObjectURL(fileId);
}
revokeReceivedFileObjectURL(url) {
if (!this.fileTransferSystem) return;
this.fileTransferSystem.revokeObjectURL(url);
}
// ============================================ // ============================================
// SESSION ACTIVATION HANDLING // SESSION ACTIVATION HANDLING
// ============================================ // ============================================
+16 -8
View File
@@ -58,18 +58,26 @@ const manager = {
isVerified: false isVerified: false
}; };
context.window.FileTransferComponent({ webrtcManager: manager, isConnected: false }); // Component no longer manages callbacks — consent is handled by the parent (app.jsx).
// pendingIncomingFiles and onIncomingDecision are passed as props.
context.window.FileTransferComponent({ webrtcManager: manager, isConnected: false, pendingIncomingFiles: [], onIncomingDecision: null });
const cleanups = effects.map(effect => effect()).filter(Boolean); const cleanups = effects.map(effect => effect()).filter(Boolean);
assert.ok(setterCalls.some(call => call.index === 2 && Array.isArray(call.value) && call.value.length === 0)); // State index 0 = dragOver, index 1 = transfers.
assert.ok(setterCalls.some(call => call.index === 3 && Array.isArray(call.value) && call.value.length === 0)); // Transfers state should be reset to empty on disconnect.
assert.ok(setterCalls.some(call => call.index === 1 && call.value.sending.length === 0 && call.value.receiving.length === 0)); assert.ok(setterCalls.some(call => call.index === 1 && call.value.sending.length === 0 && call.value.receiving.length === 0));
// Component must NOT call setFileTransferCallbacks — that is the parent's responsibility.
assert.equal(callbackCalls.length, 0, 'FileTransferComponent must not register its own callbacks');
// Cleanup effects must not null-out the manager's callbacks either.
cleanups.forEach(cleanup => cleanup()); cleanups.forEach(cleanup => cleanup());
assert.deepEqual(callbackCalls.at(-1), [null, null, null, null]); assert.equal(callbackCalls.length, 0, 'cleanup must not call setFileTransferCallbacks');
assert.equal(manager.fileTransferSystem.onProgress, null);
assert.equal(manager.fileTransferSystem.onFileReceived, null); // fileTransferSystem callbacks are untouched by the component.
assert.equal(manager.fileTransferSystem.onError, null); assert.equal(typeof manager.fileTransferSystem.onProgress, 'function');
assert.equal(manager.fileTransferSystem.onIncomingFileRequest, null); assert.equal(typeof manager.fileTransferSystem.onFileReceived, 'function');
assert.equal(typeof manager.fileTransferSystem.onError, 'function');
assert.equal(typeof manager.fileTransferSystem.onIncomingFileRequest, 'function');
console.log('File transfer UI cleanup tests passed'); console.log('File transfer UI cleanup tests passed');