cat / goq
clone

turns a linux box into a private steam streaming appliance: plug in, tap your key, play from another computer

fix/release-build-and-order
goq / portal / roster-ui.mjs
roster-ui.mjs 3.5K
1const MAX_VIEWERS = 8;2const MAX_HANDLE_BYTES = 32;34function validatePresenceId(value) {5 if (typeof value !== 'string'6 || value.length < 17 || value.length > MAX_HANDLE_BYTES8 || !/^[A-Za-z0-9-]+$/.test(value)) {9 throw new Error('viewer presence id must be a bounded opaque handle');10 }11 return value;12}1314function compactHandle(handle) {15 return handle.length <= 18 ? handle : `${handle.slice(0, 8)}...${handle.slice(-6)}`;16}1718export function rosterPresentation(snapshot) {19 if (!snapshot || !Array.isArray(snapshot.viewers)20 || snapshot.viewers.length < 1 || snapshot.viewers.length > MAX_VIEWERS) {21 throw new Error('roster must contain 1..=8 viewers');22 }23 const seen = new Set();24 const holder = snapshot.focus?.state === 'held' ? snapshot.focus.holder : null;25 const rows = snapshot.viewers.map((viewer) => {26 const handle = validatePresenceId(viewer?.presence_id);27 if (seen.has(handle)) throw new Error('roster contains a duplicate viewer');28 seen.add(handle);29 const isHolder = holder === handle && snapshot.focus.session_id === viewer.session_id;30 const input = viewer.input_capable === true ? 'input eligible' : 'view only';31 const role = isHolder ? 'holds control' : input;32 return Object.freeze({33 handle: compactHandle(handle),34 fullHandle: handle,35 you: viewer.you === true,36 inputCapable: viewer.input_capable === true,37 focusHolder: isHolder,38 role,39 adaptive: 'shared stream',40 });41 });42 const self = rows.filter((row) => row.you);43 if (self.length !== 1 || self[0].fullHandle !== snapshot.self_presence_id) {44 throw new Error('roster requires one matching you marker');45 }46 if (holder !== null && !rows.some((row) => row.focusHolder)) {47 throw new Error('focus holder is absent from the roster');48 }49 const focus = holder === null50 ? snapshot.focus?.state === 'neutralizing' ? 'control resetting' : 'control available'51 : self[0].focusHolder ? 'you hold control' : `${compactHandle(holder)} holds control`;52 return Object.freeze({53 count: rows.length,54 rows: Object.freeze(rows),55 focus,56 shell: `${rows.length} ${rows.length === 1 ? 'viewer' : 'viewers'} ยท ${focus}`,57 });58}5960function appendText(document, parent, className, text) {61 const element = document.createElement('span');62 element.className = className;63 element.textContent = text;64 parent.appendChild(element);65 return element;66}6768export function renderRoster(root, snapshot) {69 if (!root || typeof root.replaceChildren !== 'function' || !root.ownerDocument) {70 throw new TypeError('roster root must be a DOM element');71 }72 const presentation = rosterPresentation(snapshot);73 const { ownerDocument: document } = root;74 const fragment = document.createDocumentFragment();75 for (const row of presentation.rows) {76 const item = document.createElement('li');77 item.className = 'roster-viewer';78 if (row.you) item.classList.add('you');79 if (row.focusHolder) item.classList.add('focus-holder');80 appendText(document, item, 'roster-handle', row.handle);81 if (row.you) appendText(document, item, 'roster-you', 'you');82 appendText(document, item, 'roster-role', row.role);83 appendText(document, item, 'roster-adaptive', row.adaptive);84 item.setAttribute(85 'aria-label',86 `${row.handle}${row.you ? ', you' : ''}, ${row.role}, ${row.adaptive}`,87 );88 fragment.appendChild(item);89 }90 root.replaceChildren(fragment);91 root.setAttribute('aria-label', `${presentation.count} connected viewers. ${presentation.focus}`);92 return presentation;93}