release: v4.8.8 file transfer consent fix
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.
This commit is contained in:
Vendored
+33
-77
@@ -15281,6 +15281,14 @@ var EnhancedSecureWebRTCManager = class _EnhancedSecureWebRTCManager {
|
||||
if (!this.fileTransferSystem) return false;
|
||||
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
|
||||
// ============================================
|
||||
@@ -18358,11 +18366,9 @@ function Roadmap() {
|
||||
window.Roadmap = Roadmap;
|
||||
|
||||
// src/components/ui/FileTransfer.jsx
|
||||
var FileTransferComponent = ({ webrtcManager, isConnected }) => {
|
||||
var 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);
|
||||
React.useEffect(() => {
|
||||
if (!isConnected || !webrtcManager) return;
|
||||
@@ -18375,52 +18381,8 @@ var FileTransferComponent = ({ webrtcManager, isConnected }) => {
|
||||
}, [isConnected, webrtcManager]);
|
||||
React.useEffect(() => {
|
||||
if (isConnected) return;
|
||||
setReadyFiles([]);
|
||||
setPendingIncomingFiles([]);
|
||||
setTransfers({ sending: [], receiving: [] });
|
||||
}, [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) => {
|
||||
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.");
|
||||
@@ -18517,16 +18479,10 @@ var 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) {
|
||||
return React.createElement("div", {
|
||||
@@ -18732,29 +18688,29 @@ var 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), 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", {
|
||||
key: "cancel",
|
||||
onClick: () => webrtcManager.cancelFileTransfer(transfer.fileId),
|
||||
|
||||
Vendored
+2
-2
File diff suppressed because one or more lines are too long
Vendored
+34
-3
@@ -1186,10 +1186,17 @@ var EnhancedChatInterface = ({
|
||||
isVerified,
|
||||
chatMessagesRef,
|
||||
scrollToBottom,
|
||||
webrtcManager
|
||||
webrtcManager,
|
||||
pendingIncomingFiles = [],
|
||||
onIncomingDecision
|
||||
}) => {
|
||||
const [showScrollButton, setShowScrollButton] = React.useState(false);
|
||||
const [showFileTransfer, setShowFileTransfer] = React.useState(false);
|
||||
React.useEffect(() => {
|
||||
if (pendingIncomingFiles.length > 0) {
|
||||
setShowFileTransfer(true);
|
||||
}
|
||||
}, [pendingIncomingFiles.length]);
|
||||
React.useEffect(() => {
|
||||
if (chatMessagesRef.current && messages.length > 0) {
|
||||
const { scrollTop, scrollHeight, clientHeight } = chatMessagesRef.current;
|
||||
@@ -1398,7 +1405,9 @@ var EnhancedChatInterface = ({
|
||||
className: "p-4 text-center text-red-400"
|
||||
}, "FileTransferComponent not loaded")), {
|
||||
webrtcManager,
|
||||
isConnected: isFileTransferReady()
|
||||
isConnected: isFileTransferReady(),
|
||||
pendingIncomingFiles,
|
||||
onIncomingDecision
|
||||
})
|
||||
]
|
||||
)
|
||||
@@ -1494,6 +1503,7 @@ var EnhancedSecureP2PChat = () => {
|
||||
const [remoteVerificationConfirmed, setRemoteVerificationConfirmed] = React.useState(false);
|
||||
const [bothVerificationsConfirmed, setBothVerificationsConfirmed] = React.useState(false);
|
||||
const [pendingSession, setPendingSession] = React.useState(null);
|
||||
const [pendingIncomingFiles, setPendingIncomingFiles] = React.useState([]);
|
||||
const [connectionState, setConnectionState] = React.useState({
|
||||
status: "disconnected",
|
||||
hasActiveAnswer: false,
|
||||
@@ -1899,6 +1909,13 @@ var EnhancedSecureP2PChat = () => {
|
||||
} else {
|
||||
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);
|
||||
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 = () => {
|
||||
try {
|
||||
setSessionTimeLeft(0);
|
||||
@@ -3079,6 +3107,7 @@ var EnhancedSecureP2PChat = () => {
|
||||
setShowQRScanner(false);
|
||||
setShowQRScannerModal(false);
|
||||
setMessages([]);
|
||||
setPendingIncomingFiles([]);
|
||||
if (typeof console.clear === "function") {
|
||||
console.clear();
|
||||
}
|
||||
@@ -3236,7 +3265,9 @@ var EnhancedSecureP2PChat = () => {
|
||||
isVerified,
|
||||
chatMessagesRef,
|
||||
scrollToBottom,
|
||||
webrtcManager: webrtcManagerRef.current
|
||||
webrtcManager: webrtcManagerRef.current,
|
||||
pendingIncomingFiles,
|
||||
onIncomingDecision: handleIncomingDecision
|
||||
});
|
||||
})() : React.createElement(EnhancedConnectionSetup, {
|
||||
onCreateOffer: handleCreateOffer,
|
||||
|
||||
Vendored
+2
-2
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user