Brings the send screen up to the standard of the rebuilt bulk send flow.
Fixes real defects:
- The single-device auto-select never worked. It was computed in
defaultValues from devices?.data?.length === 1, but react-hook-form reads
defaultValues once on mount, while the devices query is still pending, so
the condition was always false. It now preselects once devices resolve.
- Submitting before devices loaded showed "Required" on the device field,
which then auto-filled a moment later, leaving a stale error beside a
populated field. Submit is disabled until devices load.
- Moved off the legacy raw useQuery(['devices']) plus devices.data, the shared
cache key with a different unwrapped shape that caused the devices?.filter
crash previously.
- Every field used a placeholder as its label. Placeholders vanish on focus
and are not reliably announced, so all three now have real labels.
Recipients are chips instead of a growing stack of inputs. They commit on
Enter, comma or blur, so a number typed and left uncommitted is not silently
dropped at send time. Pasting a list adds several at once.
Splitting a pasted list needed care, and an e2e caught it: splitting on
whitespace shredded "+1 (415) 555-0101" into three fragments. Only unambiguous
separators split first; whitespace splitting is a fallback for tokens that
cannot be a single number.
Also adds a segment counter, inline alerts for success and failure instead of
bare text, and a form reset after sending that keeps the device selected.
Phone helpers moved to lib/sms.ts alongside the segment helpers, so the send
page and bulk send share one tested implementation.
One test correction: the desktop footer assertion required links to sit on
exactly one row, but the row layout wraps by design, so it failed whenever
font metrics pushed a link to a second line. It now asserts the property the
design actually guarantees, that links are not one-per-row.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The screen opened with a tall filter card, so messages started below the fold,
and each message was a bordered card capped at max-w-sm, fitting three or four
per mobile screen.
List:
- Messages are now rows grouped under day headings (Today, Yesterday, then the
date). Roughly triples what fits on a phone, and scanning by time is how
people actually look for a message.
- Rows are buttons, so the list is keyboard navigable, with relative times
that reveal the exact timestamp on hover.
- Direction now comes from the `type` the API returns. It was inferred from
whether `sender` was present, which is only a proxy; the old check survives
as a fallback for rows written before `type` existed.
- Pagination renders only when there is more than one page.
- A search that matches nothing is a different state from a device with no
messages, and offers a way back.
Filter bar collapsed into one compact row: search, device, refresh, and auto
refresh moved into a dropdown instead of four inline buttons occupying a whole
row for a rarely-changed setting.
Details dialog reordered around why it is opened: the message body first, then
metadata, then errors, then actions. The exact timestamp is text here because
the list tooltip is hover-only and unreachable on touch.
Composer dialog gains real field labels (it used placeholders as labels, which
vanish on focus and are not reliably announced) and the same segment counter
bulk send has.
Two fixes found along the way:
- Reply passed message.device?._id with no fallback. The endpoint does
populate device, but if it were ever absent the composer would open with no
device and could not send. It now falls back to the device being viewed.
- The device-defaulting effect became derived state, so the list no longer
renders once with no device selected before correcting itself.
Segment helpers moved to lib/sms.ts now that the send page, bulk send and the
composer all use them. Fixtures now carry the populated `device` and relative
dates, so the mocked path matches production and exercises day grouping.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>