chore(code):"Use Cache ID" button double-click to temporarily switch to "Save ID" function
This commit is contained in:
@@ -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 drop‑in 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())}
|
||||
|
||||
Reference in New Issue
Block a user