Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 15 additions & 4 deletions desktop/src/features/onboarding/ui/MembershipDenied.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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. */
Expand Down Expand Up @@ -40,6 +40,7 @@ export function MembershipDenied({
return pubkey;
}
}, [pubkey]);
const npubRef = React.useRef<HTMLElement | null>(null);
const [copied, setCopied] = React.useState(false);
const [importError, setImportError] = React.useState<string | null>(null);
const [isImportFormOpen, setIsImportFormOpen] = React.useState(false);
Expand All @@ -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]);

Expand Down Expand Up @@ -125,7 +133,10 @@ export function MembershipDenied({
Your public key (npub)
</p>
<div className="flex items-center gap-2">
<code className="min-w-0 flex-1 truncate rounded-xl border border-border/70 bg-muted/30 px-3 py-2.5 font-mono text-xs text-foreground">
<code
ref={npubRef}
className="min-w-0 flex-1 truncate rounded-xl border border-border/70 bg-muted/30 px-3 py-2.5 font-mono text-xs text-foreground"
>
{npub}
</code>
<Button
Expand Down
4 changes: 2 additions & 2 deletions desktop/src/features/onboarding/ui/NsecMaskedDisplay.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Check, Copy, Eye, EyeOff } from "lucide-react";
import * as React from "react";
import { Button } from "@/shared/ui/button";
import { writeTextToClipboard } from "@/shared/lib/clipboard";
import { copyTextWithFallback } from "@/shared/lib/clipboard";

type NsecMaskedDisplayProps = {
nsec: string;
Expand Down Expand Up @@ -51,7 +51,7 @@ export function NsecMaskedDisplay({
}

async function handleCopy() {
await writeTextToClipboard(nsec);
await copyTextWithFallback(nsec);
onKeyInteraction?.();
setIsCopied(true);
if (copyTimerRef.current) clearTimeout(copyTimerRef.current);
Expand Down
20 changes: 19 additions & 1 deletion desktop/src/shared/lib/clipboard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,30 @@ export async function writeTextToClipboard(text: string): Promise<void> {
await copyTextToSystemClipboard(text);
}

/**
* Copy text trying navigator.clipboard.writeText first (works in
* WKWebView/WebView2 from a user gesture and avoids the arboard path
* that silently no-ops on Wayland), falling back to the native Tauri
* clipboard.
*/
export async function copyTextWithFallback(text: string): Promise<void> {
try {
if (typeof navigator !== "undefined" && navigator.clipboard?.writeText) {
await navigator.clipboard.writeText(text);
return;
}
} catch (error) {
console.warn("navigator.clipboard.writeText failed", error);
}
await writeTextToClipboard(text);
}

/** Copy plain text and show standard success/error feedback. */
export function copyTextToClipboard(
text: string,
successMessage = "Copied to clipboard",
) {
void writeTextToClipboard(text)
void copyTextWithFallback(text)
.then(() => {
toast.success(successMessage);
})
Expand Down