anydrop/web/src/pages/Share.tsx
ordinarthur 0034e91672
All checks were successful
Build & Deploy / build-and-deploy (push) Successful in 37s
feat: push notifications + iOS share sheet integration
- Web Push API for offline device notifications (VAPID)
- Custom service worker with push + share target handlers
- iOS/Android share sheet support via Web Share Target API
- Dedicated /share page with one-tap send to nearby peer
- Background tab notifications for incoming transfers
- Persistent deviceId per device
2026-04-14 12:05:59 +02:00

158 lines
4.9 KiB
TypeScript

import { useEffect, useState, useCallback } from "react";
import { useSignaling } from "../hooks/useSignaling";
import { useStore } from "../stores/useStore";
import { useProfileStore } from "../stores/useProfileStore";
import PeerList from "../components/PeerList";
import ProfileSetup from "../components/ProfileSetup";
interface SharedData {
files: File[];
text: string;
}
/** Read shared files/text from the cache stashed by the service worker */
async function readSharedData(): Promise<SharedData> {
const result: SharedData = { files: [], text: "" };
try {
const cache = await caches.open("share-target");
// Read metadata
const metaResponse = await cache.match("/share-target-meta");
if (!metaResponse) return result;
const meta = await metaResponse.json();
result.text = meta.text || "";
// Read files
for (let i = 0; i < (meta.count || 0); i++) {
const fileResponse = await cache.match(`/share-target-file/${i}`);
if (!fileResponse) continue;
const blob = await fileResponse.blob();
const fileName = decodeURIComponent(
fileResponse.headers.get("X-File-Name") || `fichier-${i}`,
);
result.files.push(new File([blob], fileName, { type: blob.type }));
}
// Clean up cache
await caches.delete("share-target");
} catch (err) {
console.error("[share] Failed to read shared data:", err);
}
return result;
}
export default function Share() {
const isSetUp = useProfileStore((s) => s.isSetUp);
if (!isSetUp) {
return <ProfileSetup onDone={() => {}} />;
}
return <ShareConnected />;
}
function ShareConnected() {
const { sendFiles, sendText } = useSignaling();
const peers = useStore((s) => s.peers);
const setSelectedPeerId = useStore((s) => s.setSelectedPeerId);
const [shared, setShared] = useState<SharedData | null>(null);
const [sent, setSent] = useState(false);
useEffect(() => {
readSharedData().then(setShared);
}, []);
const handlePeerSelect = useCallback(
(peerId: string) => {
if (!shared || sent) return;
setSelectedPeerId(peerId);
if (shared.files.length > 0) {
sendFiles(peerId, shared.files);
}
if (shared.text && shared.files.length === 0) {
sendText(peerId, shared.text);
}
setSent(true);
},
[shared, sent, sendFiles, sendText, setSelectedPeerId],
);
const fileCount = shared?.files.length || 0;
const hasText = !!shared?.text;
return (
<div className="min-h-screen bg-gradient-to-b from-slate-950 via-slate-900 to-slate-950">
<div className="max-w-lg mx-auto px-4 py-8">
{/* Header */}
<header className="text-center mb-6">
<h1 className="text-3xl font-bold text-white mb-1">
Any<span className="text-brand-400">Drop</span>
</h1>
{!shared ? (
<p className="text-slate-500 text-sm mt-4">Chargement...</p>
) : sent ? (
<div className="mt-6">
<div className="text-4xl mb-3"></div>
<p className="text-brand-400 font-medium">Envoi en cours</p>
<a
href="/"
className="inline-block mt-4 text-sm text-slate-500 hover:text-slate-300 transition-colors"
>
Retour à l'accueil
</a>
</div>
) : (
<>
<p className="text-slate-400 text-sm mt-4">Envoyer à quel appareil ?</p>
{/* What's being shared */}
<div className="mt-3 inline-flex items-center gap-2 px-3 py-1.5 rounded-full bg-slate-800/50 text-xs text-slate-400">
{fileCount > 0 && (
<span>
📎 {fileCount} {fileCount > 1 ? "fichiers" : "fichier"}
</span>
)}
{hasText && !fileCount && <span>💬 Texte</span>}
{hasText && fileCount > 0 && <span>+ texte</span>}
</div>
</>
)}
</header>
{/* Peer list — tap to send immediately */}
{!sent && shared && (
<section>
{peers.length === 0 ? (
<div className="flex flex-col items-center justify-center py-16 text-slate-500">
<div className="text-5xl mb-4">📡</div>
<p className="text-lg font-medium">En attente d'appareils...</p>
<p className="text-sm mt-2 text-center max-w-xs">
Ouvrez AnyDrop sur l'appareil destinataire.
</p>
</div>
) : (
<div className="flex flex-wrap justify-center gap-6 py-8">
<PeerList onPeerSelect={handlePeerSelect} />
</div>
)}
</section>
)}
{/* Footer */}
<footer className="text-center text-xs text-slate-600 mt-12">
<p>Peer-to-peer · Chiffré · Aucun fichier ne transite par le serveur</p>
</footer>
</div>
</div>
);
}