Composer
A draft box that knows SMS: live segment counting, unicode detection, Enter-to-send, and error recovery that gives the user their text back.
Type past 160 characters — or paste an emoji — and watch the counter. The paperclip attaches media (the demo picks a sample image); with an attachment the footer flips from segments to MMS.
Installation
npx shadcn@latest add @handset/composerUsage
import { Composer } from "@/components/handset/composer";
// Inside a thread (the usual case — Thread does this for you):
<Composer send={thread.send} disabled={conversation?.opted_out} />
// Anywhere else: hand it any (input) => Promise<Message>
<Composer send={({ body }) => sendCampaignMessage(body)} />
// MMS: wire the paperclip to your own uploader — return a public URL.
<Composer
send={thread.send}
onPickAttachment={async () => {
const file = await pickFile(); // your file picker
const { url } = await uploadToMyStorage(file); // your storage
return url; // becomes media_urls[i]
}}
/>The segment counter runs the real GSM-7/UCS-2 math from @handset/react's countSegments — one emoji flips a 160-char message to 70-char unicode segments, and your users see that before they hit send, not on the invoice.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
| send | (input: SendInput) => Promise<Message> | — | Where drafts go. Usually useThread(...).send. |
| disabled | boolean | false | Disables input and send (e.g. opted-out contact). |
| placeholder | string | "Type a message…" | Textarea placeholder. |
| onPickAttachment | () => Promise<string | string[] | null> | — | Enables the MMS paperclip. Upload the file to your storage, return its public https:// URL(s); return null to cancel. Attachments are capped at 10 and sent as media_urls. |
| className | string | — | Merged onto the wrapper. |