/** A space default pattern whose ordered panel occurrences own composition. */ import { computed, type Default, handler, ifElse, NAME, pattern, spaceOf, type Stream, UI, type VNode, wish, Writable, } from "commonfabric"; import FabriChatRoom from "../fabrichat/room.tsx"; import { ensureFileStore } from "../fabrifile/store.tsx"; import { admitPanel, assertRemovable, externalUrl, insertionIndex, validatePanel, } from "./admission.tsx"; import type { BindMeetingRequest, ChatRoomCell, ChatRoomChoice, LoomInput, LoomOutput, LoomStyle, MeetingAssociationCell, Panel, PanelAdmission, PanelPosition, PanelRetarget, PanelTitle, ParticipantProfile, PortableMeetingIdentity, Presentation, PrivatePanel, ViewerState, } from "./schemas.tsx"; import { addParticipant, rosterProfiles } from "./participants.tsx"; type State = { panels: Writable[]>; presentation: Writable; }; /** Return occurrences whose target differs from the complete piece link. */ function withoutPiece( list: readonly Writable[], piece: Writable, ): Writable[] { return list.filter((panel) => { const value = panel.get(); return value.kind !== "piece" || !value.piece.equalLinks(piece); }); } /** Throws unless `panel` is one of the occurrences in `list`. */ function assertInLoom( list: readonly Writable[], panel: Writable, ): void { if (!list.some((existing) => existing.equals(panel))) { throw new Error("The panel is no longer in this Loom"); } } /** * Whether `hidden`, the occurrences a viewer has hidden, holds `panel`. A * viewer who has never hidden a panel may read no list at all, which hides * nothing. */ function isHidden( hidden: readonly Writable[] | undefined, panel: Writable, ): boolean { return (hidden ?? []).some((entry) => entry.equals(panel)); } /** The shared panels one viewer has not hidden, in the shared order. */ function shownPanels( shared: readonly Writable[], hidden: readonly Writable[] | undefined, ): Writable[] { return shared.filter((panel) => !isHidden(hidden, panel)); } /** * The private panels anchored to `panel`, which a viewer is shown just ahead * of it while it is shown, in the order of the private list. */ function privatePanelsBefore( privates: readonly PrivatePanel[] | undefined, panel: Writable, ): Writable[] { return (privates ?? []) .filter((entry) => entry.before?.equals(panel) === true) .map((entry) => entry.panel); } /** * The private panels a viewer is shown after every shared one: those anchored * to none of the panels in `shown`, in the order of the private list. */ function trailingPrivatePanels( shown: readonly Writable[], privates: readonly PrivatePanel[] | undefined, ): Writable[] { return (privates ?? []) .filter((entry) => !shown.some((panel) => entry.before?.equals(panel) === true) ) .map((entry) => entry.panel); } /** * The panels one viewer is shown: the shared ones they have not hidden, in the * shared order, with each of their private panels just ahead of the shared one * it is anchored to, or after all of them while that one is not shown. Private * panels anchored alike keep the order of the private list. */ function viewerList( shared: readonly Writable[], hidden: readonly Writable[] | undefined, privates: readonly PrivatePanel[] | undefined, ): Writable[] { const shown = shownPanels(shared, hidden); return [ ...shown.flatMap(( panel, ) => [...privatePanelsBefore(privates, panel), panel]), ...trailingPrivatePanels(shown, privates), ]; } const removePiece = handler<{ piece: Writable }, State>( ({ piece }, { panels, presentation }) => { const list = panels.get(); const next = withoutPiece(list, piece); // Every occurrence of the piece goes or none does: one that another // principal added refuses the whole unregistration. for (const panel of list) { if (!next.some((kept) => kept.equals(panel))) { assertRemovable(panel, panels); } } panels.set(next); const current = presentation.get(); presentation.set({ stagedPanels: current.stagedPanels.filter((panel) => next.some((member) => member.equals(panel)) ), ...(current.focusedPanel && next.some((member) => member.equals(current.focusedPanel!)) ? { focusedPanel: current.focusedPanel } : {}), }); }, ); const removePanel = handler<{ panel: Writable }, State>( ({ panel }, { panels, presentation }) => { const list = panels.get(); if (list.some((existing) => existing.equals(panel))) { assertRemovable(panel, panels); } panels.set(list.filter((existing) => !existing.equals(panel))); const current = presentation.get(); presentation.set({ stagedPanels: current.stagedPanels.filter((existing) => !existing.equals(panel) ), ...(current.focusedPanel && !current.focusedPanel.equals(panel) ? { focusedPanel: current.focusedPanel } : {}), }); }, ); const movePanel = handler( ({ panel, before }, { panels }) => { const list = panels.get(); assertInLoom(list, panel); insertionIndex(list, before); if (before?.equals(panel)) return; const next = list.filter((existing) => !existing.equals(panel)); next.splice(insertionIndex(next, before), 0, panel); panels.set(next); }, ); const setPresentation = handler( (event, { panels, presentation }) => { const list = panels.get(); const staged = event.stagedPanels; for (let i = 0; i < staged.length; i++) { if (!list.some((panel) => panel.equals(staged[i]))) { throw new Error("A staged panel is no longer in this Loom"); } if (staged.slice(0, i).some((panel) => panel.equals(staged[i]))) { throw new Error("Staged panels must be unique"); } } if ( event.focusedPanel && !staged.some((panel) => panel.equals(event.focusedPanel!)) ) { throw new Error("The focused panel must be staged"); } presentation.set({ stagedPanels: staged, ...(event.focusedPanel ? { focusedPanel: event.focusedPanel } : {}), }); }, ); /** * Names `room` as the Loom's chat room, or clears it when the event names * none; of the root's other handlers, only `ensureChatRoom` changes it, and * only while it names none. The room must live in the Loom's own space, so * that its members, the principals its space's access list admits, are the * Loom's. The designation is independent of the panels: it persists while no * panel shows the room, and a reader that needs to know whether one does looks * for the room among the piece panels. * * @throws When `room` lives in another space. */ const setChatRoom = handler( ({ room }, { chatRoom }) => { // `chatRoom` holds a record rather than a link, so its space is the // Loom's. if (room !== undefined && spaceOf(room) !== spaceOf(chatRoom)) { throw new Error("The chat room must be in this Loom's space"); } chatRoom.set(room === undefined ? {} : { room }); }, ); /** * Names a new FabriChat room as the Loom's chat room, unless the Loom names * one already, in which case it changes nothing. The room is the Loom's * space's own chat: it lives in that space, is not its root, and has no * `about`, so its participants are the ones this root lists. It is not added * as a panel, nor to the piece registry, so it never appears as one. Whatever * may call `setChatRoom` may call this. */ const ensureChatRoom = handler( (_, { chatRoom }) => { if (chatRoom.get().room !== undefined) return; chatRoom.set({ room: FabriChatRoom({}) }); }, ); /** Returns the supported portable keys when `value` carries their full contract. */ function portableMeetingIdentity( value: unknown, ): PortableMeetingIdentity | undefined { if (typeof value !== "object" || value === null) return undefined; const prototype = Object.getPrototypeOf(value); if (prototype !== Object.prototype && prototype !== null) return undefined; if (!("scheme" in value) || value.scheme !== "ical-uid-slot") { return undefined; } const itemKey = "item_key" in value ? value.item_key : undefined; const occurrenceKey = "occurrence_key" in value ? value.occurrence_key : undefined; if ( typeof itemKey !== "string" || itemKey.length !== 64 || !/^[0-9a-f]{64}$/.test(itemKey) || typeof occurrenceKey !== "string" || occurrenceKey.length !== 64 || !/^[0-9a-f]{64}$/.test(occurrenceKey) ) return undefined; return { scheme: "ical-uid-slot", item_key: itemKey, occurrence_key: occurrenceKey, }; } /** * Binds an empty Loom to one meeting key and confirms a same-key replay. * A matching binding can acquire portable evidence once; conflicting evidence * leaves the association intact. Legacy replays preserve portable metadata. * The identity arrives as raw event data so invalid values remain visible to * the validator instead of being omitted by a typed projection. * A different key or unrecognized stored value leaves the association intact. * The read participates in the handler transaction, so competing bindings * retry against the association that committed first. A request whose reply * cell is the association itself changes nothing and gets no response, since * any response would replace the association. A reply cell within or above the * association is not detected: a cell can be compared with another only for * identity. */ const bindMeeting = handler< BindMeetingRequest, { meeting: MeetingAssociationCell; storedFields: Writable< Record | Default> >; } >((event, { meeting, storedFields }) => { const { key, portableIdentity, reply } = event; if (reply?.equals(meeting)) return; if ( typeof key !== "string" || key.length !== 64 || !/^[0-9a-f]{64}$/.test(key) ) { reply?.set({ status: "invalid-key" }); return; } const identity = portableMeetingIdentity(portableIdentity); if ("portableIdentity" in event && identity === undefined) { reply?.set({ status: "invalid-key" }); return; } // The untyped field projection retains a malformed `key` that the typed // view would omit, so malformed state cannot look like an empty record. // Only a plain record can: a Fabric special object, such as a temporal // value, is an object with no enumerable keys of its own. const current = storedFields.get(); const prototype = typeof current === "object" && current !== null ? Object.getPrototypeOf(current) : undefined; if (prototype !== Object.prototype && prototype !== null) { reply?.set({ status: "invalid-state" }); return; } if ("key" in current) { const storedKey = meeting.get()?.key; if ( typeof storedKey !== "string" || storedKey.length !== 64 || !/^[0-9a-f]{64}$/.test(storedKey) ) { reply?.set({ status: "invalid-state" }); return; } if (storedKey !== key) { reply?.set({ status: "conflict", key: storedKey }); return; } if (identity !== undefined) { if ("portableIdentity" in current) { const storedIdentity = portableMeetingIdentity( meeting.get()?.portableIdentity, ); if (storedIdentity === undefined) { reply?.set({ status: "invalid-state" }); return; } if ( storedIdentity.item_key !== identity.item_key || storedIdentity.occurrence_key !== identity.occurrence_key ) { reply?.set({ status: "conflict", key: storedKey }); return; } } else { meeting.key("portableIdentity").set(identity); } } reply?.set({ status: "already-bound", key: storedKey }); return; } if (Object.keys(current).length !== 0) { reply?.set({ status: "invalid-state" }); return; } meeting.set( identity === undefined ? { key } : { key, portableIdentity: identity }, ); reply?.set({ status: "bound", key }); }); /** The portable appearance a viewer can consume, or no style. */ function normalizedStyle(selected: LoomStyle): LoomStyle { if ( selected === null || !selected || typeof selected.id !== "string" || /^[a-z][a-z0-9-]{0,63}$/.exec(selected.id)?.[0] !== selected.id || !["native", "light", "dark"].includes(selected.mode) ) { return null; } return { id: selected.id, mode: selected.mode }; } /** Validates and replaces the confirmed appearance in one write. */ const setStyle = handler<{ style: LoomStyle }, { style: Writable }>( (event, { style }) => { const selected = normalizedStyle(event.style); if (event.style !== null && selected === null) { throw new Error("Invalid Loom style identifier or mode"); } style.set(selected); }, ); /** Sets the Loom's title, which is also its name. */ const retitleLoom = handler<{ title: string }, { title: Writable }>( (event, { title }) => { title.set(event.title); }, ); /** * Sets the title `panel` shows in place of its target's; an empty title * clears it, leaving no override for a copy of the panel to carry. Only * `titleOverride` is written, so the panel's target and its adder, with the * label the root stamped there, stay as they were. Any member may retitle any * panel: only removal turns on who added it. * * @throws When `panel` is not in this Loom. */ const retitlePanel = handler< PanelTitle, { panels: Writable[]> } >(({ panel, titleOverride }, { panels }) => { assertInLoom(panels.get(), panel); panel.key("titleOverride").set( titleOverride === "" ? undefined : titleOverride, ); }); /** * Points a URL or piece panel at `target` in place, so the occurrence keeps * its place in the list, its staging and focus, its title and its adder. Only * `kind` and the target's key are written, and a change of kind removes the * other kind's key: a write of the whole panel would carry the adder's fields, * which only `admitPanel` may write. The target the panel leaves is untouched. * * @throws When `panel` is not in this Loom, when it is a document, whose * content is its producer's, or when `target` names a URL a panel may not hold. */ const retargetPanel = handler< PanelRetarget, { panels: Writable[]> } >(({ panel, target }, { panels }) => { assertInLoom(panels.get(), panel); const kind = panel.get().kind; if (kind === "document") { throw new Error("A document panel shows its producer's content"); } if (target.kind === "url" && externalUrl(target.url) === undefined) { throw new Error("A URL panel requires an HTTP(S) URL without credentials"); } if (kind !== target.kind) { panel.update(kind === "url" ? { url: undefined } : { piece: undefined }); } if (target.kind === "url") panel.update({ kind: "url", url: target.url }); else panel.update({ kind: "piece", piece: target.piece }); }); /** * Hides `panel` from the acting principal's own view of the Loom, in every * session of theirs, and from nobody else's; the shared list keeps it. * Hiding a panel already hidden changes nothing. * * @throws When `panel` is not in this Loom. */ const hidePanel = handler< { panel: Writable }, { panels: Writable[]>; hiddenPanels: Writable[]>; } >(({ panel }, { panels, hiddenPanels }) => { assertInLoom(panels.get(), panel); hiddenPanels.addUnique(panel); }); /** * Adds `panel` to the acting principal's private panels, which nobody else is * shown, just ahead of `before` among the shared ones, or after all of them. * The occurrence must live in a space other than the Loom's: every member may * read what the Loom's space holds, so a private panel is private by the access * list of the space it lives in. Adding one already among them changes * nothing. * * @throws When `panel` lives in the Loom's space, when it is a URL panel whose * URL a panel may not hold, or when `before` is not in this Loom. */ const addPrivatePanel = handler< PanelPosition, { panels: Writable[]>; privatePanels: Writable; } >(({ panel, before }, { panels, privatePanels }) => { if (spaceOf(panel) === spaceOf(panels)) { throw new Error("A private panel must live outside the Loom's space"); } validatePanel(panel.get()); insertionIndex(panels.get(), before); const list = privatePanels.get() ?? []; if (list.some((entry) => entry.panel.equals(panel))) return; privatePanels.set([ ...list, before === undefined ? { panel } : { panel, before }, ]); }); /** * Takes `panel` off the acting principal's private panels. The occurrence * itself, in its own space, is left as it is. */ const removePrivatePanel = handler< { panel: Writable }, { privatePanels: Writable } >(({ panel }, { privatePanels }) => { const list = privatePanels.get() ?? []; const next = list.filter((entry) => !entry.panel.equals(panel)); if (next.length !== list.length) privatePanels.set(next); }); /** * Moves one of the acting principal's private panels to show just ahead of * `before`: a shared occurrence, or another of their private panels, whose * anchor it then takes. Without `before`, it shows after every shared panel, * last among the private panels shown there. * * @throws When `panel` is not one of the acting principal's private panels, or * when `before` is neither in this Loom nor one of them. */ const movePrivatePanel = handler< PanelPosition, { panels: Writable[]>; privatePanels: Writable; } >(({ panel, before }, { panels, privatePanels }) => { const list = privatePanels.get() ?? []; if (!list.some((entry) => entry.panel.equals(panel))) { throw new Error("The panel is not one of your private panels"); } const rest = list.filter((entry) => !entry.panel.equals(panel)); if (before === undefined) { privatePanels.set([...rest, { panel }]); return; } if (panels.get().some((shared) => shared.equals(before))) { privatePanels.set([...rest, { panel, before }]); return; } const index = rest.findIndex((entry) => entry.panel.equals(before)); if (index < 0) { throw new Error("The insertion anchor is no longer in this Loom"); } const anchor = rest[index].before; privatePanels.set([ ...rest.slice(0, index), anchor === undefined ? { panel } : { panel, before: anchor }, ...rest.slice(index), ]); }); /** * Shows `panel` again in the acting principal's own view. A panel that is not * hidden, including one no longer in the Loom, is left alone. */ const unhidePanel = handler< { panel: Writable }, { hiddenPanels: Writable[]> } >(({ panel }, { hiddenPanels }) => { hiddenPanels.removeByValue(panel); }); /** Present one linked occurrence without reading a piece's protected fields. */ function renderPanel(panel: Writable) { const value = panel.get(); if (value.kind === "piece") { return ( {value.titleOverride ?

{value.titleOverride}

: null} Open piece
); } if (value.kind === "url") { const url = externalUrl(value.url); if (url === undefined) { return

This panel requires an HTTP(S) URL without credentials.

; } return (

{value.titleOverride || url}

Open in new tab

If this page cannot be embedded, open it in a new tab.