mirror of
https://github.com/vernu/textbee.git
synced 2026-07-30 17:07:46 -04:00
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>
75 lines
2.4 KiB
TypeScript
75 lines
2.4 KiB
TypeScript
// SMS encoding helpers, shared by the send page, the bulk send flow and the
|
|
// reply composer so all three report segment counts identically.
|
|
|
|
export type SegmentInfo = {
|
|
length: number
|
|
segments: number
|
|
perSegment: number
|
|
// GSM-7 fits more per segment than UCS-2, which is needed for non-Latin
|
|
// characters and most emoji.
|
|
encoding: 'GSM-7' | 'UCS-2'
|
|
}
|
|
|
|
// Characters representable in the GSM 03.38 alphabet.
|
|
const GSM_7_CHARS =
|
|
"@£$¥èéùìòÇ\nØø\rÅåΔ_ΦΓΛΩΠΨΣΘΞÆæßÉ !\"#¤%&'()*+,-./0123456789:;<=>?¡ABCDEFGHIJKLMNOPQRSTUVWXYZÄÖÑܧ¿abcdefghijklmnopqrstuvwxyzäöñüà"
|
|
const GSM_7_EXTENDED = '^{}\\[~]|€'
|
|
|
|
export function isGsm7(text: string): boolean {
|
|
for (let i = 0; i < text.length; i += 1) {
|
|
const char = text.charAt(i)
|
|
if (GSM_7_CHARS.indexOf(char) === -1 && GSM_7_EXTENDED.indexOf(char) === -1) {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
/** Segment count for a message, so a user is not surprised by cost. */
|
|
export function getSegmentInfo(text: string): SegmentInfo {
|
|
const gsm = isGsm7(text)
|
|
const encoding = gsm ? 'GSM-7' : 'UCS-2'
|
|
const single = gsm ? 160 : 70
|
|
const multi = gsm ? 153 : 67
|
|
const length = text.length
|
|
|
|
if (length === 0) {
|
|
return { length: 0, segments: 0, perSegment: single, encoding }
|
|
}
|
|
if (length <= single) {
|
|
return { length, segments: 1, perSegment: single, encoding }
|
|
}
|
|
return {
|
|
length,
|
|
segments: Math.ceil(length / multi),
|
|
perSegment: multi,
|
|
encoding,
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Strip formatting so "+1 (415) 555-0101" and "+14155550101" are recognised as
|
|
* the same recipient for duplicate detection.
|
|
*/
|
|
export function normalizePhone(value: string): string {
|
|
const trimmed = value.trim()
|
|
const hasPlus = trimmed.startsWith('+')
|
|
const digits = trimmed.replace(/\D/g, '')
|
|
return hasPlus ? `+${digits}` : digits
|
|
}
|
|
|
|
/**
|
|
* Permissive validity check. The gateway is the real authority on whether a
|
|
* number can be reached, so this only rejects input that cannot possibly be a
|
|
* phone number, rather than enforcing a strict E.164 shape that would block
|
|
* legitimate local formats.
|
|
*/
|
|
export function isPlausiblePhone(value: string): boolean {
|
|
const normalized = normalizePhone(value)
|
|
const digits = normalized.replace(/\D/g, '')
|
|
if (digits.length < 7 || digits.length > 15) return false
|
|
// Reject anything carrying characters that are neither digits, separators,
|
|
// nor a leading plus.
|
|
return /^\+?[\d\s()./-]+$/.test(value.trim())
|
|
}
|