Files
PrivyDrop/frontend/components/ClipboardApp/SendTabPanel.tsx
T
2025-09-13 20:01:02 +08:00

231 lines
8.2 KiB
TypeScript

import React, { useState, useEffect, useCallback } from "react";
import dynamic from "next/dynamic";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import {
ReadClipboardButton,
WriteClipboardButton,
} from "@/components/common/clipboard_btn";
import { FileUploadHandler } from "@/components/ClipboardApp/FileUploadHandler";
import FileListDisplay from "@/components/ClipboardApp/FileListDisplay";
import AnimatedButton from "@/components/ui/AnimatedButton";
import type { Messages } from "@/types/messages";
import type { CustomFile, FileMeta } from "@/types/webrtc";
import { useFileTransferStore } from "@/stores/fileTransferStore";
// Dynamically import the RichTextEditor
const RichTextEditor = dynamic(
() => import("@/components/Editor/RichTextEditor"),
{
ssr: false, // This component is client-side only
loading: () => (
<div className="p-4 border rounded-lg min-h-[200px] md:min-h-[400px] bg-gray-50 flex items-center justify-center">
Loading Editor...
</div>
),
}
);
interface SendTabPanelProps {
messages: Messages;
updateShareContent: (content: string) => void;
addFilesToSend: (files: CustomFile[]) => void;
removeFileToSend: (meta: FileMeta) => void;
richTextToPlainText: (html: string) => string;
processRoomIdInput: (roomId: string) => void; // Passed from useRoomManager
joinRoom: (isSender: boolean, roomId: string) => void;
generateShareLinkAndBroadcast: () => void;
shareMessage: string;
currentValidatedShareRoomId: string;
handleLeaveSenderRoom: () => void; // New prop for leaving room
}
export function SendTabPanel({
messages,
updateShareContent,
addFilesToSend,
removeFileToSend,
richTextToPlainText,
processRoomIdInput,
joinRoom,
generateShareLinkAndBroadcast,
shareMessage,
currentValidatedShareRoomId,
handleLeaveSenderRoom,
}: SendTabPanelProps) {
// Get the status from the store
const {
shareContent,
sendFiles,
shareRoomStatusText,
sendProgress,
isAnyFileTransferring,
isSenderInRoom,
} = useFileTransferStore();
// Local state for immediate response in the input field
const [inputFieldValue, setInputFieldValue] = useState<string>(
currentValidatedShareRoomId
);
// State to track ID generation mode (false = will show simple next, true = will show random next)
const [isSimpleIdMode, setIsSimpleIdMode] = useState<boolean>(true);
// When the validatedShareRoomId from the parent component changes (e.g., after initial fetch), synchronize the local input field's value
useEffect(() => {
setInputFieldValue(currentValidatedShareRoomId);
}, [currentValidatedShareRoomId]);
const handleInputChange = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
const newValue = e.target.value;
setInputFieldValue(newValue); // Immediately update the input field display
processRoomIdInput(newValue); // Call the handler function from the hook, which will perform debounced validation
},
[processRoomIdInput]
);
const handlePaste = useCallback(
(e: React.ClipboardEvent<HTMLInputElement>) => {
const pastedText = e.clipboardData.getData("text").trim();
setInputFieldValue(pastedText);
processRoomIdInput(pastedText);
},
[processRoomIdInput]
);
// Handle ID generation toggle
const handleIdGeneration = useCallback(async () => {
if (isSimpleIdMode) {
// Generate random UUID
const randomId = crypto.randomUUID();
processRoomIdInput(randomId);
} else {
// Generate simple 4-digit ID by calling backend API
try {
const { fetchRoom } = await import("@/app/config/api");
const simpleRoomId = await fetchRoom();
if (simpleRoomId) {
// fetchRoom() already created the room, so set it as initial room ID
// This prevents joinRoom() from trying to create it again
setInputFieldValue(simpleRoomId);
const { useFileTransferStore } = await import(
"@/stores/fileTransferStore"
);
const store = useFileTransferStore.getState();
store.setShareRoomId(simpleRoomId);
// IMPORTANT: Set as initial room ID to prevent duplicate creation
store.setInitShareRoomId(simpleRoomId);
} else {
processRoomIdInput(crypto.randomUUID());
}
} catch (error) {
processRoomIdInput(crypto.randomUUID());
}
}
// Toggle mode for next click
setIsSimpleIdMode(!isSimpleIdMode);
}, [isSimpleIdMode, processRoomIdInput, setInputFieldValue]);
return (
<div id="send-panel" role="tabpanel" aria-labelledby="send-tab">
<div className="mb-3 text-sm text-gray-600">
{shareRoomStatusText ||
(isSenderInRoom
? messages.text.ClipboardApp.roomStatus.onlyOneMsg
: messages.text.ClipboardApp.roomStatus.senderEmptyMsg)}
</div>
<RichTextEditor value={shareContent} onChange={updateShareContent} />
<div className="flex flex-col sm:flex-row gap-2 my-3">
<ReadClipboardButton
title={messages.text.ClipboardApp.html.Paste_dis}
onRead={updateShareContent}
/>
<WriteClipboardButton
title={messages.text.ClipboardApp.html.Copy_dis}
textToCopy={richTextToPlainText(shareContent)}
/>
</div>
<div className="mb-3">
<FileUploadHandler onFilePicked={addFilesToSend} />
<FileListDisplay
mode="sender"
files={sendFiles}
fileProgresses={sendProgress}
isAnyFileTransferring={isAnyFileTransferring}
onDelete={removeFileToSend}
/>
</div>
<div className="space-y-3 mb-4">
{/* Room ID input section */}
<div className="space-y-2">
<p className="text-sm text-gray-600">
{messages.text.ClipboardApp.html.inputRoomId_tips}
</p>
<div className="flex flex-col sm:flex-row gap-2">
<Input
aria-label="Share Room ID"
value={inputFieldValue}
onChange={handleInputChange}
onPaste={handlePaste}
className="flex-1 min-w-0"
placeholder={
messages.text.ClipboardApp.html.retrieveRoomId_placeholder
}
/>
<Button
variant="outline"
className="w-full sm:w-auto px-4"
onClick={handleIdGeneration}
disabled={isSenderInRoom}
>
{isSimpleIdMode
? messages.text.ClipboardApp.html.generateRandomId_tips
: messages.text.ClipboardApp.html.generateSimpleId_tips}
</Button>
<Button
className="w-full sm:w-auto px-4"
onClick={() => joinRoom(true, inputFieldValue.trim())}
disabled={isSenderInRoom || !inputFieldValue.trim()}
>
{messages.text.ClipboardApp.html.joinRoom_dis}
</Button>
</div>
</div>
{/* Action buttons */}
<div className="flex flex-col sm:flex-row gap-2">
<AnimatedButton
className="flex-1 order-1"
onClick={generateShareLinkAndBroadcast}
loadingText={
messages.text.ClipboardApp.html.SyncSending_loadingText
}
disabled={
!isSenderInRoom ||
(sendFiles.length === 0 && shareContent.trim() === "") ||
!currentValidatedShareRoomId.trim() ||
isAnyFileTransferring
}
>
{messages.text.ClipboardApp.html.SyncSending_dis}
</AnimatedButton>
<Button
variant={isAnyFileTransferring ? "destructive" : "outline"}
onClick={handleLeaveSenderRoom}
disabled={!isSenderInRoom}
className="w-full sm:w-auto px-4 order-2"
>
{isAnyFileTransferring
? messages.text.ClipboardApp.roomStatus.leaveRoomBtn + " ⚠️"
: messages.text.ClipboardApp.roomStatus.leaveRoomBtn}
</Button>
</div>
</div>
{shareMessage && (
<p className="mt-3 text-sm text-blue-600">{shareMessage}</p>
)}
</div>
);
}