Skip to content

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.

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 buttons

The 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
) -> Element

The old header, show_channel_toggle, and on_channel_change props are replaced by two new props:

OldNewPurpose
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.

#[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
}

The compact inline layout uses a routing chip + drawer pattern for progressive disclosure:

  1. Routing chip — a tappable one-line summary above the textarea (e.g. SMS · +1 (555) 012-3456 or Email · 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.
  2. Routing drawer — slides open below the chip when tapped. Contains the From/To SearchableSelect fields and (if enabled) the SMS/Email channel toggle pills. A “Done” button closes it.
  3. 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.

FeatureSMSEmail
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)14

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.

A single-row input bar at the bottom of a feed view. The layout stacks vertically:

  1. Error message (if any) — top, red text
  2. Routing chip — tappable label with chevron
  3. Routing drawer — slides in with bg-muted/30 background when chip is tapped
  4. Subject field (email only)
  5. Attachment strip
  6. 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).

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).

ShortcutAction
Ctrl+EnterSend (fires on_send)
EscapeClose routing drawer (if open), or collapse expanded modal
use crate::shared::components::{ComposeField, ComposeChannel};
// Parent builds the routing label
let routing_label = Some(format!("SMS · {}", selected_phone_number));
// Parent builds the routing drawer with From/To selectors + channel toggle
let 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),
}
FileExports
src/shared/components/compose_field_component.rsComposeField, ComposeChannel
src/mods/messaging/components/message_compose_component.rsMessageCompose (primary consumer)