fix(desktop): route text copies through native clipboard (#2054)

Signed-off-by: npub1qvn3cujt28pg06ehlstrxyz6ayzp06t4uc7r566vxwwgrv24hglq9zju0n <03271c724b51c287eb37fc1633105ae90417e975e63c3a6b4c339c81b155ba3e@sprout-oss.stage.blox.sqprod.co>
Co-authored-by: npub1qvn3cujt28pg06ehlstrxyz6ayzp06t4uc7r566vxwwgrv24hglq9zju0n <03271c724b51c287eb37fc1633105ae90417e975e63c3a6b4c339c81b155ba3e@sprout-oss.stage.blox.sqprod.co>
This commit is contained in:
Bradley Axen
2026-07-18 00:01:24 +00:00
committed by GitHub
co-authored by npub1qvn3cujt28pg06ehlstrxyz6ayzp06t4uc7r566vxwwgrv24hglq9zju0n
parent 738d456367
commit 9f82850959
21 changed files with 100 additions and 64 deletions
@@ -84,6 +84,7 @@ import {
ToggleRow,
} from "./ChannelManagementSheetRows";
import { ChannelManagementModerationActions } from "./ChannelManagementModerationActions";
import { writeTextToClipboard } from "@/shared/lib/clipboard";
type ChannelManagementSheetProps = {
channel: Channel | null;
@@ -788,9 +789,9 @@ function ChannelManagementPanelContent({
icon={Copy}
label="Copy ID"
onClick={() => {
void navigator.clipboard
.writeText(resolvedChannel.id)
.then(() => toast.success("Copied channel ID"));
void writeTextToClipboard(resolvedChannel.id).then(() =>
toast.success("Copied channel ID"),
);
}}
testId="channel-management-copy-id-action"
/>
@@ -12,6 +12,7 @@ import { toast } from "sonner";
import type { Channel } from "@/shared/api/types";
import { cn } from "@/shared/lib/cn";
import { Switch } from "@/shared/ui/switch";
import { writeTextToClipboard } from "@/shared/lib/clipboard";
function getChannelIcon(channelType: Channel["channelType"]): LucideIcon {
if (channelType === "forum") {
@@ -126,7 +127,7 @@ export function CopyFieldRow({
testId?: string;
}) {
async function handleCopy() {
await navigator.clipboard.writeText(value);
await writeTextToClipboard(value);
toast.success(`Copied ${label.toLowerCase()}`);
}
+2 -1
View File
@@ -18,6 +18,7 @@ import { UpdateIndicator } from "@/features/settings/UpdateIndicator";
import { cn } from "@/shared/lib/cn";
import { channelChrome } from "@/shared/layout/chromeLayout";
import { Button } from "@/shared/ui/button";
import { writeTextToClipboard } from "@/shared/lib/clipboard";
type ChatHeaderProps = {
actions?: React.ReactNode;
@@ -104,7 +105,7 @@ export function ChatHeader({
if (!value) return;
try {
await navigator.clipboard.writeText(value);
await writeTextToClipboard(value);
toast.success("Channel name copied");
} catch {
toast.error("Failed to copy channel name");
@@ -23,6 +23,7 @@ import { Button } from "@/shared/ui/button";
import { StartupWindowDragRegion } from "@/shared/ui/StartupWindowDragRegion";
import { useSystemColorScheme } from "@/shared/theme/useSystemColorScheme";
import { OnboardingChrome } from "@/features/onboarding/ui/OnboardingChrome";
import { writeTextToClipboard } from "@/shared/lib/clipboard";
type WelcomeSetupPage = "welcome" | "join" | "invite";
type WelcomeTransitionMode = "initial" | OnboardingTransitionDirection;
@@ -190,7 +191,7 @@ export function WelcomeSetup({
className="h-10 w-10 shrink-0 text-muted-foreground hover:text-foreground"
disabled={!npub}
onClick={() => {
void navigator.clipboard.writeText(npub).then(() => {
void writeTextToClipboard(npub).then(() => {
setCopied(true);
window.setTimeout(() => setCopied(false), 1500);
});
@@ -14,6 +14,7 @@ import {
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/shared/ui/dropdown-menu";
import { writeTextToClipboard } from "@/shared/lib/clipboard";
const TTL_OPTIONS: { label: string; value: number }[] = [
{ label: "1 day", value: 24 * 60 * 60 },
@@ -72,7 +73,7 @@ export function InviteLinkSection() {
async function handleCopy() {
if (!invite) return;
try {
await navigator.clipboard.writeText(invite.url);
await writeTextToClipboard(invite.url);
setCopied(true);
toast.success("Invite link copied");
setTimeout(() => setCopied(false), 2000);
@@ -9,6 +9,7 @@ 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. */
@@ -53,7 +54,7 @@ export function MembershipDenied({
const handleCopy = React.useCallback(async () => {
try {
await navigator.clipboard.writeText(npub);
await writeTextToClipboard(npub);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
} catch {
@@ -1,6 +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";
type NsecMaskedDisplayProps = {
nsec: string;
@@ -43,7 +44,7 @@ export function NsecMaskedDisplay({
}
async function handleCopy() {
await navigator.clipboard.writeText(nsec);
await writeTextToClipboard(nsec);
setIsCopied(true);
if (copyTimerRef.current) clearTimeout(copyTimerRef.current);
copyTimerRef.current = setTimeout(() => setIsCopied(false), 2000);
@@ -16,6 +16,7 @@ import { cn } from "@/shared/lib/cn";
import { useSystemColorScheme } from "@/shared/theme/useSystemColorScheme";
import { Button } from "@/shared/ui/button";
import { StartupWindowDragRegion } from "@/shared/ui/StartupWindowDragRegion";
import { writeTextToClipboard } from "@/shared/lib/clipboard";
const COPY_SUCCESS_MESSAGE =
"Signed response copied. Paste it into the Buzz admin console.";
@@ -103,7 +104,7 @@ function formatError(error: unknown): string {
async function copyToClipboard(text: string): Promise<boolean> {
try {
await navigator.clipboard.writeText(text);
await writeTextToClipboard(text);
return true;
} catch (error) {
console.warn("copy signed nostr binding response failed:", error);
@@ -2,6 +2,7 @@ import { Check, Copy } from "lucide-react";
import * as React from "react";
import { cn } from "@/shared/lib/cn";
import { writeTextToClipboard } from "@/shared/lib/clipboard";
/** Icon button that copies arbitrary text with a brief check feedback. */
export function CopyTextButton({
@@ -15,7 +16,7 @@ export function CopyTextButton({
}) {
const [copied, setCopied] = React.useState(false);
const handleCopy = React.useCallback(() => {
void navigator.clipboard.writeText(text).then(() => {
void writeTextToClipboard(text).then(() => {
setCopied(true);
setTimeout(() => setCopied(false), 2_000);
});
@@ -17,6 +17,7 @@ import {
toggleNoteIdInSet,
} from "@/features/pulse/lib/noteActions";
import type { UserNote } from "@/shared/api/socialTypes";
import { writeTextToClipboard } from "@/shared/lib/clipboard";
export type PulseNoteActions = {
isReplySending: boolean;
@@ -144,7 +145,7 @@ export function usePulseNoteActions({
const share = React.useCallback(async (note: UserNote) => {
try {
await navigator.clipboard.writeText(buildNoteShareUri(note));
await writeTextToClipboard(buildNoteShareUri(note));
toast.success("Copied note link");
} catch {
toast.error("Failed to copy note link");
@@ -28,6 +28,7 @@ import {
} from "@/shared/ui/dialog";
import { SettingsOptionGroup, SettingsOptionRow } from "./SettingsOptionGroup";
import { SettingsSectionHeader } from "./SettingsSectionHeader";
import { writeTextToClipboard } from "@/shared/lib/clipboard";
type PairingStep =
| "generating"
@@ -168,7 +169,7 @@ function PairingDialog({
async function handleCopy() {
if (!qrUri) return;
await navigator.clipboard.writeText(qrUri);
await writeTextToClipboard(qrUri);
toast.success("Copied to clipboard");
}
@@ -36,6 +36,7 @@ import { Input } from "@/shared/ui/input";
import { Spinner } from "@/shared/ui/spinner";
import { Textarea } from "@/shared/ui/textarea";
import { SettingsSectionHeader } from "./SettingsSectionHeader";
import { writeTextToClipboard } from "@/shared/lib/clipboard";
type ProfileSettingsCardProps = {
currentPubkey?: string;
@@ -85,7 +86,7 @@ function IdentityRow({
className="inline-flex shrink-0 items-center gap-1.5 rounded-full bg-muted px-3 py-1.5 text-sm font-medium text-foreground transition-colors hover:bg-muted/80 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
data-testid={`copy-${testId}`}
onClick={async () => {
await navigator.clipboard.writeText(copyValue);
await writeTextToClipboard(copyValue);
toast.success("Copied to clipboard");
}}
title={`Copy ${label}`}
@@ -21,6 +21,7 @@ import { cn } from "@/shared/lib/cn";
import { getInitials } from "@/shared/lib/initials";
import { isMacPlatform } from "@/shared/lib/platform";
import { useIsFullscreen } from "@/shared/lib/useIsFullscreen";
import { writeTextToClipboard } from "@/shared/lib/clipboard";
type CommunityRailProps = {
communities: Community[];
@@ -223,7 +224,7 @@ export function CommunityRail({
</ContextMenuItem>
<ContextMenuItem
onClick={() => {
void navigator.clipboard.writeText(community.relayUrl);
void writeTextToClipboard(community.relayUrl);
}}
>
<Link2 className="h-4 w-4" />
+9 -7
View File
@@ -1,16 +1,18 @@
import { toast } from "sonner";
/**
* Copy plain text to the clipboard with a success toast, surfacing
* `writeText` rejections (permissions, unfocused document) as an error
* toast instead of an unhandled rejection.
*/
import { copyTextToSystemClipboard } from "@/shared/api/tauriMedia";
/** Write plain text through the native clipboard integration. */
export async function writeTextToClipboard(text: string): Promise<void> {
await copyTextToSystemClipboard(text);
}
/** Copy plain text and show standard success/error feedback. */
export function copyTextToClipboard(
text: string,
successMessage = "Copied to clipboard",
) {
void navigator.clipboard
.writeText(text)
void writeTextToClipboard(text)
.then(() => {
toast.success(successMessage);
})
+4 -5
View File
@@ -1,3 +1,5 @@
import { copyTextToSystemClipboard } from "@/shared/api/tauriMedia";
const BUZZ_CODE_BLOCK_ATTRIBUTE = "data-buzz-code-block";
function escapeHtml(value: string) {
@@ -15,13 +17,10 @@ function createBuzzCodeBlockHtml(code: string) {
export async function copyCodeBlockToClipboard(code: string) {
const clipboard = navigator.clipboard;
if (!clipboard) {
throw new Error("Clipboard API is unavailable");
}
if (
typeof ClipboardItem !== "undefined" &&
typeof clipboard.write === "function"
typeof clipboard?.write === "function"
) {
try {
await clipboard.write([
@@ -38,7 +37,7 @@ export async function copyCodeBlockToClipboard(code: string) {
}
}
await clipboard.writeText(code);
await copyTextToSystemClipboard(code);
}
export function getBuzzCodeBlockClipboardText(