eleven / messenger Public

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

Plain git — no account needed to clone.

Download

Download this file

1// Mood check-in: the COMMAND-PANEL example for member apps. `/mood [note]`
2// opens a panel of feeling buttons; tapping one dispatches a `mood.set` event
3// into your Mood thread (an action line in its chat, a row in its history)
4// and dismisses the panel. This is the whole "panel buttons dispatch events"
5// shape in ~80 lines, written against the DECLARATIVE panel form โ€” the
6// command calls ctx.panel({ title, data }) and the top-level
7// panel(data, el, panelCtx) hook draws it โ€” which is the one form the
8// member-app sandbox supports (a render closure can't cross its bridge), and
9// which works identically compiled-in. To try it as a member app: paste this
10// file into App Studio and deploy. Sandbox-legal by construction: imports
11// only from ../apps.js, no fetch, no host doorways.
12import { defineApp, addItem } from "../apps.js";
13
14const MOODS = ["๐Ÿ˜„", "๐Ÿ™‚", "๐Ÿ˜", "๐Ÿ˜ฎโ€๐Ÿ’จ", "๐Ÿ˜–"];
15
16export const app = defineApp({
17 name: "mood",
18 title: "Mood",
19 glyph: "๐ŸŒค๏ธ",
20 initialState: () => ({ items: [] }),
21
22 events: {
23 "mood.set": (s, e) =>
24 e.id && typeof e.mood === "string" && MOODS.includes(e.mood)
25 ? { items: addItem(s.items, { id: e.id, mood: e.mood, note: typeof e.note === "string" ? e.note.slice(0, 200) : "", by: e.name || "", ts: e.ts || 0 }) }
26 : s,
27 },
28
29 commands: {
30 "/mood": {
31 desc: "log how you're feeling",
32 args: "[note]",
33 run(args, ctx) {
34 ctx.panel({ title: "How are you feeling?", data: { note: (args || "").trim() } });
35 },
36 },
37 },
38
39 // Companion projection: a check-in reads as an action line in the chat view.
40 chatMessage(e) {
41 if (e.type !== "mood.set") return null;
42 return { text: `is feeling ${e.mood}${e.note ? " โ€” " + e.note : ""}`, action: true };
43 },
44
45 // The panel: five buttons; a tap emits the event and closes. `data` is what
46 // the command passed (the note typed after /mood); panelCtx offers
47 // { emit, id, me, getState, close, insertText } in both worlds.
48 panel(data, el, ctx) {
49 const row = document.createElement("div");
50 row.className = "mood-row";
51 for (const mood of MOODS) {
52 const b = document.createElement("button");
53 b.type = "button";
54 b.className = "mood-btn";
55 b.textContent = mood;
56 b.onclick = async () => {
57 await ctx.emit({ type: "mood.set", id: ctx.id(), mood, note: (data && data.note) || "" });
58 ctx.close();
59 };
60 row.appendChild(b);
61 }
62 el.appendChild(row);
63 },
64
65 styles: `
66.mood-row { display: flex; gap: 8px; padding: 6px 2px; }
67.mood-btn {
68 border: none; background: var(--bubble-them); border-radius: 10px;
69 font-size: 26px; line-height: 1; padding: 10px 12px; cursor: pointer;
70}
71.mood-btn:hover { outline: 2px solid var(--accent); }
72.mood-item { display: flex; gap: 10px; align-items: baseline; padding: 6px 2px; border-bottom: 1px solid var(--border); }
73.mood-note { color: var(--muted); font-size: 13px; }
74`,
75
76 // The app-thread surface: newest first, the history of check-ins.
77 render(state, el) {
78 const items = [...((state && state.items) || [])].sort((a, b) => b.ts - a.ts);
79 for (const it of items) {
80 const row = document.createElement("div");
81 row.className = "mood-item";
82 const glyph = document.createElement("span");
83 glyph.textContent = it.mood;
84 const note = document.createElement("span");
85 note.className = "mood-note";
86 note.textContent = it.note || new Date(it.ts).toLocaleString();
87 row.append(glyph, note);
88 el.appendChild(row);
89 }
90 },
91});
92