291 lines
9.6 KiB
TypeScript
291 lines
9.6 KiB
TypeScript
"use client";
|
|
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";
|
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
|
import { useClipboardAppMessages } from "@/hooks/useClipboardAppMessages";
|
|
import { usePageSetup } from "@/hooks/usePageSetup";
|
|
import { useRoomManager } from "@/hooks/useRoomManager";
|
|
import { useWebRTCConnection } from "@/hooks/useWebRTCConnection";
|
|
import { useFileTransferHandler } from "@/hooks/useFileTransferHandler";
|
|
import { SendTabPanel } from "./ClipboardApp/SendTabPanel";
|
|
import { RetrieveTabPanel } from "./ClipboardApp/RetrieveTabPanel";
|
|
import FullScreenDropZone from "./ClipboardApp/FullScreenDropZone";
|
|
import { traverseFileTree } from "@/lib/fileUtils";
|
|
|
|
const ClipboardApp = () => {
|
|
const { shareMessage, retrieveMessage, putMessageInMs } =
|
|
useClipboardAppMessages();
|
|
|
|
const [retrieveRoomIdInput, setRetrieveRoomIdInput] = useState("");
|
|
const [activeTab, setActiveTab] = useState<"send" | "retrieve">("send");
|
|
const [isDragging, setIsDragging] = useState(false);
|
|
const dragCounter = useRef(0);
|
|
const retrieveJoinRoomBtnRef = useRef<HTMLButtonElement>(null); // Ref for the receiver's "Join Room" button
|
|
|
|
const { messages, isLoadingMessages } = usePageSetup({
|
|
setRetrieveRoomId: setRetrieveRoomIdInput,
|
|
setActiveTab,
|
|
retrieveJoinRoomBtnRef,
|
|
});
|
|
|
|
const richTextToPlainText = useRichTextToPlainText();
|
|
|
|
// Initialize File Transfer Handler Hook
|
|
const {
|
|
shareContent,
|
|
sendFiles,
|
|
retrievedContent,
|
|
retrievedFileMetas,
|
|
updateShareContent,
|
|
addFilesToSend,
|
|
removeFileToSend,
|
|
onStringDataReceived,
|
|
onFileMetadataReceived,
|
|
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,
|
|
receiver,
|
|
sharePeerCount,
|
|
retrievePeerCount,
|
|
sendProgress,
|
|
receiveProgress,
|
|
isAnyFileTransferring,
|
|
broadcastDataToAllPeers,
|
|
requestFile,
|
|
requestFolder,
|
|
setReceiverDirectoryHandle,
|
|
getReceiverSaveType,
|
|
} = useWebRTCConnection({
|
|
shareContent,
|
|
sendFiles,
|
|
isContentPresent,
|
|
messages,
|
|
putMessageInMs,
|
|
onStringReceived: onStringDataReceived,
|
|
onFileMetaReceived: onFileMetadataReceived,
|
|
onFileReceived: onFileFullyReceived,
|
|
});
|
|
|
|
// Initialize Room Manager Hook
|
|
const {
|
|
shareRoomId,
|
|
initShareRoomId,
|
|
shareLink,
|
|
shareRoomStatusText,
|
|
retrieveRoomStatusText,
|
|
processRoomIdInput,
|
|
joinRoom,
|
|
generateShareLinkAndBroadcast,
|
|
} = useRoomManager({
|
|
messages,
|
|
putMessageInMs,
|
|
sender,
|
|
receiver,
|
|
activeTab,
|
|
sharePeerCount,
|
|
retrievePeerCount,
|
|
// Pass the actual broadcast function from useWebRTCConnection
|
|
broadcastDataToPeers: () =>
|
|
broadcastDataToAllPeers(shareContent, sendFiles),
|
|
});
|
|
|
|
const handleFileDrop = useCallback(
|
|
(items: DataTransferItemList) => {
|
|
if (activeTab !== "send") return;
|
|
const itemsArray = Array.from(items);
|
|
Promise.all(
|
|
itemsArray.map((item) => {
|
|
const entry = item.webkitGetAsEntry();
|
|
if (entry) {
|
|
return traverseFileTree(entry);
|
|
}
|
|
return Promise.resolve([]);
|
|
})
|
|
).then((results) => {
|
|
const allFiles = results.flat();
|
|
if (allFiles.length > 0) {
|
|
addFilesToSend(allFiles);
|
|
}
|
|
});
|
|
},
|
|
[activeTab, addFilesToSend]
|
|
);
|
|
|
|
useEffect(() => {
|
|
const handleDragEnter = (e: DragEvent) => {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
if (activeTab !== "send") return;
|
|
dragCounter.current++;
|
|
if (e.dataTransfer?.items && e.dataTransfer.items.length > 0) {
|
|
setIsDragging(true);
|
|
}
|
|
};
|
|
|
|
const handleDragLeave = (e: DragEvent) => {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
if (activeTab !== "send") return;
|
|
dragCounter.current--;
|
|
if (dragCounter.current === 0) {
|
|
setIsDragging(false);
|
|
}
|
|
};
|
|
|
|
const handleDragOver = (e: DragEvent) => {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
};
|
|
|
|
const handleDrop = (e: DragEvent) => {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
if (activeTab !== "send") return;
|
|
if (e.dataTransfer?.items) {
|
|
handleFileDrop(e.dataTransfer.items);
|
|
}
|
|
dragCounter.current = 0;
|
|
setIsDragging(false);
|
|
};
|
|
|
|
window.addEventListener("dragenter", handleDragEnter);
|
|
window.addEventListener("dragleave", handleDragLeave);
|
|
window.addEventListener("dragover", handleDragOver);
|
|
window.addEventListener("drop", handleDrop);
|
|
|
|
return () => {
|
|
window.removeEventListener("dragenter", handleDragEnter);
|
|
window.removeEventListener("dragleave", handleDragLeave);
|
|
window.removeEventListener("dragover", handleDragOver);
|
|
window.removeEventListener("drop", handleDrop);
|
|
};
|
|
}, [activeTab, handleFileDrop]);
|
|
|
|
if (isLoadingMessages || !messages) {
|
|
// Use a skeleton screen placeholder to replace the simple text loading prompt.
|
|
// The height of this placeholder is similar to the height of the component that is finally loaded,
|
|
// This prevents layout displacement and ensures that the lazy loading component below will not be triggered prematurely.
|
|
return (
|
|
<div className="container mx-auto px-4 py-8 w-full md:max-w-4xl">
|
|
<div className="min-h-[1000px] w-full bg-gray-200/50 dark:bg-gray-800/50 rounded-lg animate-pulse">
|
|
{" "}
|
|
Loading Editor...{" "}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="container mx-auto px-4 py-8 w-full md:max-w-4xl">
|
|
<FullScreenDropZone isDragging={isDragging} messages={messages} />
|
|
<div className="flex flex-col md:flex-row space-y-2 md:space-y-0 md:space-x-2 mb-4">
|
|
<Button
|
|
variant={activeTab === "send" ? "default" : "outline"}
|
|
onClick={() => setActiveTab("send")}
|
|
className="flex-1"
|
|
aria-controls="send-panel"
|
|
id="send-tab"
|
|
aria-selected={activeTab === "send"}
|
|
>
|
|
{messages.text.ClipboardApp.html.senderTab}
|
|
</Button>
|
|
<Button
|
|
variant={activeTab === "retrieve" ? "default" : "outline"}
|
|
onClick={() => setActiveTab("retrieve")}
|
|
className="flex-1"
|
|
aria-controls="retrieve-panel"
|
|
id="retrieve-tab"
|
|
aria-selected={activeTab === "retrieve"}
|
|
>
|
|
{messages.text.ClipboardApp.html.retrieveTab}
|
|
</Button>
|
|
</div>
|
|
<Card className="border-8 shadow-md">
|
|
<CardHeader>
|
|
<CardTitle>
|
|
{activeTab === "send"
|
|
? messages.text.ClipboardApp.html.shareTitle_dis
|
|
: messages.text.ClipboardApp.html.retrieveTitle_dis}
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
{activeTab === "send" ? (
|
|
<SendTabPanel
|
|
messages={messages}
|
|
shareRoomStatusText={shareRoomStatusText}
|
|
shareContent={shareContent}
|
|
sendFiles={sendFiles}
|
|
updateShareContent={updateShareContent}
|
|
addFilesToSend={addFilesToSend}
|
|
removeFileToSend={removeFileToSend}
|
|
richTextToPlainText={richTextToPlainText}
|
|
sendProgress={sendProgress}
|
|
isAnyFileTransferring={isAnyFileTransferring}
|
|
processRoomIdInput={processRoomIdInput}
|
|
joinRoom={joinRoom}
|
|
generateShareLinkAndBroadcast={generateShareLinkAndBroadcast}
|
|
sender={sender}
|
|
shareMessage={shareMessage}
|
|
currentValidatedShareRoomId={shareRoomId}
|
|
/>
|
|
) : (
|
|
<RetrieveTabPanel
|
|
messages={messages}
|
|
putMessageInMs={putMessageInMs} // Needed for onLocationPick
|
|
retrieveRoomStatusText={retrieveRoomStatusText}
|
|
retrieveRoomIdInput={retrieveRoomIdInput}
|
|
setRetrieveRoomIdInput={setRetrieveRoomIdInput}
|
|
joinRoom={joinRoom}
|
|
retrieveJoinRoomBtnRef={retrieveJoinRoomBtnRef}
|
|
receiver={receiver}
|
|
retrievedContent={retrievedContent}
|
|
richTextToPlainText={richTextToPlainText}
|
|
retrievedFileMetas={retrievedFileMetas}
|
|
receiveProgress={receiveProgress}
|
|
isAnyFileTransferring={isAnyFileTransferring}
|
|
handleDownloadFile={handleDownloadFile}
|
|
// Pass WebRTC interaction methods
|
|
requestFile={requestFile}
|
|
requestFolder={requestFolder}
|
|
setReceiverDirectoryHandle={setReceiverDirectoryHandle}
|
|
getReceiverSaveType={getReceiverSaveType}
|
|
retrieveMessage={retrieveMessage}
|
|
/>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
{activeTab === "send" && shareLink && messages && (
|
|
<Card className="border-2 shadow-md mt-4">
|
|
<CardHeader>
|
|
<CardTitle>
|
|
{messages.text.ClipboardApp.html.RetrieveMethodTitle}
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<QRCodeComponent RoomID={shareRoomId} shareLink={shareLink} />
|
|
</CardContent>
|
|
</Card>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default ClipboardApp;
|