fix(share): robust clipboard copy with an iOS-safe fallback

navigator.clipboard.writeText is intermittent on iOS Safari (bug 9: the
download share-link copy 'often does neither'). Add lib/clipboard.ts
copyText(): a gesture-safe execCommand path FIRST (before any await can
spend the transient user activation), async Clipboard API as fallback.
Migrate all five copy call sites to it.
This commit is contained in:
2026-07-19 05:38:42 +02:00
parent 31a8835f7f
commit 42ae451c1b
6 changed files with 77 additions and 15 deletions
+3 -3
View File
@@ -31,6 +31,7 @@ import { useMe } from "../lib/useMe";
import { api, type DownloadJob } from "../lib/api";
import { invalidateDownloads } from "../lib/downloads";
import { notify } from "../lib/notifications";
import { copyText } from "../lib/clipboard";
import { formatBytes, formatDuration, formatEta, formatSpeed } from "../lib/format";
import { inputCls } from "./ui/form";
@@ -129,10 +130,9 @@ function displayUrl(url: string): string {
function SourceRef({ url }: { url: string }) {
const { t } = useTranslation();
const copy = async () => {
try {
await navigator.clipboard.writeText(url);
if (await copyText(url)) {
notify({ level: "success", message: t("downloads.source.copied") });
} catch {
} else {
notify({ level: "error", message: t("downloads.source.copyFailed") });
}
};
+4 -3
View File
@@ -1,5 +1,6 @@
import { useEffect, useMemo, useState } from "react";
import Overlay from "./Overlay";
import { copyText } from "../lib/clipboard";
import { usePrefs } from "./PrefsProvider";
/**
@@ -205,11 +206,11 @@ export default function GlassTuner() {
return lines.length ? `${scope} {\n${lines.join("\n")}\n}` : "/* all values at defaults */";
}, [vars, palette, themeTick]);
function copy() {
navigator.clipboard?.writeText(cssOut).then(() => {
async function copy() {
if (await copyText(cssOut)) {
setCopied(true);
setTimeout(() => setCopied(false), 1200);
});
}
}
function persistOpen(v: boolean) {
+3 -3
View File
@@ -20,6 +20,7 @@ import { CSS } from "@dnd-kit/utilities";
import { api, getActiveAccount, type FeedFilters, type SavedView } from "../lib/api";
import { filtersToParams, shareUrl } from "../lib/urlState";
import { notify } from "../lib/notifications";
import { copyText } from "../lib/clipboard";
import { accountKey, LS, writeJSON } from "../lib/storage";
import { useConfirm } from "./ConfirmProvider";
@@ -108,10 +109,9 @@ export default function SavedViewsWidget({
}
async function share(v: SavedView) {
try {
await navigator.clipboard.writeText(shareUrl(asFilters(v.filters)));
if (await copyText(shareUrl(asFilters(v.filters)))) {
notify({ level: "success", message: t("views.shareCopied") });
} catch {
} else {
notify({ level: "warning", message: t("sidebar.shareFailed") });
}
}
+3 -3
View File
@@ -6,6 +6,7 @@ import clsx from "clsx";
import Modal from "./Modal";
import { useConfirm } from "./ConfirmProvider";
import { notify } from "../lib/notifications";
import { copyText } from "../lib/clipboard";
import { api, type DownloadJob, type ShareLink } from "../lib/api";
import { inputCls, btnCls } from "./ui/form";
const EXPIRY_DAYS = [null, 1, 7, 30] as const;
@@ -87,11 +88,10 @@ function LinkRow({ link, onChanged }: { link: ShareLink; onChanged: () => void }
const fullUrl = window.location.origin + link.url;
const copy = async () => {
try {
await navigator.clipboard.writeText(fullUrl);
if (await copyText(fullUrl)) {
setCopied(true);
setTimeout(() => setCopied(false), 1500);
} catch {
} else {
notify({ level: "error", message: t("downloads.share.copyFailed") });
}
};
+3 -3
View File
@@ -5,6 +5,7 @@ import { Library, Pencil, Share2, SlidersHorizontal, User } from "lucide-react";
import { api, type FeedFilters, type Tag } from "../lib/api";
import { defaultLayout, type PanelLayout } from "../lib/panelLayout";
import { shareUrl } from "../lib/urlState";
import { copyText } from "../lib/clipboard";
import { notify } from "../lib/notifications";
import { useDebounced } from "../lib/useDebounced";
import { clearedFilters, useActiveFilters } from "../lib/useActiveFilters";
@@ -125,10 +126,9 @@ export default function Sidebar({
const active = activeCount > 0;
async function shareView() {
try {
await navigator.clipboard.writeText(shareUrl(filters));
if (await copyText(shareUrl(filters))) {
notify({ level: "success", message: t("sidebar.shareCopied") });
} catch {
} else {
notify({ level: "warning", message: t("sidebar.shareFailed") });
}
}