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/composer

Usage

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

Props
PropTypeDefaultDescription
send(input: SendInput) => Promise<Message>—Where drafts go. Usually useThread(...).send.
disabledbooleanfalseDisables input and send (e.g. opted-out contact).
placeholderstring"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.
classNamestring—Merged onto the wrapper.