// Emoji browser: `/emoji [search]` opens a COMMAND PANEL (the transient // results surface above the composer — see apps.js) with the matching emoji // from the curated shortcode set; tapping one drops it into your message and // dismisses the panel. The reference implementation for panels: no events, no // state, no thread of its own — the command and its temporary surface are the // whole app. import { defineApp } from "../apps.js"; import { ICON_SMILE } from "../icons.js"; import { EMOJI } from "../emoji.js"; // searchEmoji filters the shortcode set by a substring of the name and // collapses aliases (laughing/satisfied are one glyph) to one tile each, // keeping the first name as the label. Pure, so it's unit-testable in Node. export function searchEmoji(query) { const q = (query || "").trim().toLowerCase(); const seen = new Set(); const out = []; for (const [name, ch] of Object.entries(EMOJI)) { if (q && !name.includes(q)) continue; if (seen.has(ch)) continue; seen.add(ch); out.push({ name, ch }); } return out; } export const app = defineApp({ name: "emoji", title: "Emoji", glyph: "😊", symbol: "face.smiling", // native chrome: the SF Symbols name iOS/Mac render (docs/icons.md) logo: ICON_SMILE, // Command-only: no personal thread to route to (the command acts where you // typed it) and nothing to instantiate from the Apps menu. places: [], // command-only utility — acts where you type, lives nowhere initialState: () => ({}), events: {}, commands: { "/emoji": { desc: "browse emoji — tap one to add it to your message", args: "[search]", run(args, ctx) { const q = (args || "").trim(); const results = searchEmoji(q); if (!results.length) { ctx.reply({ text: `No emoji match “${q}”. Try \`/emoji cat\`, or a bare \`/emoji\` to browse them all.` }); return; } const opened = ctx.panel({ title: q ? `Emoji matching “${q}”` : "Emoji — tap to add", render(el, panel) { const grid = document.createElement("div"); grid.className = "emoji-grid"; for (const r of results) { const b = document.createElement("button"); b.type = "button"; b.className = "emoji-grid-btn"; b.textContent = r.ch; b.title = ":" + r.name + ":"; b.setAttribute("aria-label", r.name.replace(/_/g, " ")); b.onclick = () => { panel.insertText(r.ch); panel.close(); }; grid.appendChild(b); } el.appendChild(grid); }, }); // No chat surface to draw a panel in (shouldn't happen where a // composer exists, but stay honest): answer with a reply instead. if (!opened) ctx.reply({ text: results.slice(0, 30).map((r) => r.ch).join(" ") }); }, }, }, styles: ` .emoji-grid { display: flex; flex-wrap: wrap; gap: 2px; } .emoji-grid-btn { border: none; background: none; font-size: 22px; line-height: 1; padding: 6px; border-radius: 8px; cursor: pointer; } .emoji-grid-btn:hover { background: var(--bubble-them); } `, });