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}