Skip to content

Commit b87f4de

Browse files
docs: update spectrum-ts documentation for v1.6.0 (#20)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
1 parent 2955d10 commit b87f4de

7 files changed

Lines changed: 165 additions & 75 deletions

File tree

docs-src/spectrum-ts/content.mdx.vel

Lines changed: 44 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
---
22
title: "Content"
3-
description: "Build text, attachments, voice, contacts, polls, rich links, groups, and platform-specific content for outgoing messages"
3+
description: "Build text, attachments, voice, contacts, polls, rich links, groups, replies, edits, typing indicators, and platform-specific content for outgoing messages"
44
---
55

66
import { TypeTooltip } from "/snippets/type-tooltip.mdx";
77

88
{% set ci = symbol("ts:spectrum-ts#ContentInput") %}
99
{% set cb = symbol("ts:spectrum-ts#ContentBuilder") %}
1010

11-
Spectrum exposes a family of content builders — `text`, `attachment`, `voice`, `contact`, `richlink`, `poll`, `group`, and `custom` — plus a string shortcut that's equivalent to `text()`. Any API that takes a <TypeTooltip name="ContentInput" type={`{{ ci.signature }}`} /> accepts a plain string or a <TypeTooltip name="ContentBuilder" type={`{{ cb.signature }}`} />.
11+
Spectrum exposes a family of content builders — `text`, `attachment`, `voice`, `contact`, `richlink`, `poll`, `group`, `custom`, `reaction`, `reply`, `edit`, and `typing` — plus a string shortcut that's equivalent to `text()`. Any API that takes a <TypeTooltip name="ContentInput" type={`{{ ci.signature }}`} /> accepts a plain string or a <TypeTooltip name="ContentBuilder" type={`{{ cb.signature }}`} />.
1212

1313
## Text
1414

@@ -200,7 +200,45 @@ import { custom } from "spectrum-ts";
200200
await space.send(custom({ type: "card", title: "Order Confirmed" }));
201201
```
202202

203-
The raw payload round-trips through the provider's `actions.send` — it's up to the provider to interpret it.
203+
The raw payload round-trips through the provider's `send` action — it's up to the provider to interpret it.
204+
205+
## Replies
206+
207+
Send a threaded reply by wrapping content with the message being replied to. See [Reactions and replies](/spectrum-ts/reactions-and-replies) for the full details and sugar methods.
208+
209+
```ts
210+
import { reply, text } from "spectrum-ts";
211+
212+
await space.send(reply(text("Got it"), message));
213+
```
214+
215+
`reply()` cannot wrap `reply`, `edit`, `reaction`, `group`, or `typing` content.
216+
217+
## Edits
218+
219+
Rewrite the content of a previously-sent outbound message. Edits are fire-and-forget — `space.send(edit(...))` resolves to `undefined`.
220+
221+
```ts
222+
import { edit, text } from "spectrum-ts";
223+
224+
const sent = await space.send("Draft");
225+
await space.send(edit(text("Final version"), sent));
226+
```
227+
228+
`edit()` cannot wrap `edit`, `reply`, `reaction`, `group`, or `typing` content.
229+
230+
## Typing indicators
231+
232+
Send a typing indicator signal through the content pipeline. Defaults to `"start"`.
233+
234+
```ts
235+
import { typing } from "spectrum-ts";
236+
237+
await space.send(typing()); // start typing
238+
await space.send(typing("stop")); // stop typing
239+
```
240+
241+
`space.startTyping()`, `space.stopTyping()`, and `space.responding(fn)` are sugar over `space.send(typing(...))`. Platforms without a typing-indicator API silently no-op.
204242

205243
## Composing multiple items
206244

@@ -215,9 +253,9 @@ await space.send(
215253

216254
This runs one `send()` per item on the underlying provider — not a single compound message. Reach for `group(...)` instead when you specifically want them rendered as one bundled unit.
217255

218-
## Replies
256+
## Replies (sugar)
219257

220-
`message.reply(...)` has the same variadic signature:
258+
`message.reply(...)` has the same variadic signature and delegates to `space.send(reply(...))` internally:
221259

222260
```ts
223261
await message.reply(
@@ -226,4 +264,4 @@ await message.reply(
226264
);
227265
```
228266

229-
On platforms without thread support, `reply()` resolves as a no-op. If you need guaranteed delivery, use `space.send(...)` instead.
267+
On platforms without thread support, `reply()` resolves as a no-op. If you need guaranteed delivery, use `space.send(...)` instead. See [Reactions and replies](/spectrum-ts/reactions-and-replies) for the canonical form and more details.

docs-src/spectrum-ts/custom-platforms.mdx.vel

Lines changed: 38 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -50,30 +50,33 @@ export const myPlatform = definePlatform("my-platform", {
5050
destroyClient: async ({ client }) => { await client.disconnect(); },
5151
},
5252

53-
// Event streams
54-
events: {
55-
async *messages({ client }) {
56-
for await (const msg of client.onMessage()) {
57-
yield {
58-
id: msg.id,
59-
content: { type: "text", text: msg.body },
60-
sender: { id: msg.authorId },
61-
space: { id: msg.channelId },
62-
timestamp: new Date(msg.ts),
63-
};
64-
}
65-
},
53+
// Inbound message stream
54+
async *messages({ client }) {
55+
for await (const msg of client.onMessage()) {
56+
yield {
57+
id: msg.id,
58+
content: { type: "text", text: msg.body },
59+
sender: { id: msg.authorId },
60+
space: { id: msg.channelId },
61+
timestamp: new Date(msg.ts),
62+
};
63+
}
6664
},
6765

68-
// Actions
69-
actions: {
70-
send: async ({ space, content, client }) => {
71-
if (content.type === "text") {
72-
await client.send(space.id, content.text);
73-
}
74-
},
75-
// Optional:
76-
// startTyping, stopTyping, reactToMessage, replyToMessage
66+
// Outbound dispatcher — all content types flow through here
67+
send: async ({ space, content, client }) => {
68+
switch (content.type) {
69+
case "text":
70+
return await client.send(space.id, content.text);
71+
case "reaction":
72+
await client.react(space.id, content.target.id, content.emoji);
73+
return;
74+
case "reply":
75+
return await client.reply(space.id, content.target.id, content.content);
76+
case "typing":
77+
await client.setTyping(space.id, content.state === "start");
78+
return;
79+
}
7780
},
7881

7982
// Optional static properties
@@ -95,32 +98,34 @@ export const myPlatform = definePlatform("my-platform", {
9598
| `space.params` | No | Zod schema for additional space creation parameters — surfaces as the second arg to `platform(app).space()`. |
9699
| `lifecycle.createClient` | Yes | Creates the platform client. Receives `config`, `projectId`, `projectSecret` (both may be `undefined`), and `store`. |
97100
| `lifecycle.destroyClient` | No | Tears down the client on shutdown. Omit if no cleanup is needed. |
98-
| `events.messages` | Yes | Async generator that yields incoming messages. |
101+
| `messages` | Yes | Async generator that yields incoming messages. |
102+
| `send` | Yes | Dispatches a content item to a space. All content types — text, attachments, reactions, replies, edits, typing indicators — flow through this single action. Return a `ProviderMessageRecord` for content that produces a message, or `undefined` for fire-and-forget signals (reactions, typing, edits). |
103+
| `actions.getMessage` | No | Fetches a message by ID from a space. Powers `space.getMessage(id)`. |
99104
| `events.[custom]` | No | Additional async generators for platform-specific events — exposed on `app.[eventName]`. |
100-
| `actions.send` | Yes | Sends a single content item to a space. Invoked once per item when multiple are passed. |
101-
| `actions.startTyping` | No | Shows a typing indicator. |
102-
| `actions.stopTyping` | No | Hides a typing indicator. |
103-
| `actions.reactToMessage` | No | Reacts to a message. Missing → `message.react(...)` becomes a no-op. |
104-
| `actions.replyToMessage` | No | Sends a threaded reply. Missing → `message.reply(...)` becomes a no-op. |
105105
| `message.schema` | No | Zod schema for extra properties on incoming messages. |
106106
| `static` | No | Constants attached to the platform object (e.g. tapback names). |
107107

108108
## Event producers
109109

110110
Every event generator receives `{ client, config, store }` and returns an `AsyncIterable`. The signature is <TypeTooltip name="EventProducer" type={`{{ ep.signature }}`} />.
111111

112+
The core `messages` stream lives at the top level of the definition. Optional custom event streams (presence, read receipts, etc.) live inside `events`:
113+
112114
```ts
115+
// Top-level — required
116+
async *messages({ client }) { /* ... */ },
117+
118+
// Optional custom events
113119
events: {
114-
async *messages({ client }) { /* ... */ },
115-
async *typing({ client }) {
116-
for await (const ev of client.typing()) {
117-
yield { spaceId: ev.chatId, userId: ev.user };
120+
async *presence({ client }) {
121+
for await (const ev of client.presence()) {
122+
yield { spaceId: ev.chatId, userId: ev.user, online: ev.online };
118123
}
119124
},
120125
},
121126
```
122127

123-
Non-`messages` events are auto-wired as flat properties on both the Spectrum instance (`app.typing`) and the narrowed platform instance (`myPlatform(app).typing`).
128+
Custom events are auto-wired as flat properties on both the Spectrum instance (`app.presence`) and the narrowed platform instance (`myPlatform(app).presence`).
124129

125130
## Message extras
126131

docs-src/spectrum-ts/messages.mdx.vel

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,9 @@ for await (const [space, message] of app.messages) {
123123
| `"poll"` | `title: string`, `options: { title: string }[]` |
124124
| `"poll_option"` | `option: { title }`, `poll: Poll`, `selected: boolean`, `title: string` — sent as a vote |
125125
| `"group"` | `items: Message[]` — bundled multi-message unit |
126+
| `"reply"` | `content: Content`, `target: Message` — threaded reply wrapping inner content |
127+
| `"edit"` | `content: Content`, `target: Message` — rewrite of a previously-sent message |
128+
| `"typing"` | `state: "start" \| "stop"` — typing indicator signal |
126129
| `"custom"` | `raw: unknown` — platform-specific structured data |
127130
</Accordion>
128131

docs-src/spectrum-ts/reactions-and-replies.mdx.vel

Lines changed: 68 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,26 @@ description: "React to incoming messages and send threaded replies"
55

66
Both `react` and `reply` live directly on an incoming message. They no-op silently on platforms that don't support the feature — no `try/catch` required.
77

8+
Spectrum also exports first-class `reaction()` and `reply()` content builders that you can pass directly to `space.send(...)` — the sugar methods on `message` delegate through the same `send` pipeline.
9+
810
## Reactions
911

10-
```ts
11-
await message.react("love");
12-
```
12+
<Tabs>
13+
<Tab title="Sugar (message.react)">
14+
```ts
15+
await message.react("love");
16+
```
17+
</Tab>
18+
<Tab title="Canonical (space.send)">
19+
```ts
20+
import { reaction } from "spectrum-ts";
21+
22+
await space.send(reaction("love", message));
23+
```
24+
</Tab>
25+
</Tabs>
26+
27+
Both forms are equivalent — `message.react(emoji)` delegates to `space.send(reaction(emoji, message))` internally.
1328

1429
The reaction string is platform-specific. For iMessage, use the built-in tapback constants:
1530

@@ -19,6 +34,8 @@ import { imessage } from "spectrum-ts/providers/imessage";
1934
await message.react(imessage.tapbacks.laugh);
2035
```
2136

37+
`reaction()` rejects reaction messages as targets — reacting to a reaction throws at build time.
38+
2239
Available tapbacks:
2340

2441
| Constant | Value |
@@ -32,25 +49,62 @@ Available tapbacks:
3249

3350
## Threaded replies
3451

35-
`message.reply(...)` takes the same variadic content input as `space.send(...)`:
52+
<Tabs>
53+
<Tab title="Sugar (message.reply)">
54+
```ts
55+
await message.reply("Replying to your message.");
3656

37-
```ts
38-
await message.reply("Replying to your message.");
57+
await message.reply(
58+
"Here's the attachment you asked for:",
59+
attachment("/path/to/file.pdf"),
60+
);
61+
```
62+
</Tab>
63+
<Tab title="Canonical (space.send)">
64+
```ts
65+
import { reply, text } from "spectrum-ts";
3966

40-
await message.reply(
41-
"Here's the attachment you asked for:",
42-
attachment("/path/to/file.pdf"),
43-
);
44-
```
67+
await space.send(reply(text("Replying to your message."), message));
68+
```
69+
</Tab>
70+
</Tabs>
71+
72+
Both forms are equivalent — `message.reply(content)` wraps each content item in `reply(content, message)` and delegates to `space.send(...)` internally.
4573

4674
On platforms with thread support (iMessage, WhatsApp Business), this sends a threaded reply. On platforms without, the call resolves as a no-op — **the reply is not downgraded to a regular send**. If you need guaranteed delivery, use `space.send(...)` instead.
4775

76+
`reply()` cannot wrap `reply`, `edit`, `reaction`, `group`, or `typing` content — the builder throws at construction time.
77+
78+
## Editing messages
79+
80+
<Tabs>
81+
<Tab title="Sugar (message.edit)">
82+
```ts
83+
const sent = await space.send("Draft");
84+
await sent.edit("Final version");
85+
```
86+
</Tab>
87+
<Tab title="Canonical (space.send)">
88+
```ts
89+
import { edit, text } from "spectrum-ts";
90+
91+
const sent = await space.send("Draft");
92+
await space.send(edit(text("Final version"), sent));
93+
```
94+
</Tab>
95+
</Tabs>
96+
97+
`edit()` takes new content and the outbound message to rewrite. Edits are fire-and-forget — `space.send(edit(...))` resolves to `undefined`.
98+
99+
`edit()` cannot wrap `edit`, `reply`, `reaction`, `group`, or `typing` content.
100+
48101
## When to use what
49102

50103
| Want to | Use |
51104
|---|---|
52105
| Send fresh content into the conversation | `space.send(...)` |
53-
| Reply in-thread to a specific message | `message.reply(...)` |
54-
| React to a specific message | `message.react(reaction)` |
106+
| Reply in-thread to a specific message | `message.reply(...)` or `space.send(reply(...))` |
107+
| React to a specific message | `message.react(emoji)` or `space.send(reaction(emoji, message))` |
108+
| Rewrite a sent message | `message.edit(content)` or `space.send(edit(content, message))` |
55109

56-
`space.send` is the safe default — it works on every platform. Reach for `reply` when the threading behaviour is meaningful (e.g. replying in a busy group chat) and you're willing to accept the no-op on platforms that lack threads.
110+
`space.send` is the safe default — it works on every platform. The sugar methods (`message.reply`, `message.react`, `message.edit`) and the canonical content builders (`reply()`, `reaction()`, `edit()`) are interchangeable — they both route through the same `send` pipeline.

docs-src/spectrum-ts/spaces-and-users.mdx.vel

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,8 @@ await space.startTyping();
5959
await space.stopTyping();
6060
```
6161

62+
These are sugar for `space.send(typing("start"))` and `space.send(typing("stop"))` — see [Content](/spectrum-ts/content#typing-indicators) for the canonical form.
63+
6264
### Automatic with `responding`
6365

6466
`responding` is the recommended pattern. It guarantees the typing indicator is cleared even if the inner function throws:

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
"eslint-plugin-format": "^2.0.1",
2828
"husky": "^9.1.7",
2929
"oxfmt": "^0.44.0",
30-
"spectrum-ts": "1.4.0",
30+
"spectrum-ts": "1.5.0",
3131
"tsx": "^4.21.0",
3232
"typescript": "^5.9.3"
3333
}

pnpm-lock.yaml

Lines changed: 9 additions & 21 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)