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/ShareDialog.tsx
// Share and links: the edit and view links, your name on the board, and (for owners) renaming,
// turning links off, a new edit link, the admin link and deleting the board.
import { useEffect, useState } from "react";
import { get, set } from "@bounded-sh/client";
import { Modal, copy, toast } from "./ui";
import { deleteBoard, newEditKey } from "./lib/boards";
import { boardOf, rememberBoard } from "./lib/mine";
import { explain } from "./lib/bounded";
import { navigate } from "./lib/router";
import { SITE_URL } from "../functions/lib/config";
import type { BoardDoc } from "./Editor";

function LinkRow({ label, hint, url }: { label: string; hint: string; url: string }) {
  return (
    <div className="link-row">
      <div className="link-row-head">
        <strong>{label}</strong>
        <span className="hint">{hint}</span>
      </div>
      <div className="row link-row-copy">
        <code className="link-code" title={url}>{url}</code>
        <button type="button" className="btn btn-sm" onClick={() => void copy(url, "Link copied")}>Copy</button>
      </div>
    </div>
  );
}

export function ShareDialog({ boardId, board, owner, canEdit, editKey, name, onName, onClose, onDeleting }: {
  boardId: string; board: BoardDoc; owner: boolean; canEdit: boolean; editKey: string | null;
  name: string; onName: (name: string) => void; onClose: () => void; onDeleting: () => Promise<void>;
}) {
  const [ownerKey, setOwnerKey] = useState<string | null>(null);
  const [title, setTitle] = useState(board.title);
  const [busy, setBusy] = useState("");
  const admin = owner ? boardOf(boardId)?.admin : undefined;

  useEffect(() => {
    if (!owner) return;
    get(`editkeys/${boardId}`).then((k) => setOwnerKey((k as { key?: string } | null)?.key ?? null)).catch(() => null);
  }, [owner, boardId]);

  const key = owner ? ownerKey : editKey;
  const viewUrl = `${SITE_URL}/b/${boardId}`;
  const editUrl = key ? `${SITE_URL}/b/${boardId}#edit=${key}` : "";

  const change = async (label: string, patch: Partial<BoardDoc>, done: string) => {
    setBusy(label);
    try {
      await set(`boards/${boardId}`, patch);
      toast(done);
    } catch (err) {
      toast(explain(err));
    }
    setBusy("");
  };

  const rotate = async () => {
    if (!confirm("Make a new edit link? The current edit link stops working right away, for everyone who has it.")) return;
    setBusy("rotate");
    try {
      const k = await newEditKey(boardId);
      setOwnerKey(k);
      history.replaceState(null, "", `/b/${boardId}#edit=${k}`);
      toast("New edit link made. Send it to the people who should edit.");
    } catch (err) {
      toast(explain(err));
    }
    setBusy("");
  };

  const remove = async () => {
    if (!confirm(`Delete "${board.title || "this board"}" for everyone? Everything on it is removed, and its links stop working. This cannot be undone.`)) return;
    setBusy("delete");
    try {
      await onDeleting();
      await deleteBoard(boardId);
      navigate("/");
      toast("Board deleted.");
    } catch (err) {
      toast(explain(err));
      setBusy("");
    }
  };

  const saveTitle = async () => {
    const t = title.replace(/\s+/g, " ").trim().slice(0, 120);
    if (!t || t === board.title) return;
    await change("title", { title: t }, "Board renamed");
    rememberBoard({ id: boardId, title: t });
  };

  return (
    <Modal title="Share and links" onClose={onClose}>
      <div className="share">
        <label className="field">
          <span>Your name</span>
          <input className="input" value={name} maxLength={60} onChange={(e) => onName(e.target.value)} />
          <small>Shown next to your pointer while you are on the board.</small>
        </label>

        {canEdit && board.editOn && editUrl && <LinkRow label="Edit link" hint="Anyone with it can draw and change the board." url={editUrl} />}
        {board.viewOn && <LinkRow label="View link" hint="Anyone with it can look and export, but not change anything." url={viewUrl} />}
        {!board.viewOn && !(canEdit && board.editOn) && <p className="notice">The view link is off. Only people with the edit link can open this board.</p>}
        {owner && !board.editOn && !board.viewOn && <p className="notice">Both links are off. Only you, the owner, can open this board.</p>}

        {owner ? (
          <div className="owner-box">
            <h3 className="h3">Owner settings</h3>
            <label className="field">
              <span>Board name</span>
              <div className="row">
                <input className="input grow" value={title} maxLength={120} onChange={(e) => setTitle(e.target.value)} onKeyDown={(e) => { if (e.key === "Enter") void saveTitle(); }} />
                <button type="button" className="btn btn-line" disabled={busy === "title" || !title.trim() || title.trim() === board.title} onClick={() => void saveTitle()}>Rename</button>
              </div>
            </label>
            <label className="check">
              <input type="checkbox" checked={board.editOn} disabled={Boolean(busy)} onChange={(e) => void change("edit", { editOn: e.target.checked }, e.target.checked ? "Edit link is on" : "Edit link is off. People with it can only look, if the view link is on.")} />
              Edit link is on
            </label>
            <label className="check">
              <input type="checkbox" checked={board.viewOn} disabled={Boolean(busy)} onChange={(e) => void change("view", { viewOn: e.target.checked }, e.target.checked ? "View link is on" : "View link is off")} />
              View link is on
            </label>
            <div className="row wrap-row">
              <button type="button" className="btn btn-line btn-sm" disabled={Boolean(busy)} onClick={() => void rotate()}>Make a new edit link</button>
            </div>
            {admin && <LinkRow label="Admin link" hint="Keep it private. It opens these owner settings on any device." url={`${SITE_URL}/b/${boardId}/admin#k=${admin}`} />}
            <div className="danger-zone">
              <button type="button" className="btn btn-danger btn-sm" disabled={Boolean(busy)} onClick={() => void remove()}>
                {busy === "delete" ? "Deleting..." : "Delete board"}
              </button>
              <span className="hint">Removes it for everyone.</span>
            </div>
          </div>
        ) : (
          <p className="hint">Only the board's owner can rename it, turn its links off or delete it.</p>
        )}
      </div>
    </Modal>
  );
}

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