Skip to content

Commit 5fe34a0

Browse files
committed
update docs for spectrum-ts
1 parent b875fb0 commit 5fe34a0

3 files changed

Lines changed: 225 additions & 8 deletions

File tree

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

Lines changed: 154 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
---
22
title: "Content"
3-
description: "Build text, attachments, and platform-specific content for outgoing messages"
3+
description: "Build text, attachments, voice, contacts, polls, rich links, groups, 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 provides three content builders — `text`, `attachment`, 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`, 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 }}`} />.
1212

1313
## Text
1414

@@ -40,9 +40,159 @@ await space.send(attachment(buffer, {
4040

4141
If the MIME type can't be inferred from the name and you didn't pass `options.mimeType`, `attachment()` throws when the content is built.
4242

43+
## Voice
44+
45+
Send a voice note. Same input shape as `attachment` — a path or a `Buffer` plus optional metadata.
46+
47+
```ts
48+
import { voice } from "spectrum-ts";
49+
50+
// From a file path
51+
await space.send(voice("/path/to/note.m4a"));
52+
53+
// From a buffer — duration in seconds is optional but useful for waveform UIs
54+
await space.send(voice(buffer, {
55+
name: "note.m4a",
56+
mimeType: "audio/mp4",
57+
duration: 12,
58+
}));
59+
```
60+
61+
Platforms that don't support voice notes typically downgrade to a regular audio attachment. If the MIME type can't be inferred and `options.mimeType` is omitted, the builder throws at send time.
62+
63+
<Accordion title="Voice" description="Resolved voice content delivered alongside the message.">
64+
| Field | Type | Description |
65+
|---|---|---|
66+
| `mimeType` | `string` | The audio MIME type (e.g. `audio/mp4`). |
67+
| `name` | `string` (optional) | Filename for the underlying clip. |
68+
| `duration` | `number` (optional) | Length in seconds. |
69+
| `size` | `number` (optional) | Byte length, when known up front. |
70+
| `read()` | `() => Promise<Buffer>` | Materialize the bytes. |
71+
| `stream()` | `() => Promise<ReadableStream>` | Stream the bytes — preferred for large clips. |
72+
</Accordion>
73+
74+
## Contacts
75+
76+
Share contact cards. The `contact()` builder takes either a structured `ContactInput`, a vCard string, a `vcf` instance, or a known `User` paired with optional `ContactDetails`.
77+
78+
<Tabs>
79+
<Tab title="Structured">
80+
```ts
81+
import { contact } from "spectrum-ts";
82+
83+
await space.send(contact({
84+
name: { first: "Ada", last: "Lovelace" },
85+
phones: [{ value: "+15551234567", type: "mobile" }],
86+
emails: [{ value: "ada@example.com", type: "work" }],
87+
}));
88+
```
89+
</Tab>
90+
<Tab title="From a User">
91+
```ts
92+
import { contact } from "spectrum-ts";
93+
94+
// Attach extra details on top of an existing platform user
95+
await space.send(contact(alice, {
96+
name: { first: "Alice", last: "Anderson" },
97+
org: { name: "Acme", title: "Engineer" },
98+
}));
99+
```
100+
</Tab>
101+
<Tab title="From vCard">
102+
```ts
103+
import { contact, fromVCard } from "spectrum-ts";
104+
105+
const vcf = await readFile("/path/to/ada.vcf", "utf8");
106+
await space.send(contact(vcf));
107+
108+
// Or parse first if you want to inspect/edit the fields
109+
const parsed = fromVCard(vcf);
110+
await space.send(contact({ ...parsed, note: "Met at conference" }));
111+
```
112+
</Tab>
113+
</Tabs>
114+
115+
`fromVCard(vcf)` parses a vCard string into a `ContactInput`; `toVCard(contact)` serializes a resolved `Contact` back to vCard.
116+
117+
<Accordion title="ContactInput" description="The fields you can populate on a contact card. All fields are optional except where the receiving platform requires at least one identifying field.">
118+
| Field | Type | Description |
119+
|---|---|---|
120+
| `name` | `{ formatted?, first?, last?, middle?, prefix?, suffix? }` | Structured display name. |
121+
| `phones` | `Array<{ value, type? }>` | Phone numbers. `type` is `"mobile" \| "home" \| "work" \| "other"`. |
122+
| `emails` | `Array<{ value, type? }>` | Email addresses. `type` is `"home" \| "work" \| "other"`. |
123+
| `addresses` | `Array<{ street?, city?, region?, postalCode?, country?, type? }>` | Postal addresses. |
124+
| `org` | `{ name?, title?, department? }` | Employer / org info. |
125+
| `urls` | `string[]` | Associated URLs. |
126+
| `birthday` | `string` | ISO date. |
127+
| `note` | `string` | Free-form note. |
128+
| `photo` | `{ mimeType, read() }` | Profile photo bytes. |
129+
| `raw` | `unknown` | Provider-specific extras passed through untouched. |
130+
</Accordion>
131+
132+
## Rich links
133+
134+
Render a URL as a rich preview card with title, summary, and cover image. Spectrum scrapes Open Graph metadata at send time; pass just the URL and the builder fills in the rest.
135+
136+
```ts
137+
import { richlink } from "spectrum-ts";
138+
139+
await space.send(richlink("https://example.com/article"));
140+
```
141+
142+
`title()`, `summary()`, and `cover()` are lazy async accessors — the metadata fetch happens only if the receiving platform needs it. Platforms without rich-link support fall back to the URL as plain text.
143+
144+
<Accordion title="Richlink" description="Resolved rich-link content with lazy metadata accessors.">
145+
| Field | Type | Description |
146+
|---|---|---|
147+
| `url` | `string` | The original URL. |
148+
| `title()` | `() => Promise<string \| undefined>` | OG title. |
149+
| `summary()` | `() => Promise<string \| undefined>` | OG description. |
150+
| `cover()` | `() => Promise<{ mimeType?, read(), stream() } \| undefined>` | OG image. |
151+
</Accordion>
152+
153+
## Polls
154+
155+
{% set pollChoiceInput = symbol("ts:spectrum-ts#PollChoiceInput") %}
156+
157+
Send a poll with a title and a list of choices. Each choice can be a plain string or a <TypeTooltip name="PollChoiceInput" type={`{{ pollChoiceInput.signature }}`} /> object — use `option()` when you want the explicit form.
158+
159+
```ts
160+
import { poll, option } from "spectrum-ts";
161+
162+
// Variadic strings
163+
await space.send(poll("Lunch?", "Pizza", "Sushi", "Tacos"));
164+
165+
// Or an array, optionally using option() for clarity
166+
await space.send(poll("Lunch?", [
167+
option("Pizza"),
168+
option("Sushi"),
169+
option("Tacos"),
170+
]));
171+
```
172+
173+
Poll responses arrive as `poll_option` content — see [Messages](/spectrum-ts/messages) for narrowing on incoming votes.
174+
175+
## Groups
176+
177+
{% set groupType = symbol("ts:spectrum-ts#Group") %}
178+
179+
A `group` bundles multiple messages into one logical unit (an album of images, a multi-attachment reply). Each item is delivered as its own `Message`, but they ship together so the receiving platform can render them as a single visual group when supported.
180+
181+
```ts
182+
import { group, attachment } from "spectrum-ts";
183+
184+
await space.send(group(
185+
attachment("/path/to/photo-1.jpg"),
186+
attachment("/path/to/photo-2.jpg"),
187+
attachment("/path/to/photo-3.jpg"),
188+
));
189+
```
190+
191+
Groups don't nest, and reactions can't be group members — the builder enforces both at construction time. Platforms that don't support grouping fall back to sending each item sequentially.
192+
43193
## Custom
44194

45-
Send structured, platform-specific payloads. Use this when the receiving platform supports rich content types that don't fit into text or attachments.
195+
Send structured, platform-specific payloads. Use this when the receiving platform supports rich content types that don't fit into the built-in builders.
46196

47197
```ts
48198
import { custom } from "spectrum-ts";
@@ -63,7 +213,7 @@ await space.send(
63213
);
64214
```
65215

66-
This runs one `send()` per item on the underlying provider — not a single compound message.
216+
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.
67217

68218
## Replies
69219

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

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ Every message conforms to <TypeTooltip name="Message" type={`{{ message.signatur
3838
</tr>
3939
<tr>
4040
<td><code>content</code></td>
41-
<td>Discriminated union on <code>type</code> — <code>"text"</code>, <code>"attachment"</code>, or <code>"custom"</code>.</td>
41+
<td>Discriminated union on <code>type</code> — see <a href="#narrowing-content">Narrowing content</a> for the full set of variants.</td>
4242
</tr>
4343
<tr>
4444
<td><code>sender</code></td>
@@ -80,24 +80,54 @@ for await (const [space, message] of app.messages) {
8080
case "attachment":
8181
console.log(
8282
`${message.content.name} (${message.content.mimeType})`,
83-
message.content.data.byteLength,
83+
await message.content.read(),
8484
);
8585
break;
86+
case "voice":
87+
console.log(`voice note (${message.content.duration}s)`);
88+
break;
89+
case "contact":
90+
console.log(message.content.name?.formatted, message.content.phones);
91+
break;
92+
case "richlink":
93+
console.log(message.content.url, await message.content.title());
94+
break;
95+
case "reaction":
96+
console.log(`${message.content.emoji} on ${message.content.target.id}`);
97+
break;
98+
case "poll":
99+
console.log(message.content.title, message.content.options);
100+
break;
101+
case "poll_option":
102+
console.log(`vote ${message.content.selected ? "+" : "-"}`, message.content.title);
103+
break;
104+
case "group":
105+
console.log(`group of ${message.content.items.length} items`);
106+
break;
86107
case "custom":
87108
console.log(message.content.raw);
88109
break;
89110
}
90111
}
91112
```
92113

93-
<Accordion title="Content" description="The incoming content variants that every Message carries.">
114+
<Accordion title="Content" description="The incoming content variants that every Message carries. Most platforms only emit a subset — narrow defensively.">
94115
| Type | Fields |
95116
|---|---|
96117
| `"text"` | `text: string` |
97-
| `"attachment"` | `data: Buffer`, `mimeType: string`, `name: string` |
118+
| `"attachment"` | `name: string`, `mimeType: string`, `size?: number`, `read()`, `stream()` |
119+
| `"voice"` | `name?: string`, `mimeType: string`, `duration?: number`, `size?: number`, `read()`, `stream()` |
120+
| `"contact"` | `name?`, `phones?`, `emails?`, `addresses?`, `org?`, `urls?`, `birthday?`, `note?`, `photo?`, `user?` |
121+
| `"richlink"` | `url: string`, `title()`, `summary()`, `cover()` |
122+
| `"reaction"` | `emoji: string`, `target: Message` |
123+
| `"poll"` | `title: string`, `options: { title: string }[]` |
124+
| `"poll_option"` | `option: { title }`, `poll: Poll`, `selected: boolean`, `title: string` — sent as a vote |
125+
| `"group"` | `items: Message[]` — bundled multi-message unit |
98126
| `"custom"` | `raw: unknown` — platform-specific structured data |
99127
</Accordion>
100128

129+
Outgoing-only variants like `"effect"` (an iMessage screen effect wrapping inner content) appear on messages you sent and are echoed by the platform; see [iMessage](/spectrum-ts/providers/imessage) for the builder.
130+
101131
## Filtering out your own messages
102132

103133
On platforms where your account also receives its own sends, guard with a platform-specific check — for example, iMessage carries an `isFromMe` flag on the raw message extra you can expose through a provider `message.schema`.

docs-src/spectrum-ts/providers/imessage.mdx.vel

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,43 @@ When omitted, Spectrum picks a phone at random from the available dedicated line
124124
Per-phone routing applies to dedicated lines (Business plan) only. On shared-pool plans the `phone` parameter is ignored — all conversations route through the shared pool automatically.
125125
</Note>
126126

127+
## Message effects
128+
129+
iMessage supports bubble effects (sent message animation) and screen effects (full-screen animation on receive). Wrap any content with `effect()`:
130+
131+
```ts
132+
import { effect, imessage } from "spectrum-ts/providers/imessage";
133+
134+
await space.send(effect("Happy birthday!", imessage.effect.message.celebration));
135+
await space.send(effect(attachment("/path/to/photo.jpg"), imessage.effect.message.confetti));
136+
```
137+
138+
The wrapped content can be a string or any `attachment(...)`. Effects only apply on iMessage — other platforms see the inner content unchanged.
139+
140+
<AccordionGroup>
141+
<Accordion title="Bubble effects" description="Animate the sent message bubble.">
142+
| Constant | Value |
143+
|---|---|
144+
| `imessage.effect.message.slam` | `"com.apple.MobileSMS.expressivesend.impact"` |
145+
| `imessage.effect.message.loud` | `"com.apple.MobileSMS.expressivesend.loud"` |
146+
| `imessage.effect.message.gentle` | `"com.apple.MobileSMS.expressivesend.gentle"` |
147+
| `imessage.effect.message.invisible` | `"com.apple.MobileSMS.expressivesend.invisibleink"` |
148+
</Accordion>
149+
<Accordion title="Screen effects" description="Play a full-screen animation on the recipient's device when the message arrives.">
150+
| Constant | Value |
151+
|---|---|
152+
| `imessage.effect.message.confetti` | `"com.apple.messages.effect.CKConfettiEffect"` |
153+
| `imessage.effect.message.fireworks` | `"com.apple.messages.effect.CKFireworksEffect"` |
154+
| `imessage.effect.message.balloons` | `"com.apple.messages.effect.CKBalloonEffect"` |
155+
| `imessage.effect.message.heart` | `"com.apple.messages.effect.CKHeartEffect"` |
156+
| `imessage.effect.message.lasers` | `"com.apple.messages.effect.CKLasersEffect"` |
157+
| `imessage.effect.message.celebration` | `"com.apple.messages.effect.CKHappyBirthdayEffect"` |
158+
| `imessage.effect.message.sparkles` | `"com.apple.messages.effect.CKSparklesEffect"` |
159+
| `imessage.effect.message.spotlight` | `"com.apple.messages.effect.CKSpotlightEffect"` |
160+
| `imessage.effect.message.echo` | `"com.apple.messages.effect.CKEchoEffect"` |
161+
</Accordion>
162+
</AccordionGroup>
163+
127164
## Tapback constants
128165

129166
iMessage uses a fixed set of tapback reactions. The `imessage` object exposes them as constants:

0 commit comments

Comments
 (0)