// Roster display helpers — the shared vocabulary for showing members: label // resolution (my nickname vs the published name), search matching, presence // lines, the agent tag, and the picker row/section builders. Used by the // thread list, the typeahead, the pickers, and the group-action sheets. // Extracted verbatim from shell.js. import { displayNameFor, presenceLabel } from "/static/ui.js"; import { roster, nicknames } from "/static/shellstate.js"; export function rosterLabel(userId) { const e = roster.find((x) => x.user_id === userId); return displayNameFor(nicknames[userId], e && e.label) || "Someone"; } // The published label only — for anything that leaves this device inside a // shared payload (membership announce texts, invite pre-fills): my private // names for people must never ride where other members can read them. export function publishedLabel(userId) { const e = roster.find((x) => x.user_id === userId); return e && e.label ? e.label : "Someone"; } // Does a roster entry match a (lowercased) search query? Both names count: // what pickers display is my name for them, so searching the name you can see // must find them — and searching their published name must too. export function rosterMatches(e, q) { if ((e.label || "").toLowerCase().includes(q)) return true; const nick = nicknames[e.user_id]; return !!nick && !!nick.name && nick.name.toLowerCase().includes(q); } // Presence ("online" | "away" | "offline" | "") rides on roster entries: seeded // by /api/roster, kept live by "presence" WS frames. "" = unknown (no dot). export function rosterPresence(userId) { const e = roster.find((x) => x.user_id === userId); return (e && e.presence) || ""; } // Fill `el` with a status line — a coloured dot (green/yellow/red) + the // Online/Away/Offline text, on an inline-flex wrapper so the dot centres // against the text. Empties the element when the state is unknown. export function setPresenceLine(el, presence) { el.textContent = ""; const label = presenceLabel(presence); if (!label) return; const line = document.createElement("span"); line.className = "presence-line"; const dot = document.createElement("span"); dot.className = "p-dot " + presence; dot.setAttribute("aria-hidden", "true"); line.appendChild(dot); line.appendChild(document.createTextNode(label)); el.appendChild(line); } // A small "BOT" tag to mark an agent in the roster picker, so it's clear before // you add it that it isn't a person. Self-contained styling (no CSS dependency). // Accent by default (a discovery cue in pickers/lists); pass muted=true inline // on messages, where it repeats on every reply and bright accent reads as noise. export function agentTag(muted, kind) { const t = document.createElement("span"); const ai = kind === "ai"; t.textContent = ai ? "AI" : "BOT"; t.title = ai ? "An AI helper — what you send it is sent to an AI provider to answer" : "An automated helper (bot), not a person — it runs on this server"; // The chip is centered by its flex parent (each name line is a flex row with // align-items:center — see the call sites), so it needs no baseline nudge; // flex:none keeps it from shrinking, line-height:1 keeps the box tight. t.style.cssText = "flex:none;margin-left:6px;font-size:0.7em;line-height:1;font-weight:600;letter-spacing:0.03em;padding:2px 5px;border-radius:6px;vertical-align:middle;" + (muted ? "background:var(--border,#ddd);color:var(--muted,#666);" : "background:var(--accent,#888);color:#fff;"); return t; } // A picker row's name: the label, plus for agents the BOT tag and the admin's // one-line description (the Helpers catalog blurb — the "what am I adding?" // hint), or for a person their own "about me" line (decrypted from the roster). export function pickerName(e) { const nm = document.createElement("span"); // Name + badge on a centered flex line; the description (if any) sits below. const line = document.createElement("span"); line.style.cssText = "display:flex;align-items:center;"; const txt = document.createElement("span"); txt.textContent = displayNameFor(nicknames[e.user_id], e.label); line.appendChild(txt); if (e.is_agent) line.appendChild(agentTag(false, e.kind)); nm.appendChild(line); const blurb = e.is_agent ? e.description : e.about; if (blurb) { const d = document.createElement("span"); d.className = "helper-desc"; d.textContent = blurb; nm.appendChild(d); } return nm; } // Section heading inside a pick-list (People / Helpers). export function pickerSection(title) { const s = document.createElement("div"); s.className = "pick-section"; s.textContent = title; return s; }