Source
This app’s code and revision history are public. Download a copy or explore the files below.
Public
The app’s code and revision history are public.
Run it yourself
Take the whole tree and run it on your own machine. The download needs no account. Cloning needs a sign-in.
Download .zip
bounded clone 80ddb8c5df822e290afdd3c7
A copy you download is not bounded. The guarantees this app runs under - declared egress, proven invariants, writes that fail closed - live in the runtime serving this page, not in the files. What you get is auditable source you can read and run, with no runtime guarantees attached.
src/Receive.tsx
// The receiving screen, opened from the link, the QR code or a 6-digit code: what the sender
// offers, a Download button, progress, and Save for each file once it is on this device.
import { useEffect, useState, useSyncExternalStore } from "react";
import { navigate } from "./lib/router";
import { bytes, files as nFiles, speed, timeLeft } from "./lib/format";
import { isIOS, isPhoneOrTablet } from "./lib/device";
import { freeSpace } from "./lib/sink";
import { Receiver, type FileView, type ReceiveSnapshot } from "./receiver";
import { Start } from "./Start";
import { AppBar, Icon, ProgressBar, useLeaveGuard, useWakeLock } from "./ui";
const DROP_RE = /^[A-Za-z0-9]{20}$/;
export function ReceivePage() {
const id = location.hash.slice(1);
if (!DROP_RE.test(id)) {
return (
<div className="app-page">
<AppBar />
<h1 className="app-h1">Receive files</h1>
<p className="app-lede">{id ? "This link is not complete. Open it again, or type the code shown on the sending device." : "Type the code shown on the sending device, or open the link it sent you."}</p>
<div className="card start-card"><Start /></div>
</div>
);
}
return <Receiving key={id} id={id} />;
}
function headline(s: ReceiveSnapshot): { title: string; lede: string } {
const allDone = s.count > 0 && s.done === s.count;
switch (s.state) {
case "joining":
case "connecting":
return { title: "Connecting to the other device", lede: "Drop connects the two devices directly. This takes a few seconds." };
case "offline":
return { title: "Could not reach Drop", lede: "Check this device's internet connection, then try again." };
case "reconnecting":
return { title: "Connecting again", lede: "The connection dropped. Drop is reconnecting and will pick up where it stopped." };
case "noanswer":
return { title: "The sender is not answering", lede: "Drop needs the sending page open. Ask them to open it again and keep it on screen. This page keeps trying." };
case "blocked":
return { title: "These two devices cannot connect", lede: "Some networks block direct connections, such as some mobile carriers and office, school or hotel Wi-Fi. Put both devices on the same Wi-Fi and try again." };
case "ended":
return allDone
? { title: "All files received", lede: "The sender has stopped sharing. Your files are below." }
: { title: "The sender stopped sharing", lede: "This link no longer works. Ask them to send the files again." };
case "missing":
return { title: "This link is not open", lede: "The sender may have stopped sharing or closed Drop, or the link is not complete. Ask them to share the files again." };
case "self":
return { title: "This is the sending device", lede: "Open the link, scan the QR code or type the code on the other device." };
case "live":
default:
if (!s.files.length) return { title: "Connected", lede: "Getting the list of files." };
if (allDone) return { title: s.count === 1 ? "File received" : "All files received", lede: isIOS ? "Tap Save on each file to keep it." : "Your browser saved them to its downloads. Save again any time below." };
return { title: `${nFiles(s.files.length)} from ${s.from || "the other device"}`, lede: s.count ? "Keep this page open until it finishes." : "Nothing is downloaded until you choose. The files come straight from the other device." };
}
}
function fileLine(f: FileView): string {
switch (f.state) {
case "queued": return "Waiting";
case "receiving": return `${bytes(f.received)} of ${bytes(f.info.size)}`;
case "checking": return "Checking";
case "done": return f.check === "ok" ? "Received and checked" : "Received";
case "failed": return f.error || "Did not arrive";
default: return bytes(f.info.size);
}
}
function Receiving({ id }: { id: string }) {
const [receiver] = useState(() => new Receiver(id));
useEffect(() => {
void receiver.start();
return () => void receiver.stop();
}, [receiver]);
const s = useSyncExternalStore(receiver.subscribe, receiver.snapshot);
const [room, setRoom] = useState<number | null>(null);
useEffect(() => { void freeSpace().then(setRoom); }, [s.count]);
const busy = s.files.some((f) => f.state === "queued" || f.state === "receiving" || f.state === "checking");
useWakeLock(busy || s.state === "live");
useLeaveGuard(busy);
const { title, lede } = headline(s);
const offered = s.files.filter((f) => f.state === "offered");
const offeredSize = offered.reduce((n, f) => n + f.info.size, 0);
const left = s.rate > 0 && busy ? timeLeft((s.total - s.got) / s.rate) : "";
const canShareFiles = isPhoneOrTablet && typeof navigator.canShare === "function";
const wantedSize = s.total - s.got + offeredSize;
const tight = room !== null && wantedSize > room;
return (
<div className="app-page">
<AppBar leave={busy ? "Leave this page? The files still coming will stop." : null} />
<h1 className="app-h1">{title}</h1>
<p className="app-lede">{lede}</p>
{(s.state === "noanswer" || s.state === "blocked" || s.state === "offline") && (
<div className="row"><button className="btn btn-line" type="button" onClick={() => receiver.retryNow()}>Try again now</button></div>
)}
{(s.state === "missing" || s.state === "self") && (
<div className="row"><button className="btn" type="button" onClick={() => navigate("/app")}>Open Drop</button></div>
)}
{(s.state === "joining" || s.state === "connecting" || s.state === "reconnecting") && !s.files.length && <div className="center-fill"><span className="spinner" /></div>}
{s.files.length > 0 && (
<section className="card list-card receive-card" aria-label="Files">
{s.count > 0 && (
<div className="overall">
<ProgressBar value={s.total ? s.got / s.total : 0} label="All files" />
<p className="overall-line">
{s.done === s.count
? `${nFiles(s.count)}, ${bytes(s.total)}`
: [`${bytes(s.got)} of ${bytes(s.total)}`, s.rate > 0 && busy ? speed(s.rate) : "", left].filter(Boolean).join(" · ")}
{s.state === "live" && s.route === "local" ? " · same network" : ""}
</p>
</div>
)}
{offered.length > 0 && s.state === "live" && (
<button className="btn btn-lg btn-block" type="button" onClick={() => receiver.wantAll()}>
<Icon name="download" size={18} /> {offered.length === s.files.length ? "Download" : "Download the rest"} {offered.length > 1 ? `all ${offered.length}` : ""} ({bytes(offeredSize)})
</button>
)}
<ul className="files rx-files">
{s.files.map((f) => (
<li key={f.info.i} className={`rx-${f.state}`}>
<span className="file-icon"><Icon name={f.state === "done" ? "check" : f.info.zip ? "zip" : "file"} size={18} /></span>
<div className="grow">
<span className="file-name" title={f.info.name}>{f.info.name}</span>
<span className={`file-state${f.state === "failed" ? " bad" : ""}`}>{fileLine(f)}{f.state === "queued" || f.state === "checking" || f.state === "done" ? ` · ${bytes(f.info.size)}` : ""}</span>
{f.state === "receiving" && <ProgressBar value={f.info.size ? f.received / f.info.size : 1} label={`${f.info.name} progress`} />}
</div>
<div className="file-actions">
{f.state === "offered" && s.state === "live" && <button className="btn btn-line btn-sm" type="button" onClick={() => receiver.want(f.info.i)}>Download</button>}
{f.state === "failed" && s.state === "live" && <button className="btn btn-line btn-sm" type="button" onClick={() => receiver.want(f.info.i)}>Try again</button>}
{(f.state === "receiving" || f.state === "checking") && <span className="spinner" aria-hidden="true" />}
{f.state === "done" && (
<>
{canShareFiles && f.file && navigator.canShare({ files: [f.file] }) && (
<button className="btn btn-line btn-sm" type="button" onClick={() => void navigator.share({ files: [f.file!] }).catch(() => null)} aria-label={`Share ${f.info.name}`}><Icon name="share" size={16} /></button>
)}
<button className={`btn btn-sm${f.saved ? " btn-line" : ""}`} type="button" onClick={() => receiver.save(f.info.i)}>{f.saved ? "Save again" : "Save"}</button>
</>
)}
</div>
</li>
))}
</ul>
<label className="check verify">
<input type="checkbox" checked={s.verify} onChange={(e) => receiver.setVerify(e.target.checked)} />
<span>Check each file with SHA-256 when it arrives</span>
</label>
</section>
)}
{s.files.length > 0 && (
<div className="stack notes">
{tight && <p className="notice notice-error">This browser says it has room for about {bytes(room!)}, and these files are {bytes(wantedSize)}. Free up space, or receive them on a device with more room.</p>}
{isIOS && <p className="notice">On iPhone and iPad, keep this page open on screen until it finishes. Each file waits here until you tap Save. Safari keeps very large files (several GB) in its own storage first, so they may not fit; a computer is better for those.</p>}
{!isIOS && s.inMemory && <p className="notice">This browser keeps the file in memory while it arrives, so a very large file may not fit. Chrome, Edge or Firefox on a computer can receive files of any size the disk can hold.</p>}
{busy && !isIOS && <p className="notice">Keep this page open until it finishes. If the connection drops, Drop picks up where it stopped.</p>}
</div>
)}
</div>
);
}
Change history
Point OpenApps links to openapps.free
Bind agent address in policy
Retry release capture
Use OpenApps service URLs and add contribution strip
Bounded source sync
Bounded source sync
Bounded source sync