drop-free.openapps.xyz

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.
Owned app · rev b2e053fd98cb
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.
← All files
src/Start.tsx
// The first screen, in the landing hero and on the installed app's home: pick files to send, or
// type a 6-digit code to receive. Picking files starts a share and opens the sending screen.
import { useRef, useState, useSyncExternalStore, type FormEvent } from "react";
import { get, set, serverTimestamp } from "@bounded-sh/client";
import { me, explain } from "./lib/bounded";
import { navigate, usePath } from "./lib/router";
import { picksFromInput, type Pick } from "./lib/items";
import { isPhoneOrTablet } from "./lib/device";
import { files as nFiles } from "./lib/format";
import { Sender } from "./sender";
import { Icon } from "./ui";

// The share in progress lives as long as the page, so moving between screens does not stop it.
let active: Sender | null = null;
const watchers = new Set<() => void>();
const notify = () => { for (const fn of watchers) fn(); };
const watch = (fn: () => void) => { watchers.add(fn); return () => { watchers.delete(fn); }; };

export function currentSender(): Sender | null {
  return active && active.snapshot().state !== "stopped" ? active : null;
}

export function useCurrentSender(): Sender | null {
  return useSyncExternalStore(watch, currentSender);
}

/** The share this page started last, even once stopped, so the sending screen can say that it stopped. */
export function useLastSender(): Sender | null {
  return useSyncExternalStore(watch, () => active);
}

/** Starts sharing these picks (or adds them to the share in progress) and opens the sending screen. */
export function send(picks: Pick[]) {
  if (!picks.some((p) => p.files.length)) return;
  const now = currentSender();
  if (now && now.snapshot().state === "sharing") {
    now.add(picks);
  } else {
    void now?.stop();
    active = new Sender(picks);
    void active.start();
    notify();
  }
  navigate("/send");
}

/** A share that could not start begins again as a new share with the same files. */
export function retrySending() {
  if (!active) return;
  active = new Sender([...active.picks]);
  void active.start();
  notify();
}

export async function stopSending() {
  await active?.stop();
  notify();
}

/** A 6-digit code to the share it points to: one rate-limited try per code. */
async function resolveCode(code: string): Promise<string> {
  const uid = await me();
  const tryPath = `codetries/${uid}/t/${code}`;
  if (!(await get(tryPath))) {
    try {
      await set(tryPath, { n: 1, at: serverTimestamp() });
    } catch (err) {
      const e = err as { decline?: { boundary?: { cause?: string } }; message?: string };
      if (e?.decline?.boundary?.cause === "cap_exceeded" || /code_tries|invariant|409/.test(String(e?.message))) {
        throw new Error("Too many codes tried. Wait a few minutes, or open the link or QR code instead.");
      }
      throw new Error(explain(err));
    }
  }
  const doc = await get(`codes/${code}`);
  if (!doc?.drop) throw new Error("That code did not work. Check the code on the sending device. Each code lasts 10 minutes.");
  return String(doc.drop);
}

export function Start() {
  usePath();
  const sharing = useCurrentSender();
  const fileInput = useRef<HTMLInputElement>(null);
  const folderInput = useRef<HTMLInputElement>(null);
  const [code, setCode] = useState("");
  const [busy, setBusy] = useState(false);
  const [error, setError] = useState<string | null>(null);

  const onPicked = (input: HTMLInputElement) => {
    const picks = input.files ? picksFromInput(input.files) : [];
    input.value = "";
    send(picks);
  };

  const receive = async (value: string) => {
    const digits = value.replace(/\D/g, "");
    if (digits.length !== 6) return setError("The code has 6 digits.");
    setBusy(true);
    setError(null);
    try {
      const drop = await resolveCode(digits);
      navigate(`/r#${drop}`);
    } catch (err) {
      setError((err as Error).message);
      setBusy(false);
    }
  };

  const count = sharing?.snapshot().files.length ?? 0;
  return (
    <div className="start">
      {sharing && (
        <a className="resume" href="/send" onClick={(e) => { e.preventDefault(); navigate("/send"); }}>
          <span>You are sharing {nFiles(count)}</span>
          <span className="resume-go">Open</span>
        </a>
      )}
      <div className="dropzone">
        <span className="dz-icon"><Icon name="send" size={26} /></span>
        <p className="dz-title">Send files</p>
        <p className="dz-sub">Any size, any number. They go straight to the other device.</p>
        <button className="btn btn-lg btn-block" type="button" onClick={() => fileInput.current?.click()}>Choose files</button>
        {!isPhoneOrTablet && (
          <>
            <button className="btn btn-line btn-block" type="button" onClick={() => folderInput.current?.click()}><Icon name="folder" size={18} /> Choose a folder</button>
            <p className="dz-hint">or drop files and folders here</p>
          </>
        )}
        <input ref={fileInput} type="file" multiple hidden onChange={(e) => onPicked(e.currentTarget)} />
        <input ref={folderInput} type="file" hidden {...{ webkitdirectory: "" }} onChange={(e) => onPicked(e.currentTarget)} />
      </div>
      <form className="receive-form" onSubmit={(e: FormEvent) => { e.preventDefault(); void receive(code); }} noValidate>
        <label className="field" htmlFor="code">
          <span>Receiving? Type the 6-digit code</span>
        </label>
        <div className="code-row">
          <input
            id="code" className="input code-input" inputMode="numeric" autoComplete="one-time-code" pattern="[0-9 ]*" maxLength={7}
            placeholder="000 000" value={code} disabled={busy}
            onChange={(e) => {
              const v = e.target.value.replace(/[^\d ]/g, "");
              setCode(v);
              setError(null);
              if (v.replace(/\D/g, "").length === 6) void receive(v);
            }}
          />
          <button className="btn btn-line" type="submit" disabled={busy}>{busy ? "Opening..." : "Receive"}</button>
        </div>
        {error && <p className="notice notice-error" role="alert">{error}</p>}
      </form>
      <p className="start-note">Free. No account. Nothing is uploaded.</p>
    </div>
  );
}

Change history

Point OpenApps links to openapps.free
b2e053fd98 · 17 hours ago
Bind agent address in policy
ca424dd814 · 19 hours ago
Retry release capture
ae07f1a3fd · 19 hours ago
Use OpenApps service URLs and add contribution strip
0452ce136f · 19 hours ago
Bounded source sync
ca1bc00aa4 · 1 day ago
Bounded source sync
d4982911a0 · 1 day ago
Bounded source sync
18c97fe4ca · 1 day ago