diff --git a/src/ui/features/guacamole/GuacamoleDisplay.tsx b/src/ui/features/guacamole/GuacamoleDisplay.tsx index 3a34fb6a..14fab6a9 100644 --- a/src/ui/features/guacamole/GuacamoleDisplay.tsx +++ b/src/ui/features/guacamole/GuacamoleDisplay.tsx @@ -18,11 +18,7 @@ import { resolveConnectionOrigin, buildOriginWsUrl, } from "@/lib/connection-origin.ts"; -import { - isFirefoxBrowser, - isPasteShortcut, - pasteTextToRemote, -} from "./guacamole-clipboard.ts"; +import { isPasteShortcut, pasteTextToRemote } from "./guacamole-clipboard.ts"; import { getGuacamoleDisplaySize } from "./guacamole-display-size.ts"; import { bindPointerInput } from "./guacamole-pointer.ts"; import { @@ -456,31 +452,32 @@ export const GuacamoleDisplay = forwardRef< displayElement.setAttribute("tabindex", "0"); displayElement.style.outline = "none"; - const useNativePasteFallback = isFirefoxBrowser(); - if (useNativePasteFallback) { - displayElement.addEventListener( - "keydown", - (event) => { - if (isPasteShortcut(event)) { - event.stopImmediatePropagation(); - } - }, - true, - ); - displayElement.addEventListener( - "paste", - (event) => { - if (clientRef.current !== client) return; - const text = event.clipboardData?.getData("text/plain"); - if (!text) return; - - event.preventDefault(); + // Reading navigator.clipboard outside a user gesture is denied by Safari + // and commonly denied by Chromium. The paste event carries the text under + // the browser's normal permission model, so use it on every browser and + // replace the original shortcut with an ordered clipboard update + Ctrl+V. + displayElement.addEventListener( + "keydown", + (event) => { + if (isPasteShortcut(event)) { event.stopImmediatePropagation(); - pasteTextToRemote(client, text); - }, - true, - ); - } + } + }, + true, + ); + displayElement.addEventListener( + "paste", + (event) => { + if (clientRef.current !== client) return; + const text = event.clipboardData?.getData("text/plain"); + if (!text) return; + + event.preventDefault(); + event.stopImmediatePropagation(); + pasteTextToRemote(client, text); + }, + true, + ); display.onresize = () => { if (!isMountedRef.current || clientRef.current !== client) return; @@ -759,7 +756,7 @@ export const GuacamoleDisplay = forwardRef< const syncClipboard = useCallback(() => { const client = clientRef.current; - if (!client || isFirefoxBrowser() || !navigator.clipboard?.readText) return; + if (!client || !navigator.clipboard?.readText) return; navigator.clipboard .readText() .then((text) => { diff --git a/src/ui/features/guacamole/guacamole-clipboard.ts b/src/ui/features/guacamole/guacamole-clipboard.ts index 8dc176ec..9fe2f71d 100644 --- a/src/ui/features/guacamole/guacamole-clipboard.ts +++ b/src/ui/features/guacamole/guacamole-clipboard.ts @@ -13,10 +13,6 @@ export interface GuacamoleClipboardClient { sendKeyEvent(pressed: number, keysym: number): void; } -export function isFirefoxBrowser(userAgent = navigator.userAgent): boolean { - return /(?:Firefox|FxiOS)\//.test(userAgent); -} - export function isPasteShortcut( event: Pick, ): boolean { diff --git a/src/ui/tests/features/guacamole/guacamole-clipboard.test.ts b/src/ui/tests/features/guacamole/guacamole-clipboard.test.ts index 1538e749..8b0db537 100644 --- a/src/ui/tests/features/guacamole/guacamole-clipboard.test.ts +++ b/src/ui/tests/features/guacamole/guacamole-clipboard.test.ts @@ -1,25 +1,11 @@ import { describe, expect, it, vi } from "vitest"; import { - isFirefoxBrowser, isPasteShortcut, pasteTextToRemote, type GuacamoleClipboardClient, } from "../../../features/guacamole/guacamole-clipboard.js"; -describe("Guacamole Firefox clipboard fallback", () => { - it("only enables the native paste path for Firefox", () => { - expect( - isFirefoxBrowser( - "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:140.0) Gecko/20100101 Firefox/140.0", - ), - ).toBe(true); - expect( - isFirefoxBrowser( - "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/140.0.0.0", - ), - ).toBe(false); - }); - +describe("Guacamole clipboard paste", () => { it("recognizes Ctrl+V and Command+V without intercepting Alt+V", () => { expect( isPasteShortcut({