761921684c
- Hook (useConnectionFeedback):
- Add SLOW_RTC_MS=8000 timer when entering negotiating
- Foreground-only; pending while hidden; show once per negotiation attempt
- Clear timers on connect/disconnect; reset attempt flags when leaving negotiating
- Cleanup timers on unmount
- i18n:
- Add required key ClipboardApp.rtc_slow to types
- Provide translations for zh, en, ja, es, de, fr, ko
- Docs:
- flows.zh-CN: add UI connection feedback state machine covering
join_inProgress (immediate), join_slow (3s), join_timeout (15s),
rtc_negotiating, rtc_slow (8s), rtc_connected, rtc_reconnecting, rtc_restored;
document equivalent success signals and visibility gating
- code-map.zh-CN: outline responsibilities/locations for useRoomManager (join slow/timeout)
and useConnectionFeedback (negotiation slow, reconnect/restored)
277 lines
9.1 KiB
TypeScript
277 lines
9.1 KiB
TypeScript
"use client";
|
|
import React, { 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";
|
|
import { useFileTransferStore } from "@/stores/fileTransferStore";
|
|
import { getCachedId } from "@/lib/roomIdCache";
|
|
import { useConnectionFeedback } from "@/hooks/useConnectionFeedback";
|
|
|
|
const ClipboardApp = () => {
|
|
const { shareMessage, retrieveMessage, putMessageInMs } =
|
|
useClipboardAppMessages();
|
|
|
|
const dragCounter = useRef(0);
|
|
const retrieveJoinRoomBtnRef = useRef<HTMLButtonElement>(null);
|
|
|
|
const { messages, isLoadingMessages } = 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({ messages, putMessageInMs });
|
|
|
|
// Simplified WebRTC connection initialization
|
|
const {
|
|
requestFile,
|
|
requestFolder,
|
|
setReceiverDirectoryHandle,
|
|
getReceiverSaveType,
|
|
} = useWebRTCConnection({
|
|
messages,
|
|
putMessageInMs,
|
|
});
|
|
|
|
// Greatly simplified room management - No longer need to pass any WebRTC dependencies
|
|
const {
|
|
processRoomIdInput,
|
|
joinRoom,
|
|
generateShareLinkAndBroadcast,
|
|
handleLeaveReceiverRoom,
|
|
handleLeaveSenderRoom,
|
|
} = useRoomManager({
|
|
messages,
|
|
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({ messages, putMessageInMs });
|
|
|
|
if (isLoadingMessages || !messages) {
|
|
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="w-full mx-auto px-1 sm:px-1 py-3 sm:py-8 md:max-w-4xl md:container">
|
|
<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-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"
|
|
? messages.text.ClipboardApp.html.shareTitle_dis
|
|
: messages.text.ClipboardApp.html.retrieveTitle_dis}
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<CardContent className="px-3 sm:px-6">
|
|
{activeTab === "send" ? (
|
|
<SendTabPanel
|
|
messages={messages}
|
|
updateShareContent={updateShareContent}
|
|
addFilesToSend={addFilesToSend}
|
|
removeFileToSend={removeFileToSend}
|
|
richTextToPlainText={richTextToPlainText}
|
|
processRoomIdInput={processRoomIdInput}
|
|
joinRoom={joinRoom}
|
|
generateShareLinkAndBroadcast={generateShareLinkAndBroadcast}
|
|
shareMessage={shareMessage}
|
|
currentValidatedShareRoomId={shareRoomId}
|
|
handleLeaveSenderRoom={handleLeaveSenderRoom}
|
|
putMessageInMs={putMessageInMs}
|
|
/>
|
|
) : (
|
|
<RetrieveTabPanel
|
|
messages={messages}
|
|
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 && messages && (
|
|
<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">
|
|
{messages.text.ClipboardApp.html.RetrieveMethodTitle}
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<CardContent className="pt-0 px-3 sm:px-6">
|
|
<QRCodeComponent RoomID={shareRoomId} shareLink={shareLink} />
|
|
</CardContent>
|
|
</Card>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default ClipboardApp;
|