mirror of
https://github.com/vernu/textbee.git
synced 2026-07-30 17:07:46 -04:00
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>
45 lines
1.4 KiB
TypeScript
45 lines
1.4 KiB
TypeScript
import { describe, expect, it } from 'vitest'
|
|
import { getSegmentInfo, isGsm7 } from './sms'
|
|
|
|
// Moved here from bulk-send/bulk-csv.test.ts when the segment helpers became
|
|
// shared by the send page, bulk send and the reply composer.
|
|
|
|
describe('isGsm7', () => {
|
|
it('accepts the basic latin alphabet', () => {
|
|
expect(isGsm7('Hello there 123')).toBe(true)
|
|
})
|
|
|
|
it('rejects characters that force UCS-2', () => {
|
|
expect(isGsm7('Hello 😀')).toBe(false)
|
|
expect(isGsm7('日本語')).toBe(false)
|
|
})
|
|
})
|
|
|
|
describe('getSegmentInfo', () => {
|
|
it('counts a short GSM-7 message as one segment', () => {
|
|
const info = getSegmentInfo('Hello there')
|
|
expect(info).toMatchObject({ segments: 1, encoding: 'GSM-7', perSegment: 160 })
|
|
})
|
|
|
|
it('treats an empty message as zero segments', () => {
|
|
expect(getSegmentInfo('').segments).toBe(0)
|
|
})
|
|
|
|
it('splits past 160 GSM-7 characters', () => {
|
|
expect(getSegmentInfo('a'.repeat(160)).segments).toBe(1)
|
|
expect(getSegmentInfo('a'.repeat(161)).segments).toBe(2)
|
|
expect(getSegmentInfo('a'.repeat(306)).segments).toBe(2)
|
|
expect(getSegmentInfo('a'.repeat(307)).segments).toBe(3)
|
|
})
|
|
|
|
it('drops to UCS-2 limits when a character needs it', () => {
|
|
const info = getSegmentInfo('Hello 😀')
|
|
expect(info.encoding).toBe('UCS-2')
|
|
expect(info.perSegment).toBe(70)
|
|
})
|
|
|
|
it('splits UCS-2 past 70 characters', () => {
|
|
expect(getSegmentInfo(`😀${'a'.repeat(70)}`).segments).toBe(2)
|
|
})
|
|
})
|