canvas-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 93fdaafeda16
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 788ba0fa01083b300ad50913
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/BoardPage.tsx
// Opens a board on this device: joins it (the board id is enough to view while the view link is on),
// presents the edit key from the link, and from an admin link becomes an owner. Then loads the editor.
import { lazy, Suspense, useCallback, useEffect, useState } from "react";
import { get, set, setMany, serverTimestamp } from "@bounded-sh/client";
import { me, explain } from "./lib/bounded";
import { navigate } from "./lib/router";
import { boardOf, editPath, rememberBoard } from "./lib/mine";
import type { BoardDoc } from "./Editor";

const Editor = lazy(() => import("./Editor"));

const hashParam = (name: string) => new URLSearchParams(location.hash.slice(1)).get(name);

interface Ready { uid: string; owner: boolean; editKey: string | null; board: BoardDoc }

function Loading() {
  return <div className="board-loading"><span className="spinner" /> Opening the board...</div>;
}

function Unavailable({ title, text }: { title: string; text: string }) {
  return (
    <div className="wrap page-pad narrow">
      <h1 className="h1">{title}</h1>
      <p className="lede" style={{ marginTop: 14 }}>{text}</p>
      <a className="btn" style={{ marginTop: 24 }} href="/" onClick={(e) => { e.preventDefault(); navigate("/"); }}>Start a new board</a>
    </div>
  );
}

export function BoardPage({ boardId, admin }: { boardId: string; admin: boolean }) {
  const [state, setState] = useState<"opening" | "ready" | "missing" | "badkey" | "gone" | "error">("opening");
  const [ready, setReady] = useState<Ready | null>(null);
  const [error, setError] = useState("");

  useEffect(() => {
    let cancelled = false;
    const done = (s: typeof state) => { if (!cancelled) setState(s); };
    (async () => {
      try {
        const uid = await me();
        const linkKey = admin ? null : hashParam("edit");
        const adminKey = admin ? hashParam("k") : null;
        let editKey = linkKey || boardOf(boardId)?.edit || null;
        const joinPath = `boards/${boardId}/joins/${uid}`;
        const join = (await get(joinPath)) as { key?: string | null } | null;
        if (!join) {
          try {
            await set(joinPath, { at: serverTimestamp(), ...(editKey ? { key: editKey } : {}) });
          } catch {
            return done("missing");
          }
        } else if (editKey && join.key !== editKey) {
          await set(joinPath, { key: editKey });
        }
        let owner = Boolean(await get(`boards/${boardId}/owners/${uid}`));
        if (adminKey) {
          if (!owner) {
            try {
              await setMany([
                { path: `boardclaims/${boardId}/c/${uid}`, document: { key: adminKey } },
                { path: `boards/${boardId}/owners/${uid}`, document: { at: serverTimestamp() } },
              ]);
              owner = true;
            } catch {
              return done("badkey");
            }
            rememberBoard({ id: boardId, admin: adminKey });
          } else if (!boardOf(boardId)?.admin) {
            // Already an owner, so the key was not checked: keep a key this browser already has over one from a link.
            rememberBoard({ id: boardId, admin: adminKey });
          }
        }
        const board = (await get(`boards/${boardId}`)) as BoardDoc | null;
        if (!board) return done("missing");
        if (owner) {
          // Owners can always edit; keep the current edit link at hand for sharing.
          editKey = ((await get(`editkeys/${boardId}`)) as { key?: string } | null)?.key ?? editKey;
          if (editKey) await set(joinPath, { key: editKey });
          // The admin key never stays in the address bar; this browser keeps it in "Your boards".
          if (admin && editKey) history.replaceState(null, "", editPath(boardId, editKey));
        }
        rememberBoard({ id: boardId, title: board.title, ...(linkKey || owner ? { edit: editKey ?? undefined } : {}) });
        if (!cancelled) {
          setReady({ uid, owner, editKey, board });
          setState("ready");
        }
      } catch (err) {
        if (!cancelled) { setError(explain(err)); setState("error"); }
      }
    })();
    return () => { cancelled = true; };
  }, [boardId, admin]);

  const onGone = useCallback(() => setState("gone"), []);

  if (state === "opening") return <Loading />;
  if (state === "missing") return <Unavailable title="This board is not available." text="It may have been deleted, or its owner turned its links off. Check the link you were sent: board links are long and every character counts." />;
  if (state === "badkey") return <Unavailable title="That admin link is not right." text="The key at the end of the link does not match this board. Copy the whole admin link again." />;
  if (state === "gone") return <Unavailable title="This board is no longer available." text="Its owner deleted it or turned its links off while you had it open." />;
  if (state === "error" || !ready) return <div className="wrap page-pad narrow"><p className="notice notice-error">{error || "Something went wrong. Reload to try again."}</p></div>;
  return (
    <Suspense fallback={<Loading />}>
      <Editor boardId={boardId} uid={ready.uid} owner={ready.owner} editKey={ready.editKey} initial={ready.board} onGone={onGone} />
    </Suspense>
  );
}

Change history

Point OpenApps links to openapps.free
93fdaafeda · 18 hours ago
Bind AGENT_ADDRESS in policy constants
bf8f43b096 · 20 hours ago
Use functions.openapps.xyz alias, add OpenApp contribution strip
56f1a381b2 · 20 hours ago
Bounded source sync
9ec707aeb8 · 1 day ago
Bounded source sync
5645481c23 · 1 day ago
Bounded source sync
c38fe71874 · 1 day ago
Bounded source sync
28c0350b40 · 1 day ago