All checks were successful
Build & Deploy / build-and-deploy (push) Successful in 37s
- 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
158 lines
4.9 KiB
TypeScript
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>
|
|
);
|
|
}
|