Fix the problem of resuming downloads when the webpage is closed or the network is disconnected. Further testing is needed.
This commit is contained in:
@@ -1,5 +1,11 @@
|
||||
"use client";
|
||||
import React, { useState, useRef, useCallback, useEffect } from "react";
|
||||
import React, {
|
||||
useState,
|
||||
useRef,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
} from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import useRichTextToPlainText from "../hooks/useRichTextToPlainText";
|
||||
import QRCodeComponent from "./ClipboardApp/ShareCard";
|
||||
@@ -46,7 +52,12 @@ const ClipboardApp = () => {
|
||||
onFileFullyReceived,
|
||||
handleDownloadFile,
|
||||
} = useFileTransferHandler({ messages, putMessageInMs });
|
||||
|
||||
// Calculate the derived states for unload protection
|
||||
const isContentPresent = useMemo(() => {
|
||||
return (
|
||||
shareContent !== "" || retrievedContent !== "" || sendFiles.length > 0
|
||||
);
|
||||
}, [shareContent, retrievedContent, sendFiles]);
|
||||
// Initialize WebRTC Connection Hook
|
||||
const {
|
||||
sender,
|
||||
@@ -55,6 +66,7 @@ const ClipboardApp = () => {
|
||||
retrievePeerCount,
|
||||
sendProgress,
|
||||
receiveProgress,
|
||||
isAnyFileTransferring,
|
||||
broadcastDataToAllPeers,
|
||||
requestFile,
|
||||
requestFolder,
|
||||
@@ -63,6 +75,7 @@ const ClipboardApp = () => {
|
||||
} = useWebRTCConnection({
|
||||
shareContent,
|
||||
sendFiles,
|
||||
isContentPresent,
|
||||
messages,
|
||||
putMessageInMs,
|
||||
onStringReceived: onStringDataReceived,
|
||||
@@ -224,6 +237,7 @@ const ClipboardApp = () => {
|
||||
removeFileToSend={removeFileToSend}
|
||||
richTextToPlainText={richTextToPlainText}
|
||||
sendProgress={sendProgress}
|
||||
isAnyFileTransferring={isAnyFileTransferring}
|
||||
processRoomIdInput={processRoomIdInput}
|
||||
joinRoom={joinRoom}
|
||||
generateShareLinkAndBroadcast={generateShareLinkAndBroadcast}
|
||||
@@ -245,6 +259,7 @@ const ClipboardApp = () => {
|
||||
richTextToPlainText={richTextToPlainText}
|
||||
retrievedFileMetas={retrievedFileMetas}
|
||||
receiveProgress={receiveProgress}
|
||||
isAnyFileTransferring={isAnyFileTransferring}
|
||||
handleDownloadFile={handleDownloadFile}
|
||||
// Pass WebRTC interaction methods
|
||||
requestFile={requestFile}
|
||||
|
||||
@@ -35,6 +35,7 @@ interface FileListDisplayProps {
|
||||
[peerId: string]: Progress;
|
||||
};
|
||||
};
|
||||
isAnyFileTransferring: boolean; // State lifted up
|
||||
onDownload?: (item: FileMeta) => void;
|
||||
onRequest?: (item: FileMeta) => void; // Request file
|
||||
onDelete?: (item: FileMeta) => void;
|
||||
@@ -55,6 +56,7 @@ const FileListDisplay: React.FC<FileListDisplayProps> = ({
|
||||
mode,
|
||||
files,
|
||||
fileProgresses,
|
||||
isAnyFileTransferring,
|
||||
onDownload,
|
||||
onRequest,
|
||||
onDelete,
|
||||
@@ -79,8 +81,6 @@ const FileListDisplay: React.FC<FileListDisplayProps> = ({
|
||||
const [activeTransfers, setActiveTransfers] = useState<{
|
||||
[fileId: string]: string;
|
||||
}>({});
|
||||
// Track if any file transfer is in progress
|
||||
const [isAnyFileTransferring, setIsAnyFileTransferring] = useState(false);
|
||||
// Add state for download counts
|
||||
const [downloadCounts, setDownloadCounts] = useState<{
|
||||
[fileId: string]: number;
|
||||
@@ -90,16 +90,6 @@ const FileListDisplay: React.FC<FileListDisplayProps> = ({
|
||||
.then((dict) => setMessages(dict))
|
||||
.catch((error) => console.error("Failed to load messages:", error));
|
||||
}, [locale]);
|
||||
// Monitor file transfer status
|
||||
useEffect(() => {
|
||||
const hasActiveTransfer = Object.values(fileProgresses).some(
|
||||
(fileProgress) =>
|
||||
Object.values(fileProgress).some(
|
||||
(progress) => progress.progress > 0 && progress.progress < 1
|
||||
)
|
||||
);
|
||||
setIsAnyFileTransferring(hasActiveTransfer);
|
||||
}, [fileProgresses]);
|
||||
|
||||
useEffect(() => {
|
||||
// Separate single files and folders
|
||||
|
||||
@@ -30,6 +30,7 @@ interface RetrieveTabPanelProps {
|
||||
richTextToPlainText: (html: string) => string;
|
||||
retrievedFileMetas: FileMeta[];
|
||||
receiveProgress: ProgressState;
|
||||
isAnyFileTransferring: boolean;
|
||||
handleDownloadFile: (meta: FileMeta) => void;
|
||||
// Functions for WebRTC interaction, passed from parent via useWebRTCConnection
|
||||
requestFile: (fileId: string, peerId?: string) => void;
|
||||
@@ -54,6 +55,7 @@ export function RetrieveTabPanel({
|
||||
richTextToPlainText,
|
||||
retrievedFileMetas,
|
||||
receiveProgress,
|
||||
isAnyFileTransferring,
|
||||
handleDownloadFile,
|
||||
requestFile,
|
||||
requestFolder,
|
||||
@@ -163,6 +165,7 @@ export function RetrieveTabPanel({
|
||||
mode="receiver"
|
||||
files={retrievedFileMetas}
|
||||
fileProgresses={receiveProgress}
|
||||
isAnyFileTransferring={isAnyFileTransferring}
|
||||
onDownload={handleDownloadFile}
|
||||
onRequest={handleFileRequestFromPanel} // Use the panel's own handler
|
||||
onLocationPick={onLocationPick} // Use the panel's own handler
|
||||
|
||||
@@ -37,6 +37,7 @@ interface SendTabPanelProps {
|
||||
removeFileToSend: (meta: FileMeta) => void;
|
||||
richTextToPlainText: (html: string) => string;
|
||||
sendProgress: ProgressState;
|
||||
isAnyFileTransferring: boolean;
|
||||
// shareRoomId: string; // This comes from useRoomManager and represents the validated ID
|
||||
processRoomIdInput: (roomId: string) => void; // Passed from useRoomManager
|
||||
joinRoom: (isSender: boolean, roomId: string) => void;
|
||||
@@ -59,6 +60,7 @@ export function SendTabPanel({
|
||||
removeFileToSend,
|
||||
richTextToPlainText,
|
||||
sendProgress,
|
||||
isAnyFileTransferring,
|
||||
processRoomIdInput,
|
||||
joinRoom,
|
||||
generateShareLinkAndBroadcast,
|
||||
@@ -120,6 +122,7 @@ SendTabPanelProps) {
|
||||
mode="sender"
|
||||
files={sendFiles}
|
||||
fileProgresses={sendProgress}
|
||||
isAnyFileTransferring={isAnyFileTransferring}
|
||||
onDelete={removeFileToSend}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -1,19 +1,9 @@
|
||||
import { useState, useCallback, useEffect } from "react";
|
||||
import { useState, useCallback } from "react";
|
||||
import { CustomFile, FileMeta, fileMetadata } from "@/types/webrtc";
|
||||
import { Messages } from "@/types/messages";
|
||||
import JSZip from "jszip";
|
||||
import { downloadAs } from "@/lib/fileUtils";
|
||||
|
||||
// Helper functions for beforeunload (can be kept local to this hook if not used elsewhere)
|
||||
const handleWindowBeforeUnload = (event: BeforeUnloadEvent) => {
|
||||
event.preventDefault();
|
||||
event.returnValue = ""; // Required for Chrome
|
||||
};
|
||||
|
||||
const allowWindowUnload = () => {
|
||||
window.removeEventListener("beforeunload", handleWindowBeforeUnload);
|
||||
};
|
||||
|
||||
interface UseFileTransferHandlerProps {
|
||||
messages: Messages | null;
|
||||
putMessageInMs: (
|
||||
@@ -33,23 +23,6 @@ export function useFileTransferHandler({
|
||||
const [retrievedFiles, setRetrievedFiles] = useState<CustomFile[]>([]);
|
||||
const [retrievedFileMetas, setRetrievedFileMetas] = useState<FileMeta[]>([]);
|
||||
|
||||
// Manage beforeunload event based on content
|
||||
useEffect(() => {
|
||||
if (
|
||||
sendFiles.length === 0 &&
|
||||
shareContent === "" &&
|
||||
retrievedFiles.length === 0 &&
|
||||
retrievedContent === ""
|
||||
) {
|
||||
allowWindowUnload();
|
||||
} else {
|
||||
window.addEventListener("beforeunload", handleWindowBeforeUnload);
|
||||
}
|
||||
return () => {
|
||||
allowWindowUnload(); // Clean up listener when hook unmounts or dependencies change if any
|
||||
};
|
||||
}, [sendFiles, shareContent, retrievedFiles, retrievedContent]);
|
||||
|
||||
const updateShareContent = useCallback((content: string) => {
|
||||
setShareContent(content);
|
||||
}, []);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useState, useEffect, useCallback } from "react";
|
||||
import { useState, useEffect, useCallback, useMemo } from "react";
|
||||
import WebRTC_Initiator from "@/lib/webrtc_Initiator";
|
||||
import WebRTC_Recipient from "@/lib/webrtc_Recipient";
|
||||
import FileSender from "@/lib/fileSender";
|
||||
@@ -21,6 +21,7 @@ export type ProgressState = { [fileId: string]: FileProgressPeers };
|
||||
interface UseWebRTCConnectionProps {
|
||||
shareContent: string;
|
||||
sendFiles: CustomFile[];
|
||||
isContentPresent: boolean; // To know if there is any content (text or files)
|
||||
// Callbacks for data received from peers
|
||||
onStringReceived: (data: string, peerId: string) => void;
|
||||
onFileMetaReceived: (meta: fileMetadata, peerId: string) => void;
|
||||
@@ -38,6 +39,7 @@ interface UseWebRTCConnectionProps {
|
||||
export function useWebRTCConnection({
|
||||
shareContent,
|
||||
sendFiles,
|
||||
isContentPresent,
|
||||
onStringReceived,
|
||||
onFileMetaReceived,
|
||||
onFileReceived,
|
||||
@@ -57,6 +59,19 @@ export function useWebRTCConnection({
|
||||
const [sendProgress, setSendProgress] = useState<ProgressState>({});
|
||||
const [receiveProgress, setReceiveProgress] = useState<ProgressState>({});
|
||||
|
||||
// Calculate isAnyFileTransferring internally based on progress states
|
||||
const isAnyFileTransferring = useMemo(() => {
|
||||
const allProgress = [
|
||||
...Object.values(sendProgress),
|
||||
...Object.values(receiveProgress),
|
||||
];
|
||||
return allProgress.some((fileProgress) =>
|
||||
Object.values(fileProgress).some(
|
||||
(progress) => progress.progress > 0 && progress.progress < 1
|
||||
)
|
||||
);
|
||||
}, [sendProgress, receiveProgress]);
|
||||
|
||||
// Initialize WebRTC objects and their cleanup
|
||||
useEffect(() => {
|
||||
const webRTCConfig = {
|
||||
@@ -103,6 +118,40 @@ export function useWebRTCConnection({
|
||||
[senderFileTransfer]
|
||||
);
|
||||
|
||||
// Exposed function to broadcast data to all connected sender peers
|
||||
const broadcastDataToAllPeers = useCallback(
|
||||
async (textContent: string, filesToSend: CustomFile[]) => {
|
||||
if (!sender || sender.peerConnections.size === 0) {
|
||||
// The caller (useRoomManager) will handle user message like "waiting for peers"
|
||||
if (developmentEnv)
|
||||
console.warn(
|
||||
"No sender peers to broadcast to, or sender not initialized."
|
||||
);
|
||||
return false; // Indicate failure or no action
|
||||
}
|
||||
if (!senderFileTransfer) {
|
||||
console.error("senderFileTransfer is not initialized for broadcast.");
|
||||
return false;
|
||||
}
|
||||
const peerIds = Array.from(sender.peerConnections.keys());
|
||||
if (developmentEnv)
|
||||
console.log(`Broadcasting to peers: ${peerIds.join(", ")}`);
|
||||
try {
|
||||
await Promise.all(
|
||||
peerIds.map((peerId) =>
|
||||
sendStringAndMetasToPeer(peerId, textContent, filesToSend)
|
||||
)
|
||||
);
|
||||
return true; // Indicate success
|
||||
} catch (error) {
|
||||
console.error("Error broadcasting data to peers:", error);
|
||||
// Optionally use putMessageInMs here for a generic broadcast error
|
||||
return false; // Indicate failure
|
||||
}
|
||||
},
|
||||
[sender, senderFileTransfer, sendStringAndMetasToPeer]
|
||||
);
|
||||
|
||||
// Setup sender and receiver event handlers
|
||||
useEffect(() => {
|
||||
if (sender && senderFileTransfer) {
|
||||
@@ -146,6 +195,10 @@ export function useWebRTCConnection({
|
||||
}
|
||||
);
|
||||
// Example: putMessageInMs(`Connected to a new peer (receiver side). Total: ${receiver.peerConnections.size}`, false);
|
||||
} else if (state === "failed" || state === "disconnected") {
|
||||
if (isAnyFileTransferring) {
|
||||
receiverFileTransfer.gracefulShutdown();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -182,42 +235,32 @@ export function useWebRTCConnection({
|
||||
onStringReceived,
|
||||
onFileMetaReceived,
|
||||
onFileReceived,
|
||||
// messages, putMessageInMs // Removed messages/putMessageInMs if only for console logs for now
|
||||
putMessageInMs,
|
||||
broadcastDataToAllPeers,
|
||||
shareContent,
|
||||
sendFiles,
|
||||
isAnyFileTransferring,
|
||||
]);
|
||||
|
||||
// Exposed function to broadcast data to all connected sender peers
|
||||
const broadcastDataToAllPeers = useCallback(
|
||||
async (textContent: string, filesToSend: CustomFile[]) => {
|
||||
if (!sender || sender.peerConnections.size === 0) {
|
||||
// The caller (useRoomManager) will handle user message like "waiting for peers"
|
||||
if (developmentEnv)
|
||||
console.warn(
|
||||
"No sender peers to broadcast to, or sender not initialized."
|
||||
);
|
||||
return false; // Indicate failure or no action
|
||||
// Effect to handle graceful shutdown on page unload
|
||||
useEffect(() => {
|
||||
const handleBeforeUnload = (e: BeforeUnloadEvent) => {
|
||||
if (isContentPresent || isAnyFileTransferring) {
|
||||
if (isAnyFileTransferring) {
|
||||
receiverFileTransfer?.gracefulShutdown();
|
||||
}
|
||||
// Show the browser's confirmation dialog
|
||||
e.preventDefault();
|
||||
e.returnValue = "";
|
||||
}
|
||||
if (!senderFileTransfer) {
|
||||
console.error("senderFileTransfer is not initialized for broadcast.");
|
||||
return false;
|
||||
}
|
||||
const peerIds = Array.from(sender.peerConnections.keys());
|
||||
if (developmentEnv)
|
||||
console.log(`Broadcasting to peers: ${peerIds.join(", ")}`);
|
||||
try {
|
||||
await Promise.all(
|
||||
peerIds.map((peerId) =>
|
||||
sendStringAndMetasToPeer(peerId, textContent, filesToSend)
|
||||
)
|
||||
);
|
||||
return true; // Indicate success
|
||||
} catch (error) {
|
||||
console.error("Error broadcasting data to peers:", error);
|
||||
// Optionally use putMessageInMs here for a generic broadcast error
|
||||
return false; // Indicate failure
|
||||
}
|
||||
},
|
||||
[sender, senderFileTransfer, sendStringAndMetasToPeer]
|
||||
);
|
||||
};
|
||||
|
||||
window.addEventListener("beforeunload", handleBeforeUnload);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("beforeunload", handleBeforeUnload);
|
||||
};
|
||||
}, [isContentPresent, isAnyFileTransferring, receiverFileTransfer]);
|
||||
|
||||
const requestFile = useCallback(
|
||||
(fileId: string, peerId?: string) => {
|
||||
@@ -268,6 +311,7 @@ export function useWebRTCConnection({
|
||||
retrievePeerCount,
|
||||
sendProgress,
|
||||
receiveProgress,
|
||||
isAnyFileTransferring, // Export the calculated state
|
||||
broadcastDataToAllPeers,
|
||||
requestFile,
|
||||
requestFolder,
|
||||
|
||||
@@ -493,6 +493,23 @@ class FileReceiver {
|
||||
this.webrtcConnection.sendData(confirmation, this.peerId);
|
||||
}
|
||||
// endregion
|
||||
|
||||
public gracefulShutdown(): void {
|
||||
if (this.activeFileReception?.writeStream) {
|
||||
this.log(
|
||||
"log",
|
||||
"Attempting to gracefully close write stream on page unload."
|
||||
);
|
||||
// We don't await this, as beforeunload does not wait for promises.
|
||||
// This is a "best effort" attempt to flush the buffer to disk.
|
||||
this.activeFileReception.writeStream.close().catch((err) => {
|
||||
this.log("error", "Error closing stream during graceful shutdown", {
|
||||
err,
|
||||
});
|
||||
});
|
||||
this.activeFileReception = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default FileReceiver;
|
||||
|
||||
Reference in New Issue
Block a user