7a1ab18657
Align the next-intl message schema across components, hooks, and locale files so the frontend uses one canonical structure instead of compile-first workarounds. Restore Spanish, French, German, Japanese, and Korean translations to the new schema while narrowing clipboard hook dependencies to translation contracts.
292 lines
9.9 KiB
TypeScript
292 lines
9.9 KiB
TypeScript
"use client";
|
|
import React, { useRef, useCallback, useEffect, useMemo } from "react";
|
|
import { useMessages, useTranslations } from "next-intl";
|
|
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";
|
|
import { useFileTransferStore } from "@/stores/fileTransferStore";
|
|
import { getCachedId } from "@/lib/roomIdCache";
|
|
import { useConnectionFeedback } from "@/hooks/useConnectionFeedback";
|
|
import type { Messages } from "@/types/messages";
|
|
|
|
const ClipboardApp = () => {
|
|
const messages = useMessages() as Messages;
|
|
const tTabs = useTranslations("text.clipboard.tabs");
|
|
const tTitles = useTranslations("text.clipboard.titles");
|
|
const { shareMessage, retrieveMessage, putMessageInMs } =
|
|
useClipboardAppMessages();
|
|
const roomText = messages.text.clipboard;
|
|
const fileTransferText = messages.text.clipboard.messages;
|
|
const connectionText = messages.text.clipboard.rtc;
|
|
|
|
const dragCounter = useRef(0);
|
|
const retrieveJoinRoomBtnRef = useRef<HTMLButtonElement>(null);
|
|
|
|
usePageSetup({
|
|
setRetrieveRoomId: useFileTransferStore.getState().setRetrieveRoomIdInput,
|
|
setActiveTab: useFileTransferStore.getState().setActiveTab,
|
|
retrieveJoinRoomBtnRef,
|
|
});
|
|
|
|
// Get state from store
|
|
const {
|
|
activeTab,
|
|
isDragging,
|
|
shareRoomId,
|
|
shareLink,
|
|
setIsDragging,
|
|
setRetrieveRoomIdInput,
|
|
setActiveTab,
|
|
// for auto-join on receiver side
|
|
isReceiverInRoom,
|
|
retrieveRoomIdInput,
|
|
} = useFileTransferStore();
|
|
|
|
const richTextToPlainText = useRichTextToPlainText();
|
|
|
|
// Initialize File Transfer Handler Hook
|
|
const {
|
|
updateShareContent,
|
|
addFilesToSend,
|
|
removeFileToSend,
|
|
handleDownloadFile,
|
|
} = useFileTransferHandler({ text: fileTransferText, putMessageInMs });
|
|
|
|
// Simplified WebRTC connection initialization
|
|
const {
|
|
requestFile,
|
|
requestFolder,
|
|
setReceiverDirectoryHandle,
|
|
getReceiverSaveType,
|
|
} = useWebRTCConnection({
|
|
putMessageInMs,
|
|
});
|
|
|
|
// Greatly simplified room management - No longer need to pass any WebRTC dependencies
|
|
const {
|
|
processRoomIdInput,
|
|
joinRoom,
|
|
generateShareLinkAndBroadcast,
|
|
handleLeaveReceiverRoom,
|
|
handleLeaveSenderRoom,
|
|
} = useRoomManager({
|
|
text: {
|
|
join: roomText.join,
|
|
messages: {
|
|
waiting: roomText.messages.waiting,
|
|
confirmLeave: roomText.messages.confirmLeave,
|
|
leaveSuccess: roomText.messages.leaveSuccess,
|
|
fetchRoomError: roomText.messages.fetchRoomError,
|
|
generateShareLinkError: roomText.messages.generateShareLinkError,
|
|
leaveRoomError: roomText.messages.leaveRoomError,
|
|
validateRoomError: roomText.messages.validateRoomError,
|
|
resetSenderStateError: roomText.messages.resetSenderStateError,
|
|
},
|
|
roomCheck: roomText.roomCheck,
|
|
status: {
|
|
roomEmpty: roomText.status.roomEmpty,
|
|
receiverCanAccept: roomText.status.receiverCanAccept,
|
|
onlyOne: roomText.status.onlyOne,
|
|
peopleCount: roomText.status.peopleCount,
|
|
connected: roomText.status.connected,
|
|
leftRoom: roomText.status.leftRoom,
|
|
},
|
|
},
|
|
putMessageInMs,
|
|
});
|
|
|
|
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, setIsDragging]);
|
|
|
|
// Auto-join on switching to receiver tab when cached ID exists
|
|
useEffect(() => {
|
|
if (activeTab !== "retrieve") return;
|
|
if (isReceiverInRoom) return;
|
|
|
|
// Do not auto-join if URL already specifies a roomId (URL 优先)
|
|
const params = new URLSearchParams(window.location.search);
|
|
if (params.get("roomId")) return;
|
|
|
|
// Do not override user's existing input
|
|
if ((retrieveRoomIdInput || "").trim().length > 0) return;
|
|
|
|
const cached = getCachedId();
|
|
if (!cached || cached.trim().length === 0) return;
|
|
|
|
// Fill input then join directly to improve UX
|
|
setRetrieveRoomIdInput(cached);
|
|
joinRoom(false, cached);
|
|
}, [
|
|
activeTab,
|
|
isReceiverInRoom,
|
|
retrieveRoomIdInput,
|
|
setRetrieveRoomIdInput,
|
|
joinRoom,
|
|
]);
|
|
|
|
// Connection feedback observer (Hook)
|
|
useConnectionFeedback({ text: connectionText, putMessageInMs });
|
|
|
|
return (
|
|
<div className="w-full mx-auto px-1 sm:px-1 py-3 sm:py-8 md:max-w-4xl md:container">
|
|
<FullScreenDropZone isDragging={isDragging} />
|
|
<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"}
|
|
>
|
|
{tTabs("send")}
|
|
</Button>
|
|
<Button
|
|
variant={activeTab === "retrieve" ? "default" : "outline"}
|
|
onClick={() => setActiveTab("retrieve")}
|
|
className="flex-1"
|
|
aria-controls="retrieve-panel"
|
|
id="retrieve-tab"
|
|
aria-selected={activeTab === "retrieve"}
|
|
>
|
|
{tTabs("retrieve")}
|
|
</Button>
|
|
</div>
|
|
<Card className="border-4 sm:border-8 shadow-md">
|
|
<CardHeader className="px-3 sm:px-6 py-3 sm:py-6">
|
|
<CardTitle className="text-lg sm:text-xl">
|
|
{activeTab === "send"
|
|
? tTitles("share")
|
|
: tTitles("retrieve")}
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<CardContent className="px-3 sm:px-6">
|
|
{activeTab === "send" ? (
|
|
<SendTabPanel
|
|
updateShareContent={updateShareContent}
|
|
addFilesToSend={addFilesToSend}
|
|
removeFileToSend={removeFileToSend}
|
|
richTextToPlainText={richTextToPlainText}
|
|
processRoomIdInput={processRoomIdInput}
|
|
joinRoom={joinRoom}
|
|
generateShareLinkAndBroadcast={generateShareLinkAndBroadcast}
|
|
shareMessage={shareMessage}
|
|
currentValidatedShareRoomId={shareRoomId}
|
|
handleLeaveSenderRoom={handleLeaveSenderRoom}
|
|
putMessageInMs={putMessageInMs}
|
|
/>
|
|
) : (
|
|
<RetrieveTabPanel
|
|
putMessageInMs={putMessageInMs}
|
|
setRetrieveRoomIdInput={setRetrieveRoomIdInput}
|
|
joinRoom={joinRoom}
|
|
retrieveJoinRoomBtnRef={retrieveJoinRoomBtnRef}
|
|
richTextToPlainText={richTextToPlainText}
|
|
handleDownloadFile={handleDownloadFile}
|
|
requestFile={requestFile}
|
|
requestFolder={requestFolder}
|
|
setReceiverDirectoryHandle={setReceiverDirectoryHandle}
|
|
getReceiverSaveType={getReceiverSaveType}
|
|
retrieveMessage={retrieveMessage}
|
|
handleLeaveRoom={handleLeaveReceiverRoom}
|
|
/>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
{activeTab === "send" && shareLink && (
|
|
<Card className="border-2 sm:border-4 shadow-md mt-2 sm:mt-4">
|
|
<CardHeader className="pb-3 sm:pb-6">
|
|
<CardTitle className="text-base sm:text-lg">
|
|
{tTitles("retrieveMethod")}
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<CardContent className="pt-0 px-3 sm:px-6">
|
|
<QRCodeComponent RoomID={shareRoomId} shareLink={shareLink} />
|
|
</CardContent>
|
|
</Card>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default ClipboardApp;
|