From 205d6f40fbc58be2105a50ccc22b8e870257152b Mon Sep 17 00:00:00 2001 From: JoeJoeflyn Date: Wed, 29 Jul 2026 17:37:53 +0700 Subject: [PATCH] fix(desktop): try navigator.clipboard before arboard for npub/nsec copy MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add copyTextWithFallback helper in shared/lib/clipboard that tries navigator.clipboard.writeText first, falling back to the Tauri/arboard path. - Use it in MembershipDenied and NsecMaskedDisplay copy buttons. - Implement the text-selection fallback in MembershipDenied that the catch block only commented about before. arboard without wayland-data-control writes to the XWayland clipboard, which Wayland-native apps never see. set_text returns Ok so the checkmark fires with an empty clipboard. - `pnpm run typecheck` - `just desktop-standalone` → copy npub and nsec → paste in a Wayland-native app → text present Closes #2896 Signed-off-by: JoeJoeflyn --- .../onboarding/ui/MembershipDenied.tsx | 19 ++++++++++++++---- .../onboarding/ui/NsecMaskedDisplay.tsx | 4 ++-- desktop/src/shared/lib/clipboard.ts | 20 ++++++++++++++++++- 3 files changed, 36 insertions(+), 7 deletions(-) diff --git a/desktop/src/features/onboarding/ui/MembershipDenied.tsx b/desktop/src/features/onboarding/ui/MembershipDenied.tsx index 3de6857d31..5c7dec95be 100644 --- a/desktop/src/features/onboarding/ui/MembershipDenied.tsx +++ b/desktop/src/features/onboarding/ui/MembershipDenied.tsx @@ -3,13 +3,13 @@ import { Check, Copy, KeyRound, ShieldX, Ticket } from "lucide-react"; import { useCommunityOnboarding } from "@/features/onboarding/communityOnboarding"; import { nsecToNpub, pubkeyToNpub } from "@/shared/lib/nostrUtils"; +import { copyTextWithFallback } from "@/shared/lib/clipboard"; import { Badge } from "@/shared/ui/badge"; import { Button } from "@/shared/ui/button"; import { Input } from "@/shared/ui/input"; import { Spinner } from "@/shared/ui/spinner"; import { StartupWindowDragRegion } from "@/shared/ui/StartupWindowDragRegion"; import { InviteRedeemForm } from "./InviteRedeemForm"; -import { writeTextToClipboard } from "@/shared/lib/clipboard"; type MembershipDeniedProps = { /** The relay that denied membership — used as the target for bare-code invites. */ @@ -40,6 +40,7 @@ export function MembershipDenied({ return pubkey; } }, [pubkey]); + const npubRef = React.useRef(null); const [copied, setCopied] = React.useState(false); const [importError, setImportError] = React.useState(null); const [isImportFormOpen, setIsImportFormOpen] = React.useState(false); @@ -54,11 +55,18 @@ export function MembershipDenied({ const handleCopy = React.useCallback(async () => { try { - await writeTextToClipboard(npub); + await copyTextWithFallback(npub); setCopied(true); setTimeout(() => setCopied(false), 2000); } catch { - // Fallback: select the text so the user can copy manually + const node = npubRef.current; + if (node) { + const range = document.createRange(); + range.selectNodeContents(node); + const selection = window.getSelection(); + selection?.removeAllRanges(); + selection?.addRange(range); + } } }, [npub]); @@ -125,7 +133,10 @@ export function MembershipDenied({ Your public key (npub)

- + {npub}