// The to-do mini-app: your own personal checklist. It's a `personal` app, so it // lives as its own thread in the sidebar and renders full-pane; `/todo buy milk` // typed in ANY chat files into it. Every action is a normal encrypted message on // the app's thread, so the list syncs across your devices for free and the server // only ever sees ciphertext โ€” never a to-do. // // ๐Ÿค– The app is SCHEMA-DESCRIBED: schemas/todo.json declares the state shape, // event folds, actions, and views (docs/app-schema.md); schema-engine.js folds // and schema-render.js draws โ€” the same schema the native apps interpret with // SwiftUI, so the platforms can't drift. What stays hand-written here is the // glue a schema doesn't cover: commands, sidebar preview/unread, the chat intro. // // Relative import (not "/static/apps.js") so the reducer is exercisable in Node // tests while still resolving to /static/apps.js in the browser. import { defineApp, namespace } from "../apps.js"; import { ICON_SQUARE_CHECK } from "../icons.js"; import { schemaEvents, schemaInitialState, schemaRender, schemaStyles } from "../schema-render.js"; import schema from "./schemas/todo.json" with { type: "json" }; // The schema (schemas/todo.json) is the source of truth for the `events` map // keys, so there's no drift there โ€” but the hand-written glue below (commands, // chatMessage/preview/unread) still compares against the wire type by hand. // N.ev("add") โ†’ "todo.add" keeps THOSE comparisons locked to the schema's own // name instead of a second hardcoded "todo" prefix. const N = namespace(schema.name); // A to-do is { id, text, done, doneAt, by } โ€” see schemas/todo.json for the // authoritative shape, folds (idempotent add, grace-window partition) and views. export const app = defineApp({ name: schema.name, title: schema.title, blurb: schema.blurb, // sidebar subtitle before first use (sync w/ native) glyph: schema.glyph, symbol: "checkmark.square", // native chrome: the SF Symbols name iOS/Mac render (docs/icons.md) logo: ICON_SQUARE_CHECK, // Also addable to a group chat (โ‹ฎ โ†’ Add apps): one SHARED list for the whole // group โ€” the same events and folds, applied to the group thread's log // instead of your personal one. "@To-do buy milk" in the conversation adds // one (the default command, addressed by mention โ€” see runAppMention). // Yours or a chat's โ€” the same reducer serves every copy; only the thread // differs (the places model, docs/space-apps.md). The SPACE place is parked // while the places UI is hidden (ยง The pullback: two same-named rows in the // thread list read as a bug) โ€” the reducer needs nothing more, so restoring // it is adding "space" back to this line. places: ["you", "chat"], initialState: schemaInitialState(schema), events: schemaEvents(schema), commands: { // The global routing verb: /todo typed in ANY chat files into your app-thread. "/todo": { desc: "Add a to-do", args: "", run: (args, ctx) => addTodo(args, ctx, "/todo"), }, // The in-thread verb โ€” and the DEFAULT: plain text typed into the To-do chat // runs /add, so "buy milk" just becomes a to-do (see defaultCommand below). "/add": { desc: "Add a to-do", args: "", local: true, run: (args, ctx) => addTodo(args, ctx, "/add"), }, }, defaultCommand: "/add", composerPlaceholder: "Add a to-doโ€ฆ", // Adding a to-do reads as an action in the chat history (a /me-style line); // done/reopen/remove/move happen in the Interface view and carry no text, so // they don't clutter the chat. In a group the host prefixes the actor's name. chatMessage(e, opts) { // In a GROUP the line lands among unrelated conversation, so it says // where the item went ("to the to-dos"); the personal thread IS the // list, so the shorter line stands. The host prefixes the actor // ("You"/their name) on action lines โ€” never the app. if (e.type === N.ev("add") && e.text) { return { text: opts && opts.group ? `added โ€œ${e.text}โ€ to the to-dos` : `added โ€œ${e.text}โ€`, action: true }; } return null; }, // Sidebar presence: a new to-do previews in the chat list and lights the // unread dot โ€” how a group notices shared-list activity without opening the // thread. Your own adds never light the dot (the host's ownMessage gate), and // ticks/removals/moves stay quiet everywhere. preview(e, opts) { // opts.you: the host resolved the actor as the reader (docs/native-apps // voice contract) โ€” sidebar lines read "You addedโ€ฆ" for your own act. if (e.type === N.ev("add") && e.text) { const actor = opts && opts.you ? "You" : e.name || "Someone"; return `โœ“ ${actor} added โ€œ${e.text}โ€ to the to-dos`; } return null; }, unread: (e) => e.type === N.ev("add"), // The empty-state intro shown the first time you open this bot's chat. chatIntro: "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.", styles: schemaStyles, render: schemaRender(schema), }); // The one add-a-to-do core both verbs share โ€” /todo from anywhere, /add (and // plain typed text, via defaultCommand) in the app's own thread. `verb` only // shapes the usage line a bare command gets back. function addTodo(args, ctx, verb) { const text = args.trim(); if (!text) return ctx.reply({ text: `Usage: ${verb} โ€” e.g. ${verb} buy milk` }); ctx.emit({ type: N.ev("add"), id: ctx.id(), text }); // Name the destination list (ctx.place): the same /todo can land in your own // list, this chat's, or the space's โ€” the reply saying WHICH is the places // grammar's disclosure. (The framework also stamps a place footer on routed // replies, so apps that don't word it still disclose it.) const dest = ctx.place === "space" ? "the space to-do list" : ctx.place === "chat" ? "this chat's to-do list" : "your to-do list"; // `short` is the terse one-liner the reply calms down to (~30s): "To-do // added โ€œโ€ฆโ€", vs. the loud "Added โ€œโ€ฆโ€ to โ€ฆ" card. ctx.reply({ text: `Added โ€œ${text}โ€ to ${dest}.`, short: `added โ€œ${text}โ€`, actions: [{ label: "View to-do", run: ctx.open }] }); }