chore(code):"Use Cache ID" button double-click to temporarily switch to "Save ID" function

This commit is contained in:
david_bai
2025-10-23 22:45:57 +08:00
parent 5ca89d71ad
commit b2aa493e2d
3 changed files with 205 additions and 100 deletions
@@ -0,0 +1,189 @@
import React, { useCallback, useEffect, useRef, useState } from "react";
import { Button } from "@/components/ui/button";
import Tooltip from "@/components/Tooltip";
import type { Messages } from "@/types/messages";
import { getCachedId, setCachedId } from "@/lib/roomIdCache";
/**
* CachedIdActionButton
*
* A reusable action button that unifies the "Use cached ID" and "Save ID" behaviors
* across sender and receiver panels.
*
* UX
* - If a cached Room ID exists:
* - Single click (no second click within dblClickWindowMs, default 400ms):
* writes the cached ID into the target input without availability checks
* (matching the current Random ID UX).
* - Double click (two clicks within dblClickWindowMs): switches to a temporary
* "Save ID" mode for saveModeDurationMs (default 3000ms) without filling.
* - If no cached Room ID exists: the button shows "Save ID" by default; when the
* current input length >= 8, clicking saves it to localStorage and reports success
* via putMessageInMs, then the button returns to "Use cached ID".
* - In "Save ID" mode: clicking saves the current input (>= 8) and exits the mode;
* if the user does nothing, the mode auto-exits after saveModeDurationMs.
*
* Props
* - messages: i18n dictionary used for labels/tooltips.
* - getInputValue / setInputValue: provide read/write access to the room ID input.
* - putMessageInMs: message dispatcher; isShareEnd tells which side (sender/receiver)
* should display the toast.
* - Optional styling/timing overrides: className, variant, size, dblClickWindowMs,
* saveModeDurationMs — with sensible defaults for dropin usage.
*
* Implementation
* - Local state tracks if a cached ID exists and whether we are in temporary
* "save override" mode.
* - Single/double click detection uses a short timer + click counter refs;
* timers are cleaned up on unmount to avoid leaks.
* - localStorage reads/writes are abstracted via getCachedId/setCachedId.
* - No network calls, and no availability checks during "Use cached ID" to keep
* the interaction snappy and consistent with Random ID behavior.
*/
type Props = {
messages: Messages;
getInputValue: () => string;
setInputValue: (val: string) => void;
putMessageInMs: (
message: string,
isShareEnd?: boolean,
displayTimeMs?: number
) => void;
isShareEnd: boolean; // true for sender, false for receiver
className?: string;
variant?:
| "default"
| "destructive"
| "outline"
| "secondary"
| "ghost"
| "link";
size?: "default" | "sm" | "lg" | "icon";
dblClickWindowMs?: number; // default 400ms
saveModeDurationMs?: number; // default 3000ms
};
export default function CachedIdActionButton({
messages,
getInputValue,
setInputValue,
putMessageInMs,
isShareEnd,
className = "w-full sm:w-auto px-4",
variant = "outline",
size = "default",
dblClickWindowMs = 400,
saveModeDurationMs = 3000,
}: Props) {
const [hasCachedId, setHasCachedId] = useState<boolean>(false);
const [showSaveOverride, setShowSaveOverride] = useState<boolean>(false);
const clickCountRef = useRef(0);
const singleTimerRef = useRef<number | null>(null);
const saveTimerRef = useRef<number | null>(null);
useEffect(() => {
setHasCachedId(!!getCachedId());
}, []);
useEffect(() => {
return () => {
if (singleTimerRef.current) {
clearTimeout(singleTimerRef.current);
singleTimerRef.current = null;
}
if (saveTimerRef.current) {
clearTimeout(saveTimerRef.current);
saveTimerRef.current = null;
}
};
}, []);
const isSaveMode = showSaveOverride || !hasCachedId;
const inputVal = getInputValue() || "";
const isSaveEnabled = inputVal.trim().length >= 8;
const handleClick = useCallback(() => {
if (isSaveMode) {
const trimmed = (getInputValue() || "").trim();
if (trimmed.length >= 8) {
setCachedId(trimmed);
setHasCachedId(true);
setShowSaveOverride(false);
if (saveTimerRef.current) {
clearTimeout(saveTimerRef.current);
saveTimerRef.current = null;
}
putMessageInMs(messages.text.ClipboardApp.saveId_success, isShareEnd);
}
return;
}
// Use cached with single/double click detection
clickCountRef.current += 1;
if (clickCountRef.current === 1) {
// Single click timer
singleTimerRef.current = window.setTimeout(() => {
if (clickCountRef.current === 1) {
const cached = getCachedId();
if (cached) {
setInputValue(cached);
}
}
clickCountRef.current = 0;
if (singleTimerRef.current) {
clearTimeout(singleTimerRef.current);
singleTimerRef.current = null;
}
}, dblClickWindowMs);
} else if (clickCountRef.current === 2) {
// Double click => switch to save mode
if (singleTimerRef.current) {
clearTimeout(singleTimerRef.current);
singleTimerRef.current = null;
}
clickCountRef.current = 0;
setShowSaveOverride(true);
saveTimerRef.current = window.setTimeout(() => {
setShowSaveOverride(false);
if (saveTimerRef.current) {
clearTimeout(saveTimerRef.current);
saveTimerRef.current = null;
}
}, saveModeDurationMs);
}
}, [
isSaveMode,
getInputValue,
setInputValue,
putMessageInMs,
messages.text.ClipboardApp.saveId_success,
isShareEnd,
dblClickWindowMs,
saveModeDurationMs,
]);
return (
<Tooltip
content={
isSaveMode
? messages.text.ClipboardApp.html.saveId_tips
: messages.text.ClipboardApp.html.useCachedId_tips
}
>
<span className="inline-block">
<Button
className={className}
variant={variant}
size={size}
onClick={handleClick}
disabled={isSaveMode ? !isSaveEnabled : !hasCachedId}
>
{isSaveMode
? messages.text.ClipboardApp.html.saveId_dis
: messages.text.ClipboardApp.html.useCachedId_dis}
</Button>
</span>
</Tooltip>
);
}
@@ -5,13 +5,12 @@ import {
ReadClipboardButton,
WriteClipboardButton,
} from "@/components/common/clipboard_btn";
import Tooltip from "@/components/Tooltip";
import CachedIdActionButton from "@/components/ClipboardApp/CachedIdActionButton";
import FileListDisplay from "@/components/ClipboardApp/FileListDisplay";
import type { Messages } from "@/types/messages";
import type { FileMeta } from "@/types/webrtc";
import { useFileTransferStore } from "@/stores/fileTransferStore";
import { getCachedId, setCachedId } from "@/lib/roomIdCache";
interface RetrieveTabPanelProps {
messages: Messages;
@@ -62,12 +61,6 @@ export function RetrieveTabPanel({
isReceiverInRoom,
} = useFileTransferStore();
// Cached ID state
const [hasCachedId, setHasCachedId] = useState<boolean>(false);
useEffect(() => {
setHasCachedId(!!getCachedId());
}, []);
const onLocationPick = useCallback(async (): Promise<boolean> => {
if (!messages) return false; // Should not happen if panel is rendered
if (!window.showDirectoryPicker) {
@@ -120,45 +113,13 @@ export function RetrieveTabPanel({
onRead={setRetrieveRoomIdInput}
/>
{/* Save/Use Cached ID Button placed after Paste button */}
<Tooltip
content={
hasCachedId
? messages.text.ClipboardApp.html.useCachedId_tips
: messages.text.ClipboardApp.html.saveId_tips
}
>
<span className="inline-block">
<Button
className="w-full sm:w-auto px-4"
variant="outline"
onClick={() => {
if (hasCachedId) {
const cached = getCachedId();
if (cached) {
setRetrieveRoomIdInput(cached);
}
} else {
const trimmed = retrieveRoomIdInput.trim();
if (trimmed.length >= 8) {
setCachedId(trimmed);
setHasCachedId(true);
putMessageInMs(
messages.text.ClipboardApp.saveId_success,
false
);
}
}
}}
disabled={
!hasCachedId && retrieveRoomIdInput.trim().length < 8
}
>
{hasCachedId
? messages.text.ClipboardApp.html.useCachedId_dis
: messages.text.ClipboardApp.html.saveId_dis}
</Button>
</span>
</Tooltip>
<CachedIdActionButton
messages={messages}
getInputValue={() => retrieveRoomIdInput}
setInputValue={setRetrieveRoomIdInput}
putMessageInMs={putMessageInMs}
isShareEnd={false}
/>
<Input
aria-label="Retrieve Room ID"
value={retrieveRoomIdInput}
@@ -2,7 +2,7 @@ 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 Tooltip from "@/components/Tooltip";
import CachedIdActionButton from "@/components/ClipboardApp/CachedIdActionButton";
import {
ReadClipboardButton,
WriteClipboardButton,
@@ -14,7 +14,6 @@ import type { Messages } from "@/types/messages";
import type { CustomFile, FileMeta } from "@/types/webrtc";
import { useFileTransferStore } from "@/stores/fileTransferStore";
import { getCachedId, setCachedId } from "@/lib/roomIdCache";
// Dynamically import the RichTextEditor
const RichTextEditor = dynamic(
@@ -77,18 +76,12 @@ export function SendTabPanel({
);
// State to track ID generation mode (false = will show simple next, true = will show random next)
const [isSimpleIdMode, setIsSimpleIdMode] = useState<boolean>(true);
// Cached ID state
const [hasCachedId, setHasCachedId] = useState<boolean>(false);
// When the validatedShareRoomId from the parent component changes (e.g., after initial fetch), synchronize the local input field's value
useEffect(() => {
setInputFieldValue(currentValidatedShareRoomId);
}, [currentValidatedShareRoomId]);
useEffect(() => {
setHasCachedId(!!getCachedId());
}, []);
const handleInputChange = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
const newValue = e.target.value;
@@ -141,31 +134,6 @@ export function SendTabPanel({
setIsSimpleIdMode(!isSimpleIdMode);
}, [isSimpleIdMode, processRoomIdInput, setInputFieldValue]);
// Save/Use cached ID button handlers
const isSaveEnabled = (inputFieldValue || "").trim().length >= 8;
const handleSaveOrUseCachedId = useCallback(() => {
if (hasCachedId) {
const cached = getCachedId();
if (cached) {
setInputFieldValue(cached);
}
return;
}
// Save current input to cache
const trimmed = (inputFieldValue || "").trim();
if (trimmed.length >= 8) {
setCachedId(trimmed);
setHasCachedId(true);
// Notify via messages on sender side
putMessageInMs(messages.text.ClipboardApp.saveId_success, true);
}
}, [
hasCachedId,
inputFieldValue,
putMessageInMs,
messages.text.ClipboardApp.saveId_success,
]);
return (
<div id="send-panel" role="tabpanel" aria-labelledby="send-tab">
<div className="mb-3 text-sm text-gray-600">
@@ -223,26 +191,13 @@ export function SendTabPanel({
: messages.text.ClipboardApp.html.generateSimpleId_tips}
</Button>
{/* Save/Use Cached ID Button in between */}
<Tooltip
content={
hasCachedId
? messages.text.ClipboardApp.html.useCachedId_tips
: messages.text.ClipboardApp.html.saveId_tips
}
>
<span className="inline-block">
<Button
className="w-full sm:w-auto px-4"
variant="outline"
onClick={handleSaveOrUseCachedId}
disabled={!hasCachedId && !isSaveEnabled}
>
{hasCachedId
? messages.text.ClipboardApp.html.useCachedId_dis
: messages.text.ClipboardApp.html.saveId_dis}
</Button>
</span>
</Tooltip>
<CachedIdActionButton
messages={messages}
getInputValue={() => inputFieldValue}
setInputValue={setInputFieldValue}
putMessageInMs={putMessageInMs}
isShareEnd={true}
/>
<Button
className="w-full sm:w-auto px-4"
onClick={() => joinRoom(true, inputFieldValue.trim())}