fix(frontend): guard CopyButton against concurrent copies

The button is disabled once isCopied is true, but copy() is now async, so
isCopied and the disabled prop only catch up after it resolves. Clicks
landing inside that window both passed the guard and started a write.

Nothing leaked, since the existing clearTimeout already handled the
duplicate timer, but the window did not exist before copy-to-clipboard 4
made the call async. A ref cleared in finally closes it.

Raised by CodeRabbit on #2762. 598 tests pass, build and lint unchanged.
This commit is contained in:
Alex committed 2026-09-12 20:05:20 +01:00
1 parent 88b4d89b4f
commit 4cc603db89
1 file changed
+7 -1
+7 -1
View File
@@ -35,6 +35,9 @@ export default function CopyButton({
const { t } = useTranslation();
const [isCopied, setIsCopied] = useState(false);
const timeoutIdRef = useRef<number | null>(null);
// `copy` is async, so `isCopied` and the disabled prop only catch up after it
// resolves. Guard that window so rapid clicks cannot start a second write.
const copyInFlightRef = useRef(false);
const iconWrapperClasses = clsx(
'flex items-center justify-center rounded-full transition-colors duration-150 ease-in-out',
@@ -72,7 +75,8 @@ export default function CopyButton({
: t('conversation.copy');
const handleCopy = useCallback(async () => {
if (isCopied) return;
if (isCopied || copyInFlightRef.current) return;
copyInFlightRef.current = true;
try {
const success = await copy(textToCopy);
@@ -92,6 +96,8 @@ export default function CopyButton({
}
} catch (error) {
console.error('Failed to copy text:', error);
} finally {
copyInFlightRef.current = false;
}
}, [textToCopy, copiedDuration, isCopied]);