Everyone has the same buttons; the view is personal

The host's rights and the co-host are gone: anyone in a session may
start a new page, put an image on the page, load a kept page, hide,
expand and shrink, and move or remove images and Hides; the page left
is kept in every notebook.  Fit and Normal scale act on your own screen
only.  A session goes on without whoever started it, and a newcomer to
an empty room starts page 1 with New.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
ackandClaude Fable 5.1 committed 2026-10-18 20:22:13 +02:00
1 parent 5d63a0f758
commit 60ad747e32
10 files changed
+215 -354

No files matched your search

+32 -82
View File
@@ -230,7 +230,7 @@ const CONNECTION_GRACE_MS = 1500;
* The notice line between the toolbar and the page. It says nothing
* while all is well. A STANDING notice, kept by its source until that
* source clears it, is a state: not connected, the page not being
* saved, no page from the host yet. A PASSING notice is a report of
* saved, no page from the others yet. A PASSING notice is a report of
* one event, kept for a few seconds: a page kept or exported, a file
* that was not an image. Standing notices come before a passing one.
*/
@@ -470,7 +470,6 @@ async function enterSession(invitationText, name, notebook) {
input('invitation').value = config.site ? formatInvitationLink(config.site, plainInvitation) : plainInvitation;
showInvitationCode(config.site ? input('invitation').value : '');
const isHost = identity.id === invitation.hostId;
let tool = /** @type {Tool} */ ('pen');
/** The Shape tool's current shape, remembered in this browser (Andy, 2026-10-14). */
let shape = shapeNamed(localStorage.getItem(SHAPE_KEY));
@@ -501,7 +500,7 @@ async function enterSession(invitationText, name, notebook) {
function showPageNumber() {
const waiting = session.page.number === 0;
element('page_number').textContent = waiting ? 'No page yet' : `Page ${session.page.number}`;
standingNotice('page', waiting ? 'Waiting for the host’s page' : '', false);
standingNotice('page', waiting ? 'Waiting for the others’ page; New starts one' : '', false);
}
/** @type {ReturnType<typeof setTimeout> | undefined} */
let connectionNoticeTimer;
@@ -568,18 +567,6 @@ async function enterSession(invitationText, name, notebook) {
textEditor?.layout();
writer.changed();
},
onViewCommand: (command) => {
log(`The host's view: ${command === 'fit' ? 'Fit' : 'Normal scale'}`);
if (command === 'fit') renderer.fit();
else renderer.sheetScale();
},
onPermissionsChanged: () => {
log(`May start pages: ${session.mayMakePages() ? 'yes' : 'no'}` +
(session.isHost ? `; guests allowed: ${session.permitted.size}` : ''));
showHostTools();
showParticipants(session, renderer);
refreshArrangeCues();
},
onParticipantsChanged: () => {
syncLayers();
showParticipants(session, renderer);
@@ -846,12 +833,11 @@ async function enterSession(invitationText, name, notebook) {
/**
* With the Arrange tool chosen, mark everything a press can move or
* resize with a blue box and corners (Andy, 2026-10-14): the images
* and Hides for the host and co-hosts, one's own text blocks and
* shapes; free text, which has no width to resize, gets the box without
* the corners. The one being held is left out, the green grip standing
* in its place until the pointer lifts. Called whenever the images,
* the Hides, one's own text or shapes, the right, or the hold may have
* changed.
* and Hides, one's own text blocks and shapes; free text, which has
* no width to resize, gets the box without the corners. The one being
* held is left out, the green grip standing in its place until the
* pointer lifts. Called whenever the images, the Hides, one's own
* text or shapes, or the hold may have changed.
*/
function refreshArrangeCues() {
if (tool !== 'arrange') {
@@ -861,16 +847,14 @@ async function enterSession(invitationText, name, notebook) {
/** @type {{rect: import('./page_model.js').Rectangle, kind: 'image' | 'text' | 'hide' | 'shape' | 'stroke', corners: boolean}[]} */
const cues = [];
const page = session.page;
if (session.mayMakePages()) {
page.images.forEach((im, index) => {
if (held?.kind === 'image' && held.index === index) return;
cues.push({ kind: 'image', rect: { left: im.x, top: im.y, width: im.width, height: im.height }, corners: true });
});
page.hides.forEach((h, index) => {
if (held?.kind === 'hide' && held.index === index) return;
cues.push({ kind: 'hide', rect: { left: h.x, top: h.y, width: h.width, height: h.height }, corners: true });
});
}
page.images.forEach((im, index) => {
if (held?.kind === 'image' && held.index === index) return;
cues.push({ kind: 'image', rect: { left: im.x, top: im.y, width: im.width, height: im.height }, corners: true });
});
page.hides.forEach((h, index) => {
if (held?.kind === 'hide' && held.index === index) return;
cues.push({ kind: 'hide', rect: { left: h.x, top: h.y, width: h.width, height: h.height }, corners: true });
});
for (const block of session.ownLayer.texts.values()) {
if (held?.kind === 'text' && held.id === block.id) continue;
cues.push({ kind: 'text', rect: textBounds(block), corners: block.height !== null });
@@ -1110,13 +1094,13 @@ async function enterSession(invitationText, name, notebook) {
if (rects.length) return rects;
const page = session.page;
const hide = page.hideAt(x, y, 0);
if (hide && session.mayMakePages()) {
if (hide) {
const h = page.hides[hide.index];
return [{ left: h.x, top: h.y, width: h.width, height: h.height }];
}
const found = page.imageAt(x, y, 0);
const image = found && page.images[found.index];
if (image && (session.mayMakePages() || image.by === session.identity.id)) {
if (image) {
return [{ left: image.x, top: image.y, width: image.width, height: image.height }];
}
return [];
@@ -1143,15 +1127,15 @@ async function enterSession(invitationText, name, notebook) {
zoomTo: (pixelsUp) => renderer.zoomDragTo(pixelsUp),
endZoom: () => renderer.endZoomDrag(),
// A hold moves what is under the pen, or resizes it from a corner: a
// Hide rectangle or an image (for the host and co-hosts), or a
// text block of our own. The dot drawn meanwhile goes.
// Hide rectangle, an image, or a text block of our own. The dot
// drawn meanwhile goes.
beginHold: (clientX, clientY) => {
const [x, y] = renderer.pageFromClient(clientX, clientY);
const reach = renderer.cornerReach();
const page = session.page;
/** @type {Held | null} */
let found = null;
const hide = session.mayMakePages() ? page.hideAt(x, y, reach) : null;
const hide = page.hideAt(x, y, reach);
if (hide) {
const h = page.hides[hide.index];
found = hold('hide', hide.index, h.id, { left: h.x, top: h.y, width: h.width, height: h.height }, hide.corner, x, y);
@@ -1190,7 +1174,7 @@ async function enterSession(invitationText, name, notebook) {
const stroke = shapes[shapeAt.index];
const box = /** @type {import('./page_model.js').Rectangle} */ (strokeBox(stroke));
if (session.beginShapeEdit(stroke.id)) found = hold('shape', shapeAt.index, stroke.id, box, shapeAt.corner, x, y);
} else if (session.mayMakePages()) {
} else {
const image = page.imageAt(x, y, reach);
if (image) {
const index = session.raiseImage(image.index);
@@ -1277,8 +1261,8 @@ async function enterSession(invitationText, name, notebook) {
lastEmptyTap = 0;
return;
}
// A tap of the eraser on a Hide rectangle or an image removes it
// (the right to make pages, or having added the image).
// A tap of the eraser on a Hide rectangle or an image removes it,
// for everyone (by anyone since 2026-10-18).
let took = erased.strokes + erased.texts > 0;
if (!took) {
const hide = session.page.hideAt(x, y, 0);
@@ -1458,8 +1442,8 @@ async function enterSession(invitationText, name, notebook) {
textEditor?.layout();
viewChanged();
};
// The host's Fit and Normal scale ("A4" until 2026-10-14) reach everyone (Andy, 2026-10-08); a guest's
// are the guest's own.
// Fit and Normal scale ("A4" until 2026-10-14) are each person's own
// (2026-10-18; the host's reached everyone from 2026-10-08 until then).
// The page and view commands live in the Page menu (Andy, 2026-10-14:
// seldom used, and the one row is precious).
attachPopup(element('page_menu_button'), element('page_menu'));
@@ -1505,14 +1489,8 @@ async function enterSession(invitationText, name, notebook) {
log(`The manual could not be copied: ${error}`);
}
});
element('zoom_fit').addEventListener('click', () => {
renderer.fit();
session.sendViewCommand('fit');
});
element('zoom_sheet').addEventListener('click', () => {
renderer.sheetScale();
session.sendViewCommand('sheet');
});
element('zoom_fit').addEventListener('click', () => renderer.fit());
element('zoom_sheet').addEventListener('click', () => renderer.sheetScale());
// "Expand" makes the sheet itself fill the window at the present zoom,
// in both directions; nothing on it moves (Andy, 2026-10-08 for the
// width, both ways 2026-10-11).
@@ -1541,21 +1519,9 @@ async function enterSession(invitationText, name, notebook) {
element('desk').addEventListener('scroll', viewChanged, { passive: true });
window.addEventListener('pagehide', saveView);
// The host drives: the host starts new pages, and may let a guest do so.
function showHostTools() {
// New and Image are the host's; Notebooks, in the same group so that
// nothing moves for the host, is everyone's (the chooser reserves
// loading).
for (const id of ['new_page', 'load_image']) element(id).hidden = !session.mayMakePages();
// The Hide tool is for the host and co-hosts; losing the right
// while it is chosen puts the pen back in hand.
const hideTool = /** @type {HTMLElement} */ (document.querySelector('[data-tool=hide]'));
hideTool.hidden = !session.mayMakePages();
if (hideTool.hidden && tool === 'hide') /** @type {HTMLElement} */ (document.querySelector('[data-tool=pen]')).click();
element('zoom_expand').hidden = !session.mayMakePages();
element('zoom_shrink').hidden = !session.mayMakePages();
}
showHostTools();
// Everyone has every button (Andy and his teacher, 2026-10-18; until
// then New, Image, Hide, Expand and Shrink were the host's and the
// co-hosts').
element('new_page').addEventListener('click', () => {
log('New page asked for');
session.newPage();
@@ -1567,7 +1533,7 @@ async function enterSession(invitationText, name, notebook) {
* @param {Blob} file
*/
async function loadImage(file) {
if (!session.mayMakePages() || session.page.number === 0) return;
if (session.page.number === 0) return;
try {
if (!session.addImage(await prepareImage(file))) {
throw new Error('The page has no room for another image.');
@@ -1588,7 +1554,7 @@ async function enterSession(invitationText, name, notebook) {
// A deleted notebook leaves the toolbar's list.
showNotebooks(sessionPicker).catch(() => {});
});
element('load_notebook_page').addEventListener('click', () => { chooser.open(writer.notebook, session.mayMakePages()); });
element('load_notebook_page').addEventListener('click', () => { chooser.open(writer.notebook); });
// Image files are chosen, and pictures of the page exported, through a
// chooser of our own, which begins where the notebooks are.
@@ -2056,20 +2022,6 @@ function showParticipants(session, renderer) {
const present = participant.self || participant.peer !== null;
item.classList.toggle('absent', !present);
if (session.isHost && !participant.self) {
// A checkbox with its label to the left, like Extend (Andy, 2026-10-09).
const allowLine = document.createElement('label');
allowLine.className = 'allow_line';
allowLine.setAttribute('data-help', 'Make this participant a co-host: they may then start pages, put images on the page and arrange them, load notebook pages, expand, shrink and hide, as you can.');
const allow = document.createElement('input');
allow.type = 'checkbox';
allow.className = 'allow';
allow.checked = session.permitted.has(participant.id);
allow.addEventListener('change', () => session.setPermitted(participant.id, allow.checked));
allowLine.append('Co-host ', allow);
item.append(allowLine);
}
const label = document.createElement('label');
const visible = document.createElement('input');
visible.type = 'checkbox';
@@ -2082,8 +2034,6 @@ function showParticipants(session, renderer) {
const roles = [];
if (participant.self) roles.push('you');
if (participant.host) roles.push('host');
else if (!session.isHost && session.permitted.has(participant.id)) roles.push('co-host');
if (!present) roles.push('away');
if (roles.length) {
const role = document.createElement('span');