feat(web): native share sheet for created + saved links
All checks were successful
Build & Deploy / build-and-deploy (push) Successful in 39s

Adds a "Share link" button that invokes navigator.share() so iOS
home-screen PWA users (and any platform with the Web Share API)
get the system share sheet — AirDrop, Messages, Mail, WhatsApp…
Falls back to copy-only on platforms without support.

Wired on both CloudSharePanel (just-created link) and Settings →
Shared links (re-share an existing one).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
ordinarthur 2026-04-20 15:31:38 +02:00
parent 641ec629f5
commit 48ace8af34
2 changed files with 59 additions and 7 deletions

View File

@ -71,6 +71,20 @@ export default function CloudSharePanel() {
setTimeout(() => setCopied(false), 1500);
};
const canShare = typeof navigator !== "undefined" && typeof navigator.share === "function";
const share = async (url: string, fileName: string) => {
try {
await navigator.share({
title: `AnyDrop — ${fileName}`,
text: "File shared via AnyDrop",
url,
});
} catch {
// User canceled the share sheet, or the browser rejected — nothing to do.
}
};
return (
<div className="paper-panel p-4 sm:p-5">
{stage.kind === "idle" && (
@ -235,13 +249,27 @@ export default function CloudSharePanel() {
</div>
</div>
<button
onClick={() => copy(stage.shareUrl)}
className="w-full py-2.5 bg-ink text-paper text-sm font-medium rounded-sm
hover:bg-signal transition-colors duration-fast ease-crisp"
>
{copied ? "Copied ✓" : "Copy link"}
</button>
<div className="flex gap-2">
{canShare && (
<button
onClick={() => share(stage.shareUrl, stage.fileName)}
className="flex-1 py-2.5 bg-ink text-paper text-sm font-medium rounded-sm
hover:bg-signal transition-colors duration-fast ease-crisp"
>
Share link
</button>
)}
<button
onClick={() => copy(stage.shareUrl)}
className={`py-2.5 text-sm font-medium rounded-sm transition-colors duration-fast ease-crisp ${
canShare
? "flex-1 border border-paper-edge hover:border-ink text-ink"
: "w-full bg-ink text-paper hover:bg-signal"
}`}
>
{copied ? "Copied ✓" : "Copy link"}
</button>
</div>
<p className="mt-2 font-mono text-[11px] text-ink-faint break-all">
{stage.shareUrl}

View File

@ -509,6 +509,22 @@ function SharedLinksSection() {
setTimeout(() => setCopiedId((v) => (v === item.id ? null : v)), 1500);
};
const canShare = typeof navigator !== "undefined" && typeof navigator.share === "function";
const onShare = async (item: LinkItem) => {
if (!item.keyFrag) return;
const url = `${window.location.origin}/r/${item.id}#k=${item.keyFrag}`;
try {
await navigator.share({
title: `AnyDrop — ${item.filename ?? "Encrypted file"}`,
text: "File shared via AnyDrop",
url,
});
} catch {
// User canceled — no-op.
}
};
return (
<section className="mb-12">
<div className="text-xs uppercase tracking-[0.22em] text-ink-muted">
@ -554,6 +570,14 @@ function SharedLinksSection() {
</div>
</div>
<div className="flex flex-col items-end gap-1.5 shrink-0">
{item.keyFrag && status === "active" && canShare && (
<button
onClick={() => onShare(item)}
className="text-xs text-ink hover:text-signal transition-colors"
>
Share
</button>
)}
{item.keyFrag && status === "active" && (
<button
onClick={() => onCopy(item)}