feat(ux): cached roomId auto-join + theme toggle
- Receiver: auto-fill and join on Retrieve tab when input empty, not in room, no URL roomId, and cachedId exists (ClipboardApp + roomIdCache) - Sender: “Use cached ID” now immediately joins the room (add onUseCached + disabled to CachedIdActionButton; wire in SendTabPanel) - UI: add ThemeToggle and integrate into Header (desktop and mobile) - Styles: replace hardcoded white with design tokens in Retrieve panel (bg-card/text-card-foreground) for dark mode - Docs: update AI playbook flows and code-map
This commit is contained in:
@@ -14,6 +14,7 @@ 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";
|
||||
|
||||
const ClipboardApp = () => {
|
||||
const { shareMessage, retrieveMessage, putMessageInMs } =
|
||||
@@ -37,6 +38,9 @@ const ClipboardApp = () => {
|
||||
setIsDragging,
|
||||
setRetrieveRoomIdInput,
|
||||
setActiveTab,
|
||||
// for auto-join on receiver side
|
||||
isReceiverInRoom,
|
||||
retrieveRoomIdInput,
|
||||
} = useFileTransferStore();
|
||||
|
||||
const richTextToPlainText = useRichTextToPlainText();
|
||||
@@ -144,6 +148,32 @@ const ClipboardApp = () => {
|
||||
};
|
||||
}, [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,
|
||||
]);
|
||||
|
||||
if (isLoadingMessages || !messages) {
|
||||
return (
|
||||
<div className="container mx-auto px-4 py-8 w-full md:max-w-4xl">
|
||||
|
||||
@@ -62,6 +62,10 @@ type Props = {
|
||||
size?: "default" | "sm" | "lg" | "icon";
|
||||
dblClickWindowMs?: number; // default 400ms
|
||||
saveModeDurationMs?: number; // default 3000ms
|
||||
// Optional: called after a cached ID is applied (single-click "Use cached ID")
|
||||
onUseCached?: (cachedId: string) => void;
|
||||
// Optional: external disabled flag
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
export default function CachedIdActionButton({
|
||||
@@ -75,6 +79,8 @@ export default function CachedIdActionButton({
|
||||
size = "default",
|
||||
dblClickWindowMs = 400,
|
||||
saveModeDurationMs = 3000,
|
||||
onUseCached,
|
||||
disabled = false,
|
||||
}: Props) {
|
||||
const [hasCachedId, setHasCachedId] = useState<boolean>(false);
|
||||
const [showSaveOverride, setShowSaveOverride] = useState<boolean>(false);
|
||||
@@ -128,6 +134,8 @@ export default function CachedIdActionButton({
|
||||
const cached = getCachedId();
|
||||
if (cached) {
|
||||
setInputValue(cached);
|
||||
// Notify caller after applying cached value
|
||||
onUseCached?.(cached);
|
||||
}
|
||||
}
|
||||
clickCountRef.current = 0;
|
||||
@@ -161,6 +169,7 @@ export default function CachedIdActionButton({
|
||||
isShareEnd,
|
||||
dblClickWindowMs,
|
||||
saveModeDurationMs,
|
||||
onUseCached,
|
||||
]);
|
||||
|
||||
return (
|
||||
@@ -177,7 +186,9 @@ export default function CachedIdActionButton({
|
||||
variant={variant}
|
||||
size={size}
|
||||
onClick={handleClick}
|
||||
disabled={isSaveMode ? !isSaveEnabled : !hasCachedId}
|
||||
disabled={
|
||||
disabled || (isSaveMode ? !isSaveEnabled : !hasCachedId)
|
||||
}
|
||||
>
|
||||
{isSaveMode
|
||||
? messages.text.ClipboardApp.html.saveId_dis
|
||||
|
||||
@@ -156,7 +156,7 @@ export function RetrieveTabPanel({
|
||||
</div>
|
||||
{retrievedContent && (
|
||||
<div className="my-3 p-3 border rounded-md">
|
||||
<div className="bg-white p-3 rounded border border-gray-200 text-sm leading-relaxed">
|
||||
<div className="bg-card text-card-foreground p-3 rounded border text-sm leading-relaxed">
|
||||
<div dangerouslySetInnerHTML={{ __html: retrievedContent }} />
|
||||
</div>
|
||||
<div className="flex justify-start">
|
||||
|
||||
@@ -197,6 +197,11 @@ export function SendTabPanel({
|
||||
setInputValue={setInputFieldValue}
|
||||
putMessageInMs={putMessageInMs}
|
||||
isShareEnd={true}
|
||||
disabled={isSenderInRoom}
|
||||
onUseCached={(id) => {
|
||||
// Immediately join as sender after applying cached ID
|
||||
joinRoom(true, id.trim());
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
className="w-full sm:w-auto px-4"
|
||||
|
||||
@@ -8,6 +8,7 @@ import Image from "next/image";
|
||||
import { Menu, X, Github } from "lucide-react";
|
||||
import LanguageSwitcher from "@/components/LanguageSwitcher";
|
||||
import { Messages } from "@/types/messages";
|
||||
import ThemeToggle from "@/components/web/ThemeToggle";
|
||||
|
||||
/**
|
||||
* Props interface for the Header component
|
||||
@@ -91,6 +92,7 @@ const Header = ({ messages, lang }: HeaderProps) => {
|
||||
<Github className="h-5 w-5" />
|
||||
</Link>
|
||||
</Button>
|
||||
<ThemeToggle />
|
||||
<LanguageSwitcher />
|
||||
</div>
|
||||
</div>
|
||||
@@ -98,6 +100,7 @@ const Header = ({ messages, lang }: HeaderProps) => {
|
||||
{/* Mobile menu controls */}
|
||||
<div className="md:hidden flex items-center space-x-2">
|
||||
<LanguageSwitcher />
|
||||
<ThemeToggle />
|
||||
<Button asChild variant="ghost" size="icon">
|
||||
<Link
|
||||
href={githubUrl}
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
"use client";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Moon, Sun } from "lucide-react";
|
||||
import { useTheme } from "next-themes";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
export default function ThemeToggle() {
|
||||
const { resolvedTheme, setTheme } = useTheme();
|
||||
const [mounted, setMounted] = useState(false);
|
||||
|
||||
useEffect(() => setMounted(true), []);
|
||||
|
||||
const isDark = resolvedTheme === "dark";
|
||||
const toggle = () => setTheme(isDark ? "light" : "dark");
|
||||
|
||||
return (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
aria-label="Toggle theme"
|
||||
onClick={toggle}
|
||||
disabled={!mounted}
|
||||
>
|
||||
{isDark ? <Sun className="h-5 w-5" /> : <Moon className="h-5 w-5" />}
|
||||
<span className="sr-only">Toggle theme</span>
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user