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 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.
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
Bind AGENT_ADDRESS in policy constants
Use functions.openapps.xyz alias, add OpenApp contribution strip
Bounded source sync
Bounded source sync
Bounded source sync
Bounded source sync