eleven / messenger Public

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

Plain git — no account needed to clone.

Download

Download this file

1// The to-do mini-app: your own personal checklist. It's a `personal` app, so it
2// lives as its own thread in the sidebar and renders full-pane; `/todo buy milk`
3// typed in ANY chat files into it. Every action is a normal encrypted message on
4// the app's thread, so the list syncs across your devices for free and the server
5// only ever sees ciphertext โ€” never a to-do.
6//
7// ๐Ÿค– The app is SCHEMA-DESCRIBED: schemas/todo.json declares the state shape,
8// event folds, actions, and views (docs/app-schema.md); schema-engine.js folds
9// and schema-render.js draws โ€” the same schema the native apps interpret with
10// SwiftUI, so the platforms can't drift. What stays hand-written here is the
11// glue a schema doesn't cover: commands, sidebar preview/unread, the chat intro.
12//
13// Relative import (not "/static/apps.js") so the reducer is exercisable in Node
14// tests while still resolving to /static/apps.js in the browser.
15import { defineApp, namespace } from "../apps.js";
16import { ICON_SQUARE_CHECK } from "../icons.js";
17import { schemaEvents, schemaInitialState, schemaRender, schemaStyles } from "../schema-render.js";
18import schema from "./schemas/todo.json" with { type: "json" };
19
20// The schema (schemas/todo.json) is the source of truth for the `events` map
21// keys, so there's no drift there โ€” but the hand-written glue below (commands,
22// chatMessage/preview/unread) still compares against the wire type by hand.
23// N.ev("add") โ†’ "todo.add" keeps THOSE comparisons locked to the schema's own
24// name instead of a second hardcoded "todo" prefix.
25const N = namespace(schema.name);
26
27// A to-do is { id, text, done, doneAt, by } โ€” see schemas/todo.json for the
28// authoritative shape, folds (idempotent add, grace-window partition) and views.
29export const app = defineApp({
30 name: schema.name,
31 title: schema.title,
32 blurb: schema.blurb, // sidebar subtitle before first use (sync w/ native)
33 glyph: schema.glyph,
34 symbol: "checkmark.square", // native chrome: the SF Symbols name iOS/Mac render (docs/icons.md)
35 logo: ICON_SQUARE_CHECK,
36 // Also addable to a group chat (โ‹ฎ โ†’ Add apps): one SHARED list for the whole
37 // group โ€” the same events and folds, applied to the group thread's log
38 // instead of your personal one. "@To-do buy milk" in the conversation adds
39 // one (the default command, addressed by mention โ€” see runAppMention).
40 // Yours or a chat's โ€” the same reducer serves every copy; only the thread
41 // differs (the places model, docs/space-apps.md). The SPACE place is parked
42 // while the places UI is hidden (ยง The pullback: two same-named rows in the
43 // thread list read as a bug) โ€” the reducer needs nothing more, so restoring
44 // it is adding "space" back to this line.
45 places: ["you", "chat"],
46 initialState: schemaInitialState(schema),
47 events: schemaEvents(schema),
48 commands: {
49 // The global routing verb: /todo typed in ANY chat files into your app-thread.
50 "/todo": {
51 desc: "Add a to-do",
52 args: "<task>",
53 run: (args, ctx) => addTodo(args, ctx, "/todo"),
54 },
55 // The in-thread verb โ€” and the DEFAULT: plain text typed into the To-do chat
56 // runs /add, so "buy milk" just becomes a to-do (see defaultCommand below).
57 "/add": {
58 desc: "Add a to-do",
59 args: "<task>",
60 local: true,
61 run: (args, ctx) => addTodo(args, ctx, "/add"),
62 },
63 },
64 defaultCommand: "/add",
65 composerPlaceholder: "Add a to-doโ€ฆ",
66
67 // Adding a to-do reads as an action in the chat history (a /me-style line);
68 // done/reopen/remove/move happen in the Interface view and carry no text, so
69 // they don't clutter the chat. In a group the host prefixes the actor's name.
70 chatMessage(e, opts) {
71 // In a GROUP the line lands among unrelated conversation, so it says
72 // where the item went ("to the to-dos"); the personal thread IS the
73 // list, so the shorter line stands. The host prefixes the actor
74 // ("You"/their name) on action lines โ€” never the app.
75 if (e.type === N.ev("add") && e.text) {
76 return { text: opts && opts.group ? `added โ€œ${e.text}โ€ to the to-dos` : `added โ€œ${e.text}โ€`, action: true };
77 }
78 return null;
79 },
80
81 // Sidebar presence: a new to-do previews in the chat list and lights the
82 // unread dot โ€” how a group notices shared-list activity without opening the
83 // thread. Your own adds never light the dot (the host's ownMessage gate), and
84 // ticks/removals/moves stay quiet everywhere.
85 preview(e, opts) {
86 // opts.you: the host resolved the actor as the reader (docs/native-apps
87 // voice contract) โ€” sidebar lines read "You addedโ€ฆ" for your own act.
88 if (e.type === N.ev("add") && e.text) {
89 const actor = opts && opts.you ? "You" : e.name || "Someone";
90 return `โœ“ ${actor} added โ€œ${e.text}โ€ to the to-dos`;
91 }
92 return null;
93 },
94 unread: (e) => e.type === N.ev("add"),
95
96 // The empty-state intro shown the first time you open this bot's chat.
97 chatIntro:
98 "This is the start of your chat history with To-do. Just type something to add it as a to-do โ€” e.g. buy milk. You can see, add, and check off to-dos in the Interface view.",
99
100 styles: schemaStyles,
101 render: schemaRender(schema),
102});
103
104// The one add-a-to-do core both verbs share โ€” /todo from anywhere, /add (and
105// plain typed text, via defaultCommand) in the app's own thread. `verb` only
106// shapes the usage line a bare command gets back.
107function addTodo(args, ctx, verb) {
108 const text = args.trim();
109 if (!text) return ctx.reply({ text: `Usage: ${verb} <task> โ€” e.g. ${verb} buy milk` });
110 ctx.emit({ type: N.ev("add"), id: ctx.id(), text });
111 // Name the destination list (ctx.place): the same /todo can land in your own
112 // list, this chat's, or the space's โ€” the reply saying WHICH is the places
113 // grammar's disclosure. (The framework also stamps a place footer on routed
114 // replies, so apps that don't word it still disclose it.)
115 const dest = ctx.place === "space" ? "the space to-do list" : ctx.place === "chat" ? "this chat's to-do list" : "your to-do list";
116 // `short` is the terse one-liner the reply calms down to (~30s): "To-do
117 // added โ€œโ€ฆโ€", vs. the loud "Added โ€œโ€ฆโ€ to โ€ฆ" card.
118 ctx.reply({ text: `Added โ€œ${text}โ€ to ${dest}.`, short: `added โ€œ${text}โ€`, actions: [{ label: "View to-do", run: ctx.open }] });
119}
120