Files
david_bai 7a1ab18657 refactor(i18n): stabilize schema and restore locale translations
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.
2026-03-27 17:13:31 +08:00

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;