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:
david_bai
2025-07-19 00:24:44 +08:00
parent 522f567fb4
commit 9ce63992b7
7 changed files with 121 additions and 76 deletions
+17 -2
View File
@@ -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 -28
View File
@@ -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);
}, []);
+78 -34
View File
@@ -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,
+17
View File
@@ -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;