/** A space default pattern whose ordered panel occurrences own composition. */ import { computed, handler, ifElse, NAME, pattern, spaceOf, type Stream, UI, type VNode, wish, Writable, } from "commonfabric"; import FabriChatRoom from "../fabrichat/room.tsx"; import { admitPanel, assertRemovable, externalUrl, insertionIndex, validatePanel, } from "./admission.tsx"; import type { ChatRoomCell, ChatRoomChoice, LoomInput, LoomOutput, Panel, PanelAdmission, PanelPosition, PanelRetarget, PanelTitle, ParticipantProfile, 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({}) }); }, ); /** 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.