eleven / messenger Public

Clone
git clone https://amadan.net/eleven/messenger

Plain git — no account needed to clone.

Download

Download this file

1// Roster display helpers — the shared vocabulary for showing members: label
2// resolution (my nickname vs the published name), search matching, presence
3// lines, the agent tag, and the picker row/section builders. Used by the
4// thread list, the typeahead, the pickers, and the group-action sheets.
5// Extracted verbatim from shell.js.
6import { displayNameFor, presenceLabel } from "/static/ui.js";
7import { roster, nicknames } from "/static/shellstate.js";
8
9export function rosterLabel(userId) {
10 const e = roster.find((x) => x.user_id === userId);
11 return displayNameFor(nicknames[userId], e && e.label) || "Someone";
12}
13
14// The published label only — for anything that leaves this device inside a
15// shared payload (membership announce texts, invite pre-fills): my private
16// names for people must never ride where other members can read them.
17export function publishedLabel(userId) {
18 const e = roster.find((x) => x.user_id === userId);
19 return e && e.label ? e.label : "Someone";
20}
21
22// Does a roster entry match a (lowercased) search query? Both names count:
23// what pickers display is my name for them, so searching the name you can see
24// must find them — and searching their published name must too.
25export function rosterMatches(e, q) {
26 if ((e.label || "").toLowerCase().includes(q)) return true;
27 const nick = nicknames[e.user_id];
28 return !!nick && !!nick.name && nick.name.toLowerCase().includes(q);
29}
30
31// Presence ("online" | "away" | "offline" | "") rides on roster entries: seeded
32// by /api/roster, kept live by "presence" WS frames. "" = unknown (no dot).
33export function rosterPresence(userId) {
34 const e = roster.find((x) => x.user_id === userId);
35 return (e && e.presence) || "";
36}
37
38// Fill `el` with a status line — a coloured dot (green/yellow/red) + the
39// Online/Away/Offline text, on an inline-flex wrapper so the dot centres
40// against the text. Empties the element when the state is unknown.
41export function setPresenceLine(el, presence) {
42 el.textContent = "";
43 const label = presenceLabel(presence);
44 if (!label) return;
45 const line = document.createElement("span");
46 line.className = "presence-line";
47 const dot = document.createElement("span");
48 dot.className = "p-dot " + presence;
49 dot.setAttribute("aria-hidden", "true");
50 line.appendChild(dot);
51 line.appendChild(document.createTextNode(label));
52 el.appendChild(line);
53}
54
55// A small "BOT" tag to mark an agent in the roster picker, so it's clear before
56// you add it that it isn't a person. Self-contained styling (no CSS dependency).
57// Accent by default (a discovery cue in pickers/lists); pass muted=true inline
58// on messages, where it repeats on every reply and bright accent reads as noise.
59export function agentTag(muted, kind) {
60 const t = document.createElement("span");
61 const ai = kind === "ai";
62 t.textContent = ai ? "AI" : "BOT";
63 t.title = ai
64 ? "An AI helper — what you send it is sent to an AI provider to answer"
65 : "An automated helper (bot), not a person — it runs on this server";
66 // The chip is centered by its flex parent (each name line is a flex row with
67 // align-items:center — see the call sites), so it needs no baseline nudge;
68 // flex:none keeps it from shrinking, line-height:1 keeps the box tight.
69 t.style.cssText =
70 "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;" +
71 (muted ? "background:var(--border,#ddd);color:var(--muted,#666);" : "background:var(--accent,#888);color:#fff;");
72 return t;
73}
74
75// A picker row's name: the label, plus for agents the BOT tag and the admin's
76// one-line description (the Helpers catalog blurb — the "what am I adding?"
77// hint), or for a person their own "about me" line (decrypted from the roster).
78export function pickerName(e) {
79 const nm = document.createElement("span");
80 // Name + badge on a centered flex line; the description (if any) sits below.
81 const line = document.createElement("span");
82 line.style.cssText = "display:flex;align-items:center;";
83 const txt = document.createElement("span");
84 txt.textContent = displayNameFor(nicknames[e.user_id], e.label);
85 line.appendChild(txt);
86 if (e.is_agent) line.appendChild(agentTag(false, e.kind));
87 nm.appendChild(line);
88 const blurb = e.is_agent ? e.description : e.about;
89 if (blurb) {
90 const d = document.createElement("span");
91 d.className = "helper-desc";
92 d.textContent = blurb;
93 nm.appendChild(d);
94 }
95 return nm;
96}
97
98// Section heading inside a pick-list (People / Helpers).
99export function pickerSection(title) {
100 const s = document.createElement("div");
101 s.className = "pick-section";
102 s.textContent = title;
103 return s;
104}
105