ComposeField
The ComposeField is a shared UI component that provides a channel-aware compose area for SMS and email. It features a compact single-row inline layout inspired by iMessage/WhatsApp (~60px), an expanded modal mode for long-form composition, SMS character/segment counting, email markdown preview, and attachment slots — while leaving all messaging logic to the parent.
Architecture
Section titled “Architecture”ComposeField (pure UI)├── Compact inline mode — single-row input bar│ ├── Routing chip (one-line context, e.g. "SMS · +1 555-0123")│ ├── Routing drawer (From/To selectors, channel toggle — on tap)│ ├── Subject field (email only)│ ├── Attachment strip│ └── Input row: [attach] [pill textarea] [send]└── Expanded modal — full-height overlay ├── Modal header with collapse button ├── Routing drawer slot ├── Attachment strip ├── Subject / SMS counter + textarea (flex-1) └── Footer with attach + send buttonsThe parent owns body: Signal<String> and passes it down. ComposeField reads and writes it directly — no internal copy.
#[component]pub fn ComposeField( channel: ComposeChannel, // Sms (default) or Email body: Signal<String>, // shared body signal is_sending: bool, // disables inputs while sending is_uploading: bool, // disables inputs while uploading can_send: bool, // enables the send button error: Option<String>, // error banner above compose area on_send: EventHandler<()>, // Ctrl+Enter or Send click on_attach: Option<EventHandler<()>>, // attachment button handler routing_label: Option<String>, // one-line routing context chip routing_drawer: Option<Element>, // From/To selectors revealed on tap subject_field: Option<Element>, // email subject input slot attachment_strip: Option<Element>, // attachment preview slot expanded_signal: Option<Signal<bool>>,// external expanded state) -> ElementKey change from the previous API
Section titled “Key change from the previous API”The old header, show_channel_toggle, and on_channel_change props are replaced by two new props:
| Old | New | Purpose |
|---|---|---|
header: Option<Element> | routing_drawer: Option<Element> | From/To selectors and channel toggle, revealed on demand |
show_channel_toggle + on_channel_change | (moved to parent’s routing drawer) | Channel toggle now lives inside the drawer, rendered by the parent |
| — | routing_label: Option<String> | Compact one-line chip shown above the input (e.g. “SMS · +1 555-0123”) |
The component manages the drawer’s open/close state internally via a routing_open signal.
ComposeChannel
Section titled “ComposeChannel”#[derive(Debug, Clone, Copy, PartialEq, Eq)]pub enum ComposeChannel { Sms, // char/segment counter, attachment button, no markdown Email, // markdown compose/preview toggle, no segment counter}Progressive Disclosure
Section titled “Progressive Disclosure”The compact inline layout uses a routing chip + drawer pattern for progressive disclosure:
- Routing chip — a tappable one-line summary above the textarea (e.g.
SMS · +1 (555) 012-3456orEmail · team@company.com). Shows a green status dot on the left and a chevron on the right. The SMS segment counter appears inline at the right end of the chip. - Routing drawer — slides open below the chip when tapped. Contains the From/To
SearchableSelectfields and (if enabled) the SMS/Email channel toggle pills. A “Done” button closes it. - Escape key — closes the routing drawer if open.
This replaces the always-visible multi-row header, reducing the compose bar height from ~150px to ~60px.
Channel Behaviors
Section titled “Channel Behaviors”| Feature | SMS | |
|---|---|---|
| Character counter | ✅ GSM-7 aware (in routing chip) | — |
| Segment counter | ✅ 160/153 or 70/67 (in routing chip) | — |
| Markdown preview | — | ✅ Eye icon toggle |
| Expand button | — | ✅ Inline (top-right of textarea) |
| Placeholder | ”Type a message…" | "Compose your email…” |
| Default rows (inline) | 1 | 4 |
SMS Segment Calculation
Section titled “SMS Segment Calculation”The component uses GSM-7 encoding rules:
- GSM-7 text: extension characters (
{ } [ ] ~ \ | ^ €) cost 2 chars. Single segment ≤ 160 chars, multipart segments ≤ 153 chars each. - Non-GSM-7 text (Unicode): single segment ≤ 70 chars, multipart ≤ 67 chars each.
Display Modes
Section titled “Display Modes”Compact Inline Mode
Section titled “Compact Inline Mode”A single-row input bar at the bottom of a feed view. The layout stacks vertically:
- Error message (if any) — top, red text
- Routing chip — tappable label with chevron
- Routing drawer — slides in with
bg-muted/30background when chip is tapped - Subject field (email only)
- Attachment strip
- Input row —
[attach button][pill textarea][send button]in a horizontal flex layout
The textarea uses a pill shape (rounded-2xl) with field-sizing: content for auto-grow up to max-h-20. The attach and send buttons are circular (rounded-full, h-9 w-9).
Expanded Modal
Section titled “Expanded Modal”A fixed overlay (z-50) anchored at 10vh from the top with a max width of max-w-2xl and height of h-[32rem]. Press Escape to collapse. The modal header shows “Compose” and a collapse button. The routing drawer renders directly (always visible in expanded mode).
Keyboard Shortcuts
Section titled “Keyboard Shortcuts”| Shortcut | Action |
|---|---|
Ctrl+Enter | Send (fires on_send) |
Escape | Close routing drawer (if open), or collapse expanded modal |
Usage Example
Section titled “Usage Example”use crate::shared::components::{ComposeField, ComposeChannel};
// Parent builds the routing labellet routing_label = Some(format!("SMS · {}", selected_phone_number));
// Parent builds the routing drawer with From/To selectors + channel togglelet routing_drawer = Some(rsx! { div { class: "space-y-2", // Channel toggle pills div { class: "flex items-center gap-1", button { class: active_pill, onclick: move |_| set_sms(), "SMS" } button { class: inactive_pill, onclick: move |_| set_email(), "Email" } } // From / To SearchableSelect rows FromSelector { /* ... */ } ToSelector { /* ... */ } }});
ComposeField { channel: ComposeChannel::Sms, body: body_signal, can_send: !body_signal().is_empty(), on_send: move |_| send_message(), on_attach: move |_| open_file_picker(), routing_label, routing_drawer, subject_field: None, expanded_signal: Some(expanded),}File Locations
Section titled “File Locations”| File | Exports |
|---|---|
src/shared/components/compose_field_component.rs | ComposeField, ComposeChannel |
src/mods/messaging/components/message_compose_component.rs | MessageCompose (primary consumer) |