@robono/messaging-react-native 0.8.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/README.md +40 -37
  3. package/SBOM.spdx.json +229 -157
  4. package/dist/account.js +5 -3
  5. package/dist/attachment-menu.js +3 -1
  6. package/dist/call-message.js +6 -4
  7. package/dist/calling.js +3 -1
  8. package/dist/conversation-details.js +9 -6
  9. package/dist/expo-connected-calling.js +4 -1
  10. package/dist/expo-extras.js +4 -1
  11. package/dist/expo-media.js +27 -5
  12. package/dist/expo-messenger.d.ts +38 -2
  13. package/dist/expo-messenger.js +56 -6
  14. package/dist/expo-outbox.js +22 -1
  15. package/dist/expo-screen.d.ts +1 -1
  16. package/dist/expo-screen.js +23 -14
  17. package/dist/expo-social.js +3 -1
  18. package/dist/expo.d.ts +1 -1
  19. package/dist/expo.js +1 -1
  20. package/dist/extras.js +9 -6
  21. package/dist/forward.js +6 -4
  22. package/dist/group-editor.js +7 -5
  23. package/dist/header-calls.js +7 -5
  24. package/dist/inbox-search.js +4 -2
  25. package/dist/inbox.js +9 -7
  26. package/dist/index.d.ts +6 -3
  27. package/dist/index.js +91 -61
  28. package/dist/localization.d.ts +24 -0
  29. package/dist/localization.js +12 -0
  30. package/dist/maps-connection.js +3 -1
  31. package/dist/media-ui.js +13 -7
  32. package/dist/message-menu.js +3 -1
  33. package/dist/native-outbox.d.ts +9 -1
  34. package/dist/native-outbox.js +7 -0
  35. package/dist/polls.js +18 -14
  36. package/dist/popup.js +6 -3
  37. package/dist/quick-send.js +4 -2
  38. package/dist/safety.js +9 -7
  39. package/dist/saved.js +5 -3
  40. package/dist/scheduled.js +17 -13
  41. package/dist/search.js +5 -3
  42. package/dist/social.js +7 -4
  43. package/dist/translation-settings.js +5 -3
  44. package/dist/voice-composer.js +9 -5
  45. package/package.json +9 -7
  46. package/setup.plugin.cjs +20 -0
package/dist/saved.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { useRobonoLocale } from './localization.js';
2
3
  import { messagingPopupColors } from './popup.js';
3
4
  import { useEffect, useRef, useState } from 'react';
4
5
  import { Platform, Pressable, ScrollView, Text, View } from 'react-native';
@@ -6,6 +7,7 @@ import { MessagingPopup } from './popup.js';
6
7
  import { MessageIcon } from './icons.js';
7
8
  /** Same app-sized popup and media viewer as the conversation; no server-history copy. */
8
9
  export function SavedMessagesPopup({ records, appearance, onClose, onOpen, onRemove }) {
10
+ const { text: uiText, locale: uiLocale, number: uiNumber, count: uiCount } = useRobonoLocale();
9
11
  const c = { ...appearance.colors, ...messagingPopupColors }, [limit, setLimit] = useState(30), [error, setError] = useState(''), [busy, setBusy] = useState(false), [detail, setDetail] = useState(null);
10
12
  const [visible, setVisible] = useState(true), pending = useRef(null), alive = useRef(true);
11
13
  useEffect(() => { alive.current = true; return () => { alive.current = false; pending.current = null; }; }, []);
@@ -29,7 +31,7 @@ export function SavedMessagesPopup({ records, appearance, onClose, onOpen, onRem
29
31
  }
30
32
  catch (e) {
31
33
  if (alive.current) {
32
- setError(e instanceof Error ? e.message : 'This saved attachment could not be opened.');
34
+ setError(e instanceof Error ? e.message : uiText("This saved attachment could not be opened."));
33
35
  setBusy(false);
34
36
  setVisible(true);
35
37
  }
@@ -42,10 +44,10 @@ export function SavedMessagesPopup({ records, appearance, onClose, onOpen, onRem
42
44
  return;
43
45
  } if (busy)
44
46
  return; setBusy(true); setError(''); pending.current = record; setVisible(false); }
45
- return _jsxs(MessagingPopup, { contentWidth: 640, visible: visible, onDismiss: () => void dismissed(), background: c.background, title: detail ? 'Saved message' : 'Saved', foreground: c.foreground, closeLabel: detail ? 'Back to Saved' : 'Close Saved', closeDisabled: busy, headerBack: !!detail, onClose: () => { if (!busy) {
47
+ return _jsxs(MessagingPopup, { contentWidth: 640, visible: visible, onDismiss: () => void dismissed(), background: c.background, title: detail ? uiText("Saved message") : uiText("Saved"), foreground: c.foreground, closeLabel: detail ? uiText("Back to Saved") : uiText("Close Saved"), closeDisabled: busy, headerBack: !!detail, onClose: () => { if (!busy) {
46
48
  if (detail)
47
49
  setDetail(null);
48
50
  else
49
51
  onClose();
50
- } }, children: [!detail && _jsx(Text, { style: { color: c.secondary, fontSize: 13, marginBottom: 12 }, children: "Saved from all chats" }), _jsxs(ScrollView, { style: { flex: 1, minHeight: 0 }, children: [!records.length && _jsx(Text, { style: { color: c.secondary, marginVertical: 16 }, children: "No saved messages yet." }), detail ? _jsxs(_Fragment, { children: [_jsxs(Text, { style: { color: c.secondary, fontSize: 12, marginBottom: 10 }, children: [detail.conversationTitle, " \u00B7 ", new Date(detail.message.createdAt).toLocaleDateString()] }), _jsx(Text, { selectable: true, style: { color: c.foreground, fontSize: 16, lineHeight: 23 }, children: detail.message.text })] }) : records.slice(0, limit).map(record => _jsxs(View, { style: { flexDirection: 'row', alignItems: 'center', borderBottomWidth: 1, borderBottomColor: c.secondary + '20' }, children: [_jsxs(Pressable, { accessibilityRole: "button", accessibilityLabel: 'Open saved message: ' + (record.message.text || record.message.media?.fileName || 'Attachment'), disabled: busy, onPress: () => void open(record), style: { flex: 1, paddingVertical: 14, paddingHorizontal: 8, gap: 4 }, children: [_jsxs(Text, { style: { color: c.secondary, fontSize: 12 }, children: [record.conversationTitle, " \u00B7 ", new Date(record.message.createdAt).toLocaleDateString()] }), _jsx(Text, { numberOfLines: 2, style: { color: c.foreground, fontSize: 15, lineHeight: 20 }, children: record.message.text || record.message.media?.fileName || 'Attachment' }), record.message.media && !!record.message.text && _jsx(Text, { numberOfLines: 1, style: { color: c.secondary, fontSize: 12 }, children: record.message.media.fileName || record.message.kind })] }), _jsx(Pressable, { accessibilityRole: "button", accessibilityLabel: 'Remove saved message: ' + (record.message.text || record.message.media?.fileName || 'Attachment'), disabled: busy, onPress: () => { setBusy(true); setError(''); void onRemove(record.key).catch(e => setError(e instanceof Error ? e.message : 'This saved message could not be removed.')).finally(() => setBusy(false)); }, style: { minWidth: 44, minHeight: 44, alignItems: 'center', justifyContent: 'center' }, children: _jsx(MessageIcon, { name: "trash", color: c.secondary, size: 20 }) })] }, record.key))] }), !detail && records.length > limit && _jsx(Pressable, { accessibilityRole: "button", onPress: () => setLimit(n => n + 30), style: { minHeight: 44, justifyContent: 'center', alignSelf: 'flex-end' }, children: _jsx(Text, { style: { color: c.accent }, children: "More saved messages" }) }), !!error && _jsx(Text, { accessibilityRole: "alert", style: { color: c.foreground, marginTop: 8 }, children: error })] });
52
+ } }, children: [!detail && _jsx(Text, { style: { color: c.secondary, fontSize: 13, marginBottom: 12 }, children: uiText("Saved from all chats") }), _jsxs(ScrollView, { style: { flex: 1, minHeight: 0 }, children: [!records.length && _jsx(Text, { style: { color: c.secondary, marginVertical: 16 }, children: uiText("No saved messages yet.") }), detail ? _jsxs(_Fragment, { children: [_jsxs(Text, { style: { color: c.secondary, fontSize: 12, marginBottom: 10 }, children: [detail.conversationTitle, " \u00B7 ", new Date(detail.message.createdAt).toLocaleDateString(uiLocale)] }), _jsx(Text, { selectable: true, style: { color: c.foreground, fontSize: 16, lineHeight: 23 }, children: detail.message.text })] }) : records.slice(0, limit).map(record => _jsxs(View, { style: { flexDirection: 'row', alignItems: 'center', borderBottomWidth: 1, borderBottomColor: c.secondary + '20' }, children: [_jsxs(Pressable, { accessibilityRole: "button", accessibilityLabel: 'Open saved message: ' + (record.message.text || record.message.media?.fileName || 'Attachment'), disabled: busy, onPress: () => void open(record), style: { flex: 1, paddingVertical: 14, paddingHorizontal: 8, gap: 4 }, children: [_jsxs(Text, { style: { color: c.secondary, fontSize: 12 }, children: [record.conversationTitle, " \u00B7 ", new Date(record.message.createdAt).toLocaleDateString(uiLocale)] }), _jsx(Text, { numberOfLines: 2, style: { color: c.foreground, fontSize: 15, lineHeight: 20 }, children: record.message.text || record.message.media?.fileName || uiText("Attachment") }), record.message.media && !!record.message.text && _jsx(Text, { numberOfLines: 1, style: { color: c.secondary, fontSize: 12 }, children: record.message.media.fileName || record.message.kind })] }), _jsx(Pressable, { accessibilityRole: "button", accessibilityLabel: 'Remove saved message: ' + (record.message.text || record.message.media?.fileName || 'Attachment'), disabled: busy, onPress: () => { setBusy(true); setError(''); void onRemove(record.key).catch(e => setError(e instanceof Error ? e.message : uiText("This saved message could not be removed."))).finally(() => setBusy(false)); }, style: { minWidth: 44, minHeight: 44, alignItems: 'center', justifyContent: 'center' }, children: _jsx(MessageIcon, { name: "trash", color: c.secondary, size: 20 }) })] }, record.key))] }), !detail && records.length > limit && _jsx(Pressable, { accessibilityRole: "button", onPress: () => setLimit(n => n + 30), style: { minHeight: 44, justifyContent: 'center', alignSelf: 'flex-end' }, children: _jsx(Text, { style: { color: c.accent }, children: uiText("More saved messages") }) }), !!error && _jsx(Text, { accessibilityRole: "alert", style: { color: c.foreground, marginTop: 8 }, children: error })] });
51
53
  }
package/dist/scheduled.js CHANGED
@@ -1,42 +1,46 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useRobonoLocale } from './localization.js';
2
3
  import { useEffect, useRef, useState } from 'react';
3
4
  import { Pressable, ScrollView, Text, TextInput, View } from 'react-native';
4
5
  import { MessagingPopup, messagingPopupColors as colors } from './popup.js';
5
6
  const pad = (n) => String(n).padStart(2, '0');
6
7
  /** Same bounded popup as the other Messenger tools; all dates are shown locally. */
7
- export function ScheduleTimePicker({ initial, retryAt, onSave, onClose, accent, busy = false, error = '', title = 'Send later' }) {
8
- const twelveHour = Intl.DateTimeFormat(undefined, { hour: 'numeric' }).resolvedOptions().hour12 === true;
8
+ export function ScheduleTimePicker({ initial, retryAt, onSave, onClose, accent, busy = false, error = '', title }) {
9
+ const { text: uiText, locale: uiLocale, number: uiNumber, count: uiCount } = useRobonoLocale();
10
+ title ??= uiText('Send later');
11
+ const twelveHour = Intl.DateTimeFormat(uiLocale, { hour: 'numeric' }).resolvedOptions().hour12 === true;
9
12
  const [period, setPeriod] = useState(() => new Date(initial ?? Date.now() + 3600000).getHours() < 12 ? 'AM' : 'PM');
10
13
  const [date, setDate] = useState(() => new Date(initial ?? Date.now() + 3600000)), [hour, setHour] = useState(() => pad(twelveHour ? (date.getHours() % 12 || 12) : date.getHours())), [minute, setMinute] = useState(() => pad(date.getMinutes())), [localError, setError] = useState('');
11
14
  const [month, setMonth] = useState(() => new Date(date.getFullYear(), date.getMonth(), 1));
12
15
  const first = new Date(month.getFullYear(), month.getMonth(), 1), days = new Date(month.getFullYear(), month.getMonth() + 1, 0).getDate(), today = new Date();
13
16
  today.setHours(0, 0, 0, 0);
14
- const end = new Date(Date.now() + 30 * 86400000), zone = Intl.DateTimeFormat(undefined, { timeZoneName: 'long' }).formatToParts(date).find(p => p.type === 'timeZoneName')?.value ?? Intl.DateTimeFormat().resolvedOptions().timeZone;
17
+ const end = new Date(Date.now() + 30 * 86400000), zone = Intl.DateTimeFormat(uiLocale, { timeZoneName: 'long' }).formatToParts(date).find(p => p.type === 'timeZoneName')?.value ?? Intl.DateTimeFormat().resolvedOptions().timeZone;
15
18
  const choose = (value) => { setDate(value); setMonth(new Date(value.getFullYear(), value.getMonth(), 1)); setError(''); };
16
19
  const submit = () => {
17
20
  if (!/^\d{1,2}$/.test(hour) || !/^\d{1,2}$/.test(minute) || Number(hour) > (twelveHour ? 12 : 23) || twelveHour && Number(hour) < 1 || Number(minute) > 59) {
18
- setError(twelveHour ? 'Use hours 1–12 and minutes 00–59.' : 'Use hours 00–23 and minutes 00–59.');
21
+ setError(twelveHour ? uiText("Use hours 1–12 and minutes 00–59.") : uiText("Use hours 00–23 and minutes 00–59."));
19
22
  return;
20
23
  }
21
24
  const hours = twelveHour ? (Number(hour) % 12) + (period === 'PM' ? 12 : 0) : Number(hour);
22
25
  const value = new Date(date.getFullYear(), date.getMonth(), date.getDate(), hours, Number(minute));
23
26
  if (value.getHours() !== hours || value.getMinutes() !== Number(minute)) {
24
- setError('That time is skipped by the clock change. Choose another time.');
27
+ setError(uiText("That time is skipped by the clock change. Choose another time."));
25
28
  return;
26
29
  }
27
30
  if (value.getTime() < Date.now() + 30000 || value.getTime() > Date.now() + 30 * 86400000) {
28
- setError('Choose a time at least one minute ahead, within the next 30 days.');
31
+ setError(uiText("Choose a time at least one minute ahead, within the next 30 days."));
29
32
  return;
30
33
  }
31
34
  onSave(value.toISOString());
32
35
  };
33
36
  if (retryAt)
34
- return _jsx(MessagingPopup, { title: "Confirm scheduled message", onClose: () => { if (!busy)
35
- onClose(); }, closeDisabled: busy, background: colors.background, contentWidth: 420, children: _jsxs(View, { style: { padding: 12, gap: 16 }, children: [_jsx(Text, { style: { color: colors.foreground, fontSize: 17 }, children: "The last attempt could not be confirmed. Retry to check it without creating another message." }), _jsx(Text, { style: { color: colors.secondary, fontSize: 16 }, children: new Date(retryAt).toLocaleString() }), !!error && _jsx(Text, { accessibilityRole: "alert", style: { color: colors.foreground }, children: error }), _jsx(Pressable, { accessibilityRole: "button", accessibilityLabel: "Retry scheduling", disabled: busy, onPress: () => onSave(retryAt), style: { minHeight: 46, borderRadius: 12, justifyContent: 'center', alignItems: 'center', backgroundColor: accent, opacity: busy ? .5 : 1 }, children: _jsx(Text, { style: { fontSize: 16, color: '#111', fontWeight: '600' }, children: busy ? 'Checking…' : 'Retry scheduling' }) })] }) });
37
+ return _jsx(MessagingPopup, { title: uiText("Confirm scheduled message"), onClose: () => { if (!busy)
38
+ onClose(); }, closeDisabled: busy, background: colors.background, contentWidth: 420, children: _jsxs(View, { style: { padding: 12, gap: 16 }, children: [_jsx(Text, { style: { color: colors.foreground, fontSize: 17 }, children: uiText("The last attempt could not be confirmed. Retry to check it without creating another message.") }), _jsx(Text, { style: { color: colors.secondary, fontSize: 16 }, children: new Date(retryAt).toLocaleString(uiLocale) }), !!error && _jsx(Text, { accessibilityRole: "alert", style: { color: colors.foreground }, children: error }), _jsx(Pressable, { accessibilityRole: "button", accessibilityLabel: uiText("Retry scheduling"), disabled: busy, onPress: () => onSave(retryAt), style: { minHeight: 46, borderRadius: 12, justifyContent: 'center', alignItems: 'center', backgroundColor: accent, opacity: busy ? .5 : 1 }, children: _jsx(Text, { style: { fontSize: 16, color: '#111', fontWeight: '600' }, children: busy ? uiText("Checking…") : uiText("Retry scheduling") }) })] }) });
36
39
  return _jsxs(MessagingPopup, { title: title, onClose: () => { if (!busy)
37
- onClose(); }, closeDisabled: busy, background: colors.background, contentWidth: 420, children: [_jsxs(ScrollView, { keyboardShouldPersistTaps: "handled", contentContainerStyle: { padding: 12, gap: 12 }, children: [_jsx(Text, { style: { color: colors.foreground, fontSize: 17 }, children: "Choose when this message should be sent." }), _jsxs(View, { style: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center' }, children: [_jsx(Pressable, { accessibilityRole: "button", accessibilityLabel: "Previous month", disabled: busy || first <= today, onPress: () => setMonth(new Date(month.getFullYear(), month.getMonth() - 1, 1)), style: { padding: 12 }, children: _jsx(Text, { style: { color: accent, fontSize: 24 }, children: "\u2039" }) }), _jsx(Text, { style: { color: colors.foreground, fontSize: 18, fontWeight: '600' }, children: month.toLocaleDateString(undefined, { month: 'long', year: 'numeric' }) }), _jsx(Pressable, { accessibilityRole: "button", accessibilityLabel: "Next month", disabled: busy || new Date(month.getFullYear(), month.getMonth() + 1, 1) > end, onPress: () => setMonth(new Date(month.getFullYear(), month.getMonth() + 1, 1)), style: { padding: 12 }, children: _jsx(Text, { style: { color: accent, fontSize: 24 }, children: "\u203A" }) })] }), _jsxs(View, { style: { flexDirection: 'row', flexWrap: 'wrap' }, children: [['S', 'M', 'T', 'W', 'T', 'F', 'S'].map((day, i) => _jsx(Text, { style: { width: '14.285%', textAlign: 'center', color: colors.secondary, paddingBottom: 8 }, children: day }, 'week' + i)), Array.from({ length: first.getDay() }, (_, i) => _jsx(View, { style: { width: '14.285%' } }, 'blank' + i)), Array.from({ length: days }, (_, i) => { const d = new Date(month.getFullYear(), month.getMonth(), i + 1), selected = d.toDateString() === date.toDateString(), disabled = busy || d < today || d > end; return _jsx(Pressable, { accessibilityRole: "button", accessibilityLabel: d.toLocaleDateString(undefined, { weekday: 'long', month: 'long', day: 'numeric' }), accessibilityState: { selected, disabled }, disabled: disabled, onPress: () => choose(d), style: { width: '14.285%', height: 44, alignItems: 'center', justifyContent: 'center', borderRadius: 22, backgroundColor: selected ? accent + '30' : 'transparent', opacity: disabled ? .3 : 1 }, children: _jsx(Text, { style: { fontSize: 17, color: selected ? accent : colors.foreground, fontWeight: selected ? '700' : '400' }, children: i + 1 }) }, i); })] }), _jsxs(View, { style: { flexDirection: 'row', gap: 12, alignItems: 'center' }, children: [_jsx(Text, { style: { color: colors.foreground, fontSize: 16 }, children: twelveHour ? 'Time' : 'Time (24-hour)' }), [[hour, setHour, 'Hour'], [minute, setMinute, 'Minute']].map(([value, set, label]) => _jsx(TextInput, { accessibilityLabel: label, value: value, onChangeText: (text) => { set(text.replace(/\D/g, '').slice(0, 2)); setError(''); }, editable: !busy, keyboardType: "number-pad", maxLength: 2, selectTextOnFocus: true, style: { borderWidth: 1, borderColor: colors.secondary, borderRadius: 10, width: 56, padding: 10, color: colors.foreground, fontSize: 20, textAlign: 'center' } }, label)), twelveHour && _jsx(Pressable, { accessibilityRole: "button", accessibilityLabel: period + ', switch to ' + (period === 'AM' ? 'PM' : 'AM'), onPress: () => setPeriod(period === 'AM' ? 'PM' : 'AM'), disabled: busy, style: { minHeight: 44, justifyContent: 'center', padding: 4 }, children: _jsx(Text, { style: { color: accent, fontSize: 17, fontWeight: '600' }, children: period }) })] }), _jsxs(Text, { style: { color: colors.secondary, fontSize: 14 }, children: [zone, ". The app can be closed when it is time to send."] }), !!(error || localError) && _jsx(Text, { accessibilityRole: "alert", style: { color: colors.foreground }, children: error || localError })] }), _jsx(View, { style: { paddingHorizontal: 12, paddingTop: 8, paddingBottom: 8, gap: 8 }, children: _jsx(Pressable, { accessibilityRole: "button", disabled: busy, onPress: submit, style: { minHeight: 46, borderRadius: 12, justifyContent: 'center', alignItems: 'center', backgroundColor: accent, opacity: busy ? .5 : 1 }, children: _jsx(Text, { style: { fontSize: 16, color: '#111', fontWeight: '600' }, children: busy ? 'Saving…' : 'Schedule message' }) }) })] });
40
+ onClose(); }, closeDisabled: busy, background: colors.background, contentWidth: 420, children: [_jsxs(ScrollView, { keyboardShouldPersistTaps: "handled", contentContainerStyle: { padding: 12, gap: 12 }, children: [_jsx(Text, { style: { color: colors.foreground, fontSize: 17 }, children: uiText("Choose when this message should be sent.") }), _jsxs(View, { style: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center' }, children: [_jsx(Pressable, { accessibilityRole: "button", accessibilityLabel: uiText("Previous month"), disabled: busy || first <= today, onPress: () => setMonth(new Date(month.getFullYear(), month.getMonth() - 1, 1)), style: { padding: 12 }, children: _jsx(Text, { style: { color: accent, fontSize: 24 }, children: "\u2039" }) }), _jsx(Text, { style: { color: colors.foreground, fontSize: 18, fontWeight: '600' }, children: month.toLocaleDateString(uiLocale, { month: 'long', year: 'numeric' }) }), _jsx(Pressable, { accessibilityRole: "button", accessibilityLabel: uiText("Next month"), disabled: busy || new Date(month.getFullYear(), month.getMonth() + 1, 1) > end, onPress: () => setMonth(new Date(month.getFullYear(), month.getMonth() + 1, 1)), style: { padding: 12 }, children: _jsx(Text, { style: { color: accent, fontSize: 24 }, children: "\u203A" }) })] }), _jsxs(View, { style: { flexDirection: 'row', flexWrap: 'wrap' }, children: [Array.from({ length: 7 }, (_, i) => new Intl.DateTimeFormat(uiLocale, { weekday: 'narrow' }).format(new Date(2026, 0, 4 + i))).map((day, i) => _jsx(Text, { style: { width: '14.285%', textAlign: 'center', color: colors.secondary, paddingBottom: 8 }, children: day }, 'week' + i)), Array.from({ length: first.getDay() }, (_, i) => _jsx(View, { style: { width: '14.285%' } }, 'blank' + i)), Array.from({ length: days }, (_, i) => { const d = new Date(month.getFullYear(), month.getMonth(), i + 1), selected = d.toDateString() === date.toDateString(), disabled = busy || d < today || d > end; return _jsx(Pressable, { accessibilityRole: "button", accessibilityLabel: d.toLocaleDateString(uiLocale, { weekday: 'long', month: 'long', day: 'numeric' }), accessibilityState: { selected, disabled }, disabled: disabled, onPress: () => choose(d), style: { width: '14.285%', height: 44, alignItems: 'center', justifyContent: 'center', borderRadius: 22, backgroundColor: selected ? accent + '30' : 'transparent', opacity: disabled ? .3 : 1 }, children: _jsx(Text, { style: { fontSize: 17, color: selected ? accent : colors.foreground, fontWeight: selected ? '700' : '400' }, children: i + 1 }) }, i); })] }), _jsxs(View, { style: { flexDirection: 'row', gap: 12, alignItems: 'center' }, children: [_jsx(Text, { style: { color: colors.foreground, fontSize: 16 }, children: twelveHour ? uiText("Time") : uiText("Time (24-hour)") }), [[hour, setHour, 'Hour'], [minute, setMinute, 'Minute']].map(([value, set, label]) => _jsx(TextInput, { accessibilityLabel: uiText(label), value: value, onChangeText: (text) => { set(text.replace(/\D/g, '').slice(0, 2)); setError(''); }, editable: !busy, keyboardType: "number-pad", maxLength: 2, selectTextOnFocus: true, style: { borderWidth: 1, borderColor: colors.secondary, borderRadius: 10, width: 56, padding: 10, color: colors.foreground, fontSize: 20, textAlign: 'center' } }, label)), twelveHour && _jsx(Pressable, { accessibilityRole: "button", accessibilityLabel: period + ', switch to ' + (period === 'AM' ? 'PM' : 'AM'), onPress: () => setPeriod(period === 'AM' ? 'PM' : 'AM'), disabled: busy, style: { minHeight: 44, justifyContent: 'center', padding: 4 }, children: _jsx(Text, { style: { color: accent, fontSize: 17, fontWeight: '600' }, children: period }) })] }), _jsx(Text, { style: { color: colors.secondary, fontSize: 14 }, children: uiText('{zone}. The app can be closed when it is time to send.', { zone }) }), !!(error || localError) && _jsx(Text, { accessibilityRole: "alert", style: { color: colors.foreground }, children: error || localError })] }), _jsx(View, { style: { paddingHorizontal: 12, paddingTop: 8, paddingBottom: 8, gap: 8 }, children: _jsx(Pressable, { accessibilityRole: "button", disabled: busy, onPress: submit, style: { minHeight: 46, borderRadius: 12, justifyContent: 'center', alignItems: 'center', backgroundColor: accent, opacity: busy ? .5 : 1 }, children: _jsx(Text, { style: { fontSize: 16, color: '#111', fontWeight: '600' }, children: busy ? uiText("Saving…") : uiText("Schedule message") }) }) })] });
38
41
  }
39
42
  export function ScheduledMessages({ client, accent, onClose }) {
43
+ const { text: uiText, locale: uiLocale, number: uiNumber, count: uiCount } = useRobonoLocale();
40
44
  const [items, setItems] = useState([]), [loading, setLoading] = useState(true), [busy, setBusy] = useState(false), [error, setError] = useState(''), [editing, setEditing] = useState(null);
41
45
  const active = useRef(true);
42
46
  const load = async () => { try {
@@ -48,7 +52,7 @@ export function ScheduledMessages({ client, accent, onClose }) {
48
52
  }
49
53
  catch (e) {
50
54
  if (active.current)
51
- setError(e instanceof Error ? e.message : 'Scheduled messages could not load.');
55
+ setError(e instanceof Error ? e.message : uiText("Scheduled messages could not load."));
52
56
  }
53
57
  finally {
54
58
  if (active.current)
@@ -63,13 +67,13 @@ export function ScheduledMessages({ client, accent, onClose }) {
63
67
  }
64
68
  catch (e) {
65
69
  if (active.current)
66
- setError(e instanceof Error ? e.message : 'This message could not be changed.');
70
+ setError(e instanceof Error ? e.message : uiText("This message could not be changed."));
67
71
  }
68
72
  finally {
69
73
  if (active.current)
70
74
  setBusy(false);
71
75
  } };
72
76
  if (editing)
73
- return _jsx(ScheduleTimePicker, { initial: editing.sendAt, title: "Reschedule message", accent: accent, busy: busy, error: error, onSave: time => void change(editing, time), onClose: () => setEditing(null) });
74
- return _jsx(MessagingPopup, { title: "Scheduled messages", background: colors.background, onClose: onClose, contentWidth: 640, children: _jsxs(ScrollView, { contentContainerStyle: { padding: 12, gap: 12 }, children: [_jsx(Text, { style: { color: colors.secondary, fontSize: 15 }, children: "Only you can see these until they are sent." }), loading && _jsx(Text, { style: { color: colors.foreground }, children: "Loading\u2026" }), !loading && !items.length && !error && _jsx(Text, { style: { color: colors.foreground, fontSize: 17 }, children: "No scheduled messages. Hold Send after writing a message to choose a time." }), !!error && _jsx(Pressable, { onPress: () => void load(), accessibilityRole: "button", accessibilityLabel: "Retry scheduled messages", children: _jsxs(Text, { accessibilityRole: "alert", style: { color: colors.foreground }, children: [error, " Tap to refresh."] }) }), items.map(item => _jsxs(View, { style: { borderRadius: 12, backgroundColor: '#202020', padding: 12, gap: 8 }, children: [_jsxs(Text, { style: { color: colors.foreground, fontWeight: '600', fontSize: 16 }, children: [new Date(item.sendAt).toLocaleString(undefined, { month: 'short', day: 'numeric', hour: 'numeric', minute: '2-digit' }), " \u00B7 ", item.state === 'pending' ? 'Scheduled' : item.state === 'failed' ? 'Not sent' : item.state === 'sent' ? 'Sent' : 'Cancelled'] }), item.content && _jsx(Text, { numberOfLines: 4, style: { color: colors.foreground, fontSize: 16 }, children: item.content.text || item.content.media?.fileName || item.content.poll?.question || item.content.kind }), item.state === 'failed' && _jsxs(Text, { style: { color: colors.secondary }, children: ["Not sent. ", item.errorCode === 'schedule_expired' ? 'The sending window passed.' : item.errorCode?.includes('screening') ? 'The app’s safety check did not allow delivery.' : item.errorCode?.includes('billing') || item.errorCode?.includes('spending') ? 'The app’s spending limit or billing stopped delivery.' : 'Access, message rules or service availability changed.'] }), item.state === 'pending' && _jsxs(View, { style: { flexDirection: 'row', gap: 12 }, children: [_jsx(Pressable, { disabled: busy, onPress: () => setEditing(item), style: { minHeight: 44, justifyContent: 'center' }, children: _jsx(Text, { style: { color: accent, fontSize: 16 }, children: "Reschedule" }) }), _jsx(Pressable, { disabled: busy, onPress: () => void change(item), style: { minHeight: 44, justifyContent: 'center' }, children: _jsx(Text, { style: { color: accent, fontSize: 16 }, children: "Cancel message" }) })] })] }, item.id))] }) });
77
+ return _jsx(ScheduleTimePicker, { initial: editing.sendAt, title: uiText("Reschedule message"), accent: accent, busy: busy, error: error, onSave: time => void change(editing, time), onClose: () => setEditing(null) });
78
+ return _jsx(MessagingPopup, { title: uiText("Scheduled messages"), background: colors.background, onClose: onClose, contentWidth: 640, children: _jsxs(ScrollView, { contentContainerStyle: { padding: 12, gap: 12 }, children: [_jsx(Text, { style: { color: colors.secondary, fontSize: 15 }, children: uiText("Only you can see these until they are sent.") }), loading && _jsx(Text, { style: { color: colors.foreground }, children: uiText("Loading…") }), !loading && !items.length && !error && _jsx(Text, { style: { color: colors.foreground, fontSize: 17 }, children: uiText("No scheduled messages. Hold Send after writing a message to choose a time.") }), !!error && _jsx(Pressable, { onPress: () => void load(), accessibilityRole: "button", accessibilityLabel: uiText("Retry scheduled messages"), children: _jsxs(Text, { accessibilityRole: "alert", style: { color: colors.foreground }, children: [error, ' ', uiText("Tap to refresh.")] }) }), items.map(item => _jsxs(View, { style: { borderRadius: 12, backgroundColor: '#202020', padding: 12, gap: 8 }, children: [_jsxs(Text, { style: { color: colors.foreground, fontWeight: '600', fontSize: 16 }, children: [new Date(item.sendAt).toLocaleString(uiLocale, { month: 'short', day: 'numeric', hour: 'numeric', minute: '2-digit' }), " \u00B7 ", item.state === 'pending' ? uiText("Scheduled") : item.state === 'failed' ? uiText("Not sent") : item.state === 'sent' ? uiText("Sent") : uiText("Cancelled")] }), item.content && _jsx(Text, { numberOfLines: 4, style: { color: colors.foreground, fontSize: 16 }, children: item.content.text || item.content.media?.fileName || item.content.poll?.question || item.content.kind }), item.state === 'failed' && _jsxs(Text, { style: { color: colors.secondary }, children: [uiText("Not sent."), ' ', item.errorCode === 'schedule_expired' ? uiText("The sending window passed.") : item.errorCode?.includes('screening') ? uiText("The app’s safety check did not allow delivery.") : item.errorCode?.includes('billing') || item.errorCode?.includes('spending') ? uiText("The app’s spending limit or billing stopped delivery.") : uiText("Access, message rules or service availability changed.")] }), item.state === 'pending' && _jsxs(View, { style: { flexDirection: 'row', gap: 12 }, children: [_jsx(Pressable, { disabled: busy, onPress: () => setEditing(item), style: { minHeight: 44, justifyContent: 'center' }, children: _jsx(Text, { style: { color: accent, fontSize: 16 }, children: uiText("Reschedule") }) }), _jsx(Pressable, { disabled: busy, onPress: () => void change(item), style: { minHeight: 44, justifyContent: 'center' }, children: _jsx(Text, { style: { color: accent, fontSize: 16 }, children: uiText("Cancel message") }) })] })] }, item.id))] }) });
75
79
  }
package/dist/search.js CHANGED
@@ -1,9 +1,11 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useRobonoLocale } from './localization.js';
2
3
  import { useEffect, useRef, useState } from 'react';
3
4
  import { ActivityIndicator, Pressable, Text, TextInput, View } from 'react-native';
4
5
  import { MessageIcon } from './icons.js';
5
6
  /** Messenger's in-conversation search: keep the chat visible and move between matches. */
6
7
  export function MessagingSearch({ client, appearance, onClose }) {
8
+ const { text: uiText, locale: uiLocale, number: uiNumber, count: uiCount } = useRobonoLocale();
7
9
  const c = appearance.colors, [query, setQuery] = useState(''), [messages, setMessages] = useState([]), [index, setIndex] = useState(0), [next, setNext] = useState(null), [busy, setBusy] = useState(false), [error, setError] = useState('');
8
10
  const request = useRef(null), generation = useRef(0), timer = useRef(undefined);
9
11
  useEffect(() => () => { generation.current++; request.current?.abort(); clearTimeout(timer.current); }, []);
@@ -12,7 +14,7 @@ export function MessagingSearch({ client, appearance, onClose }) {
12
14
  }
13
15
  catch (e) {
14
16
  if (id === generation.current)
15
- setError(e instanceof Error ? e.message : 'This message is unavailable.');
17
+ setError(e instanceof Error ? e.message : uiText("This message is unavailable."));
16
18
  } }
17
19
  async function search(more = false) {
18
20
  clearTimeout(timer.current);
@@ -39,7 +41,7 @@ export function MessagingSearch({ client, appearance, onClose }) {
39
41
  }
40
42
  catch (e) {
41
43
  if (id === generation.current && !control.signal.aborted)
42
- setError(e instanceof Error ? e.message : 'Search could not be completed. Try again.');
44
+ setError(e instanceof Error ? e.message : uiText("Search could not be completed. Try again."));
43
45
  }
44
46
  finally {
45
47
  if (id === generation.current)
@@ -55,5 +57,5 @@ export function MessagingSearch({ client, appearance, onClose }) {
55
57
  return;
56
58
  } const target = (index + direction + messages.length) % messages.length; setIndex(target); await focus(messages[target], generation.current); }
57
59
  const nav = (label, icon, direction) => _jsx(Pressable, { accessibilityRole: "button", accessibilityLabel: label, disabled: busy || !messages.length, onPress: () => void move(direction), style: { width: 36, height: 44, alignItems: 'center', justifyContent: 'center', opacity: (busy || !messages.length) ? 0.4 : 1 }, children: _jsx(MessageIcon, { name: icon, size: 20, color: c.foreground }) });
58
- return _jsxs(View, { testID: "conversation-search", style: { backgroundColor: c.background, borderBottomWidth: 1, borderBottomColor: c.foreground + '20' }, children: [_jsxs(View, { style: { flexDirection: 'row', alignItems: 'center', paddingLeft: 10, gap: 4 }, children: [_jsx(MessageIcon, { name: "search", size: 18, color: c.secondary }), _jsx(TextInput, { accessibilityLabel: "Search this chat", placeholder: "Search this chat", value: query, onChangeText: change, onSubmitEditing: () => void search(), autoFocus: true, autoCapitalize: "none", autoCorrect: false, maxLength: 200, returnKeyType: "search", placeholderTextColor: c.secondary, style: { flex: 1, minWidth: 0, minHeight: 44, color: c.foreground, fontSize: 15 } }), busy ? _jsx(ActivityIndicator, { accessibilityLabel: "Searching messages", color: c.accent }) : !!query.trim() && _jsx(Text, { accessibilityLiveRegion: "polite", style: { color: c.secondary, fontSize: 12 }, children: messages.length ? `${index + 1}/${messages.length}${next ? '+' : ''}` : '0' }), nav('Previous search result', 'chevron-up', -1), nav('Next search result', 'chevron-down', 1), _jsx(Pressable, { accessibilityRole: "button", accessibilityLabel: "Close search", onPress: onClose, style: { width: 44, height: 44, alignItems: 'center', justifyContent: 'center' }, children: _jsx(MessageIcon, { name: "close", size: 20, color: c.foreground }) })] }), !!error && _jsx(Text, { accessibilityRole: "alert", style: { color: c.foreground, paddingHorizontal: 12, paddingBottom: 8, fontSize: 13 }, children: error })] });
60
+ return _jsxs(View, { testID: "conversation-search", style: { backgroundColor: c.background, borderBottomWidth: 1, borderBottomColor: c.foreground + '20' }, children: [_jsxs(View, { style: { flexDirection: 'row', alignItems: 'center', paddingLeft: 10, gap: 4 }, children: [_jsx(MessageIcon, { name: "search", size: 18, color: c.secondary }), _jsx(TextInput, { accessibilityLabel: uiText("Search this chat"), placeholder: uiText("Search this chat"), value: query, onChangeText: change, onSubmitEditing: () => void search(), autoFocus: true, autoCapitalize: "none", autoCorrect: false, maxLength: 200, returnKeyType: "search", placeholderTextColor: c.secondary, style: { flex: 1, minWidth: 0, minHeight: 44, color: c.foreground, fontSize: 15 } }), busy ? _jsx(ActivityIndicator, { accessibilityLabel: uiText("Searching messages"), color: c.accent }) : !!query.trim() && _jsx(Text, { accessibilityLiveRegion: "polite", style: { color: c.secondary, fontSize: 12 }, children: messages.length ? `${index + 1}/${messages.length}${next ? '+' : ''}` : '0' }), nav('Previous search result', 'chevron-up', -1), nav('Next search result', 'chevron-down', 1), _jsx(Pressable, { accessibilityRole: "button", accessibilityLabel: uiText("Close search"), onPress: onClose, style: { width: 44, height: 44, alignItems: 'center', justifyContent: 'center' }, children: _jsx(MessageIcon, { name: "close", size: 20, color: c.foreground }) })] }), !!error && _jsx(Text, { accessibilityRole: "alert", style: { color: c.foreground, paddingHorizontal: 12, paddingBottom: 8, fontSize: 13 }, children: error })] });
59
61
  }
package/dist/social.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useRobonoLocale } from './localization.js';
2
3
  import { forwardRef, useImperativeHandle, useEffect, useState } from 'react';
3
4
  import { ActivityIndicator, AppState, Linking, Pressable, Text, View } from 'react-native';
4
5
  import { socialShare } from '@robono/messaging';
@@ -6,6 +7,7 @@ import { MessagingPopup } from './popup.js';
6
7
  import { MessageIcon } from './icons.js';
7
8
  import { messageLinks } from './message-links.js';
8
9
  export const SocialMessagePreview = forwardRef(function SocialMessagePreview({ message, client, Player, origin, accent, foreground, background, onLongPress, renderMessage }, ref) {
10
+ const { text: uiText, locale: uiLocale, number: uiNumber, count: uiCount } = useRobonoLocale();
9
11
  const share = messageLinks(message.text ?? '').map(link => link.url ? socialShare(link.url, origin) : null).find(Boolean);
10
12
  const [opened, setOpened] = useState(null);
11
13
  useImperativeHandle(ref, () => ({ open: () => { if (share && !message.deleted)
@@ -15,17 +17,18 @@ export const SocialMessagePreview = forwardRef(function SocialMessagePreview({ m
15
17
  setOpened(null); }); return () => sub.remove(); }, []);
16
18
  if (!share || message.deleted)
17
19
  return null;
18
- const action = _jsx(Pressable, { testID: "social-preview-action", hitSlop: 6, accessibilityRole: "button", accessibilityLabel: `Play ${share.provider} preview`, onLongPress: onLongPress, onPress: event => { event.stopPropagation(); setOpened({ client, message }); }, style: ({ pressed }) => ({ flexShrink: 0, width: 32, height: 32, borderRadius: 16, borderWidth: 1, borderColor: accent + '70', backgroundColor: accent + (pressed ? '30' : '0d'), flexDirection: 'row', alignItems: 'center', justifyContent: 'center' }), children: _jsx(MessageIcon, { name: "play", color: accent, size: 16 }) });
20
+ const action = _jsx(Pressable, { testID: "social-preview-action", hitSlop: 6, accessibilityRole: "button", accessibilityLabel: uiText("Play {value1} preview", { value1: share.provider }), onLongPress: onLongPress, onPress: event => { event.stopPropagation(); setOpened({ client, message }); }, style: ({ pressed }) => ({ flexShrink: 0, width: 32, height: 32, borderRadius: 16, borderWidth: 1, borderColor: accent + '70', backgroundColor: accent + (pressed ? '30' : '0d'), flexDirection: 'row', alignItems: 'center', justifyContent: 'center' }), children: _jsx(MessageIcon, { name: "play", color: accent, size: 16 }) });
19
21
  return _jsxs(_Fragment, { children: [renderMessage ? renderMessage(action) : action, open && _jsx(SocialPreviewPopup, { message: message, client: client, Player: Player, origin: origin, provider: share.provider, url: share.url, accent: accent, foreground: foreground, background: background, onClose: () => setOpened(null) }, message.id)] });
20
22
  });
21
23
  function SocialPreviewPopup({ message, client, Player, origin, provider, url, accent, foreground, background, onClose }) {
24
+ const { text: uiText, locale: uiLocale, number: uiNumber, count: uiCount } = useRobonoLocale();
22
25
  const [preview, setPreview] = useState(null), [busy, setBusy] = useState(true), [error, setError] = useState('');
23
26
  useEffect(() => { let active = true; void client.socialPreview(message.id, origin).then(result => { if (active) {
24
27
  setPreview(result);
25
28
  if (!result.html)
26
- setError('This post cannot be played here. Open the original below.');
29
+ setError(uiText("This post cannot be played here. Open the original below."));
27
30
  } }).catch(() => { if (active)
28
- setError('This preview could not be loaded. Open the original below.'); }).finally(() => { if (active)
31
+ setError(uiText("This preview could not be loaded. Open the original below.")); }).finally(() => { if (active)
29
32
  setBusy(false); }); return () => { active = false; }; }, [client, message.id, origin]);
30
- return _jsxs(MessagingPopup, { expanded: true, bottomPadding: 4, background: "#000000", title: provider, foreground: "#ffffff", closeLabel: "Close social preview", onClose: onClose, children: [busy && _jsx(View, { style: { flex: 1, alignItems: 'center', justifyContent: 'center' }, children: _jsx(ActivityIndicator, { accessibilityLabel: "Loading social preview", color: accent }) }), !!preview?.html && _jsx(View, { testID: "social-preview-player", style: { flex: 1, minHeight: 0, overflow: 'hidden', borderRadius: 10 }, children: _jsx(Player, { html: preview.html, frame: preview.frame, origin: origin, provider: provider, onClose: onClose, onError: () => setError('This preview is unavailable. Open the original below.') }) }), !!error && _jsx(Text, { accessibilityRole: "alert", style: { color: '#ffffff', fontSize: 13, paddingVertical: 8 }, children: error }), _jsx(Pressable, { accessibilityRole: "link", accessibilityLabel: `Open ${provider}`, onPress: () => { void Linking.openURL(url).catch(() => setError('This link could not be opened.')); }, style: { minHeight: 44, justifyContent: 'center' }, children: _jsxs(Text, { style: { color: accent, fontWeight: '600' }, children: ["Open ", provider] }) })] });
33
+ return _jsxs(MessagingPopup, { expanded: true, bottomPadding: 4, background: "#000000", title: provider, foreground: "#ffffff", closeLabel: uiText("Close social preview"), onClose: onClose, children: [busy && _jsx(View, { style: { flex: 1, alignItems: 'center', justifyContent: 'center' }, children: _jsx(ActivityIndicator, { accessibilityLabel: uiText("Loading social preview"), color: accent }) }), !!preview?.html && _jsx(View, { testID: "social-preview-player", style: { flex: 1, minHeight: 0, overflow: 'hidden', borderRadius: 10 }, children: _jsx(Player, { html: preview.html, frame: preview.frame, origin: origin, provider: provider, onClose: onClose, onError: () => setError(uiText("This preview is unavailable. Open the original below.")) }) }), !!error && _jsx(Text, { accessibilityRole: "alert", style: { color: '#ffffff', fontSize: 13, paddingVertical: 8 }, children: error }), _jsx(Pressable, { accessibilityRole: "link", accessibilityLabel: uiText("Open {value1}", { value1: provider }), onPress: () => { void Linking.openURL(url).catch(() => setError(uiText("This link could not be opened."))); }, style: { minHeight: 44, justifyContent: 'center' }, children: _jsxs(Text, { style: { color: accent, fontWeight: '600' }, children: [uiText("Open"), provider] }) })] });
31
34
  }
@@ -1,4 +1,5 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useRobonoLocale } from './localization.js';
2
3
  import { MessageIcon } from './icons.js';
3
4
  import { messagingPopupColors } from './popup.js';
4
5
  import { useState } from 'react';
@@ -6,11 +7,12 @@ import { Pressable, ScrollView, Text, View } from 'react-native';
6
7
  import { normalizeLanguageCode, supportedLanguages, supportedLanguageLabel } from '@robono/messaging';
7
8
  /** The choice belongs to the conversation, never to an individual message. */
8
9
  export function TranslationSettings({ client, appearance, preference, language, sourceLanguage, voiceAvailable, onSaved }) {
10
+ const { text: uiText, locale: uiLocale, number: uiNumber, count: uiCount } = useRobonoLocale();
9
11
  const [value, setValue] = useState(() => preference ?? { mode: 'off', language: normalizeLanguageCode(language ?? Intl.DateTimeFormat().resolvedOptions().locale) ?? 'en', sourceLanguage: normalizeLanguageCode(sourceLanguage) });
10
12
  const [choosing, setChoosing] = useState(null), [busy, setBusy] = useState(false), [error, setError] = useState('');
11
13
  const c = { ...appearance.colors, ...messagingPopupColors };
12
- return _jsxs(View, { style: { gap: 8 }, children: [_jsx(Text, { style: { fontSize: 13, fontWeight: '600', color: c.secondary, paddingHorizontal: 4 }, children: "Translation" }), _jsxs(View, { style: { backgroundColor: '#111111', borderRadius: 12, borderWidth: 1, borderColor: '#ffffff14', padding: 12, gap: 4 }, children: [_jsx(View, { accessibilityRole: "radiogroup", style: { flexDirection: 'row', borderWidth: 1, borderColor: '#ffffff14', borderRadius: 9, overflow: 'hidden', padding: 3, gap: 3 }, children: ['off', 'text', 'voice'].map(mode => _jsx(Pressable, { accessibilityRole: "radio", accessibilityLabel: 'Translation ' + mode, accessibilityState: { checked: value.mode === mode, disabled: busy || mode === 'voice' && !voiceAvailable }, disabled: busy || mode === 'voice' && !voiceAvailable, onPress: () => setValue(v => ({ ...v, mode })), style: { flex: 1, minHeight: 36, borderRadius: 6, alignItems: 'center', justifyContent: 'center', backgroundColor: value.mode === mode ? c.accent + '25' : 'transparent', opacity: mode === 'voice' && !voiceAvailable ? .4 : 1 }, children: _jsx(Text, { style: { color: value.mode === mode ? c.accent : c.secondary, fontSize: 14, fontWeight: '600' }, children: mode === 'off' ? 'Off' : mode === 'text' ? 'Text' : 'Voice' }) }, mode)) }), ['sourceLanguage', 'language'].map(field => _jsxs(View, { children: [_jsxs(Pressable, { accessibilityRole: "button", accessibilityLabel: field === 'language' ? 'Translation language' : 'Incoming language', disabled: busy, onPress: () => setChoosing(choosing === field ? null : field), style: { minHeight: 48, flexDirection: 'row', alignItems: 'center', gap: 10, borderBottomWidth: field === 'sourceLanguage' ? 1 : 0, borderBottomColor: '#ffffff14' }, children: [_jsx(Text, { style: { flex: 1, color: c.secondary, fontSize: 14 }, children: field === 'language' ? 'Translate into' : 'Messages in' }), _jsx(Text, { style: { color: c.accent, fontSize: 15, flexShrink: 1, textAlign: 'right' }, children: value[field] ? supportedLanguageLabel(value[field]) : 'Choose language' }), _jsx(MessageIcon, { name: choosing === field ? 'chevron-up' : 'chevron-down', color: c.secondary, size: 16 })] }), choosing === field && _jsx(ScrollView, { style: { maxHeight: 180 }, keyboardShouldPersistTaps: "handled", children: supportedLanguages.map(item => _jsx(Pressable, { accessibilityRole: "button", accessibilityLabel: 'Choose ' + item.name, onPress: () => { setValue(v => ({ ...v, [field]: item.code })); setChoosing(null); }, style: { minHeight: 40, paddingHorizontal: 8, justifyContent: 'center' }, children: _jsx(Text, { style: { color: c.foreground }, children: item.name }) }, item.code)) })] }, field)), !!error && _jsx(Text, { accessibilityRole: "alert", style: { color: c.foreground }, children: error }), _jsx(Pressable, { accessibilityRole: "button", accessibilityLabel: "Save translation preference", disabled: busy, onPress: () => { if (value.mode !== 'off' && !value.sourceLanguage) {
13
- setError('Choose the incoming language once for this conversation.');
14
+ return _jsxs(View, { style: { gap: 8 }, children: [_jsx(Text, { style: { fontSize: 13, fontWeight: '600', color: c.secondary, paddingHorizontal: 4 }, children: uiText("Translation") }), _jsxs(View, { style: { backgroundColor: '#111111', borderRadius: 12, borderWidth: 1, borderColor: '#ffffff14', padding: 12, gap: 4 }, children: [_jsx(View, { accessibilityRole: "radiogroup", style: { flexDirection: 'row', borderWidth: 1, borderColor: '#ffffff14', borderRadius: 9, overflow: 'hidden', padding: 3, gap: 3 }, children: ['off', 'text', 'voice'].map(mode => _jsx(Pressable, { accessibilityRole: "radio", accessibilityLabel: uiText('Translation: {mode}', { mode: mode === 'off' ? uiText('Off') : mode === 'text' ? uiText('Text') : uiText('Voice') }), accessibilityState: { checked: value.mode === mode, disabled: busy || mode === 'voice' && !voiceAvailable }, disabled: busy || mode === 'voice' && !voiceAvailable, onPress: () => setValue(v => ({ ...v, mode })), style: { flex: 1, minHeight: 36, borderRadius: 6, alignItems: 'center', justifyContent: 'center', backgroundColor: value.mode === mode ? c.accent + '25' : 'transparent', opacity: mode === 'voice' && !voiceAvailable ? .4 : 1 }, children: _jsx(Text, { style: { color: value.mode === mode ? c.accent : c.secondary, fontSize: 14, fontWeight: '600' }, children: mode === 'off' ? uiText("Off") : mode === 'text' ? uiText("Text") : uiText("Voice") }) }, mode)) }), ['sourceLanguage', 'language'].map(field => _jsxs(View, { children: [_jsxs(Pressable, { accessibilityRole: "button", accessibilityLabel: field === 'language' ? uiText("Translation language") : uiText("Incoming language"), disabled: busy, onPress: () => setChoosing(choosing === field ? null : field), style: { minHeight: 48, flexDirection: 'row', alignItems: 'center', gap: 10, borderBottomWidth: field === 'sourceLanguage' ? 1 : 0, borderBottomColor: '#ffffff14' }, children: [_jsx(Text, { style: { flex: 1, color: c.secondary, fontSize: 14 }, children: field === 'language' ? uiText("Translate into") : uiText("Messages in") }), _jsx(Text, { style: { color: c.accent, fontSize: 15, flexShrink: 1, textAlign: 'right' }, children: value[field] ? supportedLanguageLabel(value[field]) : uiText("Choose language") }), _jsx(MessageIcon, { name: choosing === field ? 'chevron-up' : 'chevron-down', color: c.secondary, size: 16 })] }), choosing === field && _jsx(ScrollView, { style: { maxHeight: 180 }, keyboardShouldPersistTaps: "handled", children: supportedLanguages.map(item => _jsx(Pressable, { accessibilityRole: "button", accessibilityLabel: uiText('Choose {language}', { language: item.name }), onPress: () => { setValue(v => ({ ...v, [field]: item.code })); setChoosing(null); }, style: { minHeight: 40, paddingHorizontal: 8, justifyContent: 'center' }, children: _jsx(Text, { style: { color: c.foreground }, children: item.name }) }, item.code)) })] }, field)), !!error && _jsx(Text, { accessibilityRole: "alert", style: { color: c.foreground }, children: error }), _jsx(Pressable, { accessibilityRole: "button", accessibilityLabel: uiText("Save translation preference"), disabled: busy, onPress: () => { if (value.mode !== 'off' && !value.sourceLanguage) {
15
+ setError(uiText("Choose the incoming language once for this conversation."));
14
16
  return;
15
- } setError(''); setBusy(true); void client.setTranslationPreference(value).then(onSaved).catch(() => setError('The translation preference could not be saved. Try again.')).finally(() => setBusy(false)); }, style: { minHeight: 40, minWidth: 96, paddingHorizontal: 18, marginTop: 8, alignSelf: 'flex-end', alignItems: 'center', justifyContent: 'center', borderRadius: 9, backgroundColor: c.accent, opacity: busy ? 0.5 : 1 }, children: _jsx(Text, { style: { color: '#111111', fontSize: 14, fontWeight: '600' }, children: busy ? 'Saving…' : 'Save' }) })] })] });
17
+ } setError(''); setBusy(true); void client.setTranslationPreference(value).then(onSaved).catch(() => setError(uiText("The translation preference could not be saved. Try again."))).finally(() => setBusy(false)); }, style: { minHeight: 40, minWidth: 96, paddingHorizontal: 18, marginTop: 8, alignSelf: 'flex-end', alignItems: 'center', justifyContent: 'center', borderRadius: 9, backgroundColor: c.accent, opacity: busy ? 0.5 : 1 }, children: _jsx(Text, { style: { color: '#111111', fontSize: 14, fontWeight: '600' }, children: busy ? uiText("Saving…") : uiText("Save") }) })] })] });
16
18
  }
@@ -1,20 +1,24 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useRobonoLocale } from './localization.js';
2
3
  import { ActivityIndicator, Pressable, Text, View } from 'react-native';
3
4
  import { MessageIcon, duration, composerSurface } from './icons.js';
4
5
  function Waveform({ samples = [], color }) {
5
- return _jsx(View, { accessibilityLabel: "Recording waveform", style: { flex: 1, minWidth: 0, height: 24, flexDirection: 'row', alignItems: 'center', gap: 2, overflow: 'hidden' }, children: Array.from({ length: 28 }, (_, i) => { const value = samples[Math.floor(i * samples.length / 28)] ?? 0; return _jsx(View, { style: { flex: 1, borderRadius: 2, height: 3 + Math.max(0, Math.min(1, value)) * 21, backgroundColor: color, opacity: samples.length ? .8 : .25 } }, i); }) });
6
+ const { text: uiText, locale: uiLocale, number: uiNumber, count: uiCount } = useRobonoLocale();
7
+ return _jsx(View, { accessibilityLabel: uiText("Recording waveform"), style: { flex: 1, minWidth: 0, height: 24, flexDirection: 'row', alignItems: 'center', gap: 2, overflow: 'hidden' }, children: Array.from({ length: 28 }, (_, i) => { const value = samples[Math.floor(i * samples.length / 28)] ?? 0; return _jsx(View, { style: { flex: 1, borderRadius: 2, height: 3 + Math.max(0, Math.min(1, value)) * 21, backgroundColor: color, opacity: samples.length ? .8 : .25 } }, i); }) });
6
8
  }
7
9
  /** Messenger's inline recorder; adapters supply media state and the optional text composer. */
8
10
  export function VoiceMessageComposer({ appearance, maxSeconds, busy, onRecord, actions, phase = 'idle', elapsedMs = 0, waveform, error, review, onStop, onSend, onRemove, renderTextComposer, renderAttachments }) {
11
+ const { text: uiText, locale: uiLocale, number: uiNumber, count: uiCount } = useRobonoLocale();
9
12
  const c = appearance.colors, recording = phase === 'recording', waiting = phase === 'preparing' || phase === 'uploading' || (busy && phase === 'idle');
10
13
  const canSend = !!review && (phase === 'review' || phase === 'failed') && !!onSend;
11
14
  if (renderTextComposer && !recording && phase !== 'preparing' && phase !== 'uploading')
12
- return _jsxs(View, { style: { gap: 4 }, children: [renderAttachments ? renderAttachments(review, waiting) : review && _jsx(View, { style: { paddingHorizontal: 8, paddingTop: 4 }, children: review }), renderTextComposer({ sendVoice: canSend ? onSend : undefined, hasVoice: !!review, blocked: waiting }), !!error && _jsxs(View, { style: { flexDirection: 'row', alignItems: 'center', gap: 8, paddingHorizontal: 8 }, children: [_jsx(Text, { accessibilityRole: "alert", style: { color: c.foreground, fontSize: 12, flex: 1 }, children: error }), onRemove && _jsx(Pressable, { accessibilityRole: "button", accessibilityLabel: "Cancel recording", onPress: onRemove, style: { padding: 8 }, children: _jsx(MessageIcon, { name: "close", color: c.foreground, size: 18 }) })] })] });
13
- return _jsxs(View, { testID: "voice-only-composer", style: { gap: 5 }, children: [renderAttachments?.(review, waiting || recording), _jsxs(View, { style: { flexDirection: 'row', alignItems: 'flex-end', gap: 4 }, children: [_jsxs(View, { style: { flex: 1, minWidth: 0, minHeight: 44, borderRadius: 22, backgroundColor: composerSurface(c.background), flexDirection: 'row', alignItems: 'center', overflow: 'hidden' }, children: [!recording && !review && !waiting && actions, review && !renderAttachments ? _jsx(View, { style: { flex: 1, minWidth: 0, paddingLeft: 10 }, children: review }) : recording ? _jsxs(View, { style: { flex: 1, minWidth: 0, flexDirection: 'row', alignItems: 'center', gap: 6, paddingLeft: 10 }, children: [_jsx(Text, { style: { fontSize: 11, fontWeight: '800', color: c.incomingText, fontVariant: ['tabular-nums'] }, children: duration(Math.floor(elapsedMs / 1000) * 1000) }), appearance.showWaveform !== false ? _jsx(Waveform, { samples: waveform, color: c.accent }) : _jsx(View, { style: { flex: 1 } }), _jsx(Text, { style: { fontSize: 11, fontWeight: '800', color: c.incomingText, fontVariant: ['tabular-nums'] }, children: duration(maxSeconds * 1000) })] }) : _jsx(View, { style: { flex: 1, minWidth: 0, paddingHorizontal: 12 }, children: _jsx(Text, { style: { color: c.secondary, fontSize: 14, fontWeight: '800' }, children: waiting ? (phase === 'uploading' ? 'Sending…' : 'Preparing…') : `Voice up to ${maxSeconds}s` }) }), _jsx(Pressable, { accessibilityRole: "button", accessibilityLabel: recording ? 'Stop recording' : 'Voice message', disabled: waiting || !!review || phase === 'failed', onPress: recording ? onStop : onRecord, style: { width: 42, height: 44, alignItems: 'center', justifyContent: 'center', opacity: (waiting || !!review || phase === 'failed') ? .45 : 1 }, children: waiting ? _jsx(ActivityIndicator, { size: "small", color: c.accent }) : _jsx(MessageIcon, { name: "mic", color: recording ? '#c94040' : c.accent, size: 23 }) })] }), _jsx(Pressable, { accessibilityRole: "button", accessibilityLabel: canSend ? 'Send voice message' : 'Send unavailable', accessibilityState: { disabled: !canSend }, disabled: !canSend, onPress: onSend, style: { width: 44, height: 44, alignItems: 'center', justifyContent: 'center', opacity: canSend ? 1 : .4 }, children: _jsx(MessageIcon, { name: "send", color: c.accent, size: 24 }) })] }), !!error && _jsxs(View, { style: { flexDirection: 'row', alignItems: 'center', gap: 8, paddingHorizontal: 8 }, children: [_jsx(Text, { accessibilityRole: "alert", style: { color: c.foreground, fontSize: 12, flex: 1 }, children: error }), onRemove && _jsx(Pressable, { accessibilityRole: "button", accessibilityLabel: "Cancel recording", onPress: onRemove, style: { padding: 8 }, children: _jsx(MessageIcon, { name: "close", color: c.foreground, size: 18 }) })] })] });
15
+ return _jsxs(View, { style: { gap: 4 }, children: [renderAttachments ? renderAttachments(review, waiting) : review && _jsx(View, { style: { paddingHorizontal: 8, paddingTop: 4 }, children: review }), renderTextComposer({ sendVoice: canSend ? onSend : undefined, hasVoice: !!review, blocked: waiting }), !!error && _jsxs(View, { style: { flexDirection: 'row', alignItems: 'center', gap: 8, paddingHorizontal: 8 }, children: [_jsx(Text, { accessibilityRole: "alert", style: { color: c.foreground, fontSize: 12, flex: 1 }, children: error }), onRemove && _jsx(Pressable, { accessibilityRole: "button", accessibilityLabel: uiText("Cancel recording"), onPress: onRemove, style: { padding: 8 }, children: _jsx(MessageIcon, { name: "close", color: c.foreground, size: 18 }) })] })] });
16
+ return _jsxs(View, { testID: "voice-only-composer", style: { gap: 5 }, children: [renderAttachments?.(review, waiting || recording), _jsxs(View, { style: { flexDirection: 'row', alignItems: 'flex-end', gap: 4 }, children: [_jsxs(View, { style: { flex: 1, minWidth: 0, minHeight: 44, borderRadius: 22, backgroundColor: composerSurface(c.background), flexDirection: 'row', alignItems: 'center', overflow: 'hidden' }, children: [!recording && !review && !waiting && actions, review && !renderAttachments ? _jsx(View, { style: { flex: 1, minWidth: 0, paddingLeft: 10 }, children: review }) : recording ? _jsxs(View, { style: { flex: 1, minWidth: 0, flexDirection: 'row', alignItems: 'center', gap: 6, paddingLeft: 10 }, children: [_jsx(Text, { style: { fontSize: 11, fontWeight: '800', color: c.incomingText, fontVariant: ['tabular-nums'] }, children: duration(Math.floor(elapsedMs / 1000) * 1000) }), appearance.showWaveform !== false ? _jsx(Waveform, { samples: waveform, color: c.accent }) : _jsx(View, { style: { flex: 1 } }), _jsx(Text, { style: { fontSize: 11, fontWeight: '800', color: c.incomingText, fontVariant: ['tabular-nums'] }, children: duration(maxSeconds * 1000) })] }) : _jsx(View, { style: { flex: 1, minWidth: 0, paddingHorizontal: 12 }, children: _jsx(Text, { style: { color: c.secondary, fontSize: 14, fontWeight: '800' }, children: waiting ? (phase === 'uploading' ? 'Sending…' : 'Preparing…') : uiText("Voice up to {value1}s", { value1: maxSeconds }) }) }), _jsx(Pressable, { accessibilityRole: "button", accessibilityLabel: recording ? uiText("Stop recording") : uiText("Voice message"), disabled: waiting || !!review || phase === 'failed', onPress: recording ? onStop : onRecord, style: { width: 42, height: 44, alignItems: 'center', justifyContent: 'center', opacity: (waiting || !!review || phase === 'failed') ? .45 : 1 }, children: waiting ? _jsx(ActivityIndicator, { size: "small", color: c.accent }) : _jsx(MessageIcon, { name: "mic", color: recording ? '#c94040' : c.accent, size: 23 }) })] }), _jsx(Pressable, { accessibilityRole: "button", accessibilityLabel: canSend ? uiText("Send voice message") : uiText("Send unavailable"), accessibilityState: { disabled: !canSend }, disabled: !canSend, onPress: onSend, style: { width: 44, height: 44, alignItems: 'center', justifyContent: 'center', opacity: canSend ? 1 : .4 }, children: _jsx(MessageIcon, { name: "send", color: c.accent, size: 24 }) })] }), !!error && _jsxs(View, { style: { flexDirection: 'row', alignItems: 'center', gap: 8, paddingHorizontal: 8 }, children: [_jsx(Text, { accessibilityRole: "alert", style: { color: c.foreground, fontSize: 12, flex: 1 }, children: error }), onRemove && _jsx(Pressable, { accessibilityRole: "button", accessibilityLabel: uiText("Cancel recording"), onPress: onRemove, style: { padding: 8 }, children: _jsx(MessageIcon, { name: "close", color: c.foreground, size: 18 }) })] })] });
14
17
  }
15
18
  export function VoiceDraftPlayback({ appearance, playing, positionMs = 0, durationMs = 0, waveform, onToggle, onRemove, disabled = false, error, compact = false }) {
19
+ const { text: uiText, locale: uiLocale, number: uiNumber, count: uiCount } = useRobonoLocale();
16
20
  const c = appearance.colors;
17
21
  if (compact)
18
- return _jsxs(View, { style: { width: 88, height: 84, paddingTop: 8, paddingRight: 10 }, children: [_jsxs(View, { style: { width: 76, height: 76, borderRadius: 9, borderWidth: 1, borderColor: c.secondary + '40', backgroundColor: c.background, alignItems: 'center', justifyContent: 'center', gap: 2 }, children: [_jsx(Pressable, { accessibilityRole: "button", accessibilityLabel: playing ? 'Pause recorded voice message' : 'Play recorded voice message', disabled: disabled, onPress: onToggle, style: { width: 44, height: 38, alignItems: 'center', justifyContent: 'center' }, children: _jsx(View, { style: { width: 28, height: 28, borderRadius: 14, backgroundColor: c.accent, alignItems: 'center', justifyContent: 'center' }, children: _jsx(MessageIcon, { name: playing ? 'pause' : 'play', color: "#fff", size: 18 }) }) }), _jsx(Text, { style: { fontSize: 10, fontWeight: '800', color: c.foreground }, children: duration(positionMs >= durationMs ? durationMs : durationMs - positionMs) })] }), _jsx(Pressable, { accessibilityRole: "button", accessibilityLabel: "Remove voice message", disabled: disabled, onPress: onRemove, style: { position: 'absolute', right: 0, top: 0, width: 30, height: 30, alignItems: 'center', justifyContent: 'center' }, children: _jsx(View, { style: { width: 22, height: 22, borderRadius: 11, backgroundColor: c.foreground, alignItems: 'center', justifyContent: 'center' }, children: _jsx(MessageIcon, { name: "close", color: c.background, size: 14 }) }) }), !!error && _jsx(Text, { accessibilityRole: "alert", style: { color: c.foreground, fontSize: 11 }, children: error })] });
19
- return _jsxs(View, { style: { flex: 1, minWidth: 0, gap: 3 }, children: [_jsxs(View, { style: { flexDirection: 'row', alignItems: 'center', gap: 7, minHeight: 44 }, children: [_jsx(Pressable, { accessibilityRole: "button", accessibilityLabel: playing ? 'Pause recorded voice message' : 'Play recorded voice message', disabled: disabled, onPress: onToggle, style: { width: 32, height: 32, borderRadius: 16, backgroundColor: c.accent, alignItems: 'center', justifyContent: 'center' }, children: _jsx(MessageIcon, { name: playing ? 'pause' : 'play', color: "#fff", size: 17 }) }), appearance.showWaveform !== false ? _jsx(Waveform, { samples: waveform, color: c.accent }) : _jsx(View, { style: { flex: 1 } }), _jsx(Text, { style: { fontSize: 12, fontWeight: '800', color: c.incomingText, fontVariant: ['tabular-nums'] }, children: duration(positionMs >= durationMs ? durationMs : durationMs - positionMs) }), _jsx(Pressable, { accessibilityRole: "button", accessibilityLabel: "Remove voice message", disabled: disabled, onPress: onRemove, style: { width: 30, height: 44, alignItems: 'center', justifyContent: 'center' }, children: _jsx(MessageIcon, { name: "close", color: c.incomingText, size: 16 }) })] }), !!error && _jsxs(View, { style: { flexDirection: 'row', alignItems: 'center', gap: 8, paddingHorizontal: 8 }, children: [_jsx(Text, { accessibilityRole: "alert", style: { color: c.foreground, fontSize: 12, flex: 1 }, children: error }), onRemove && _jsx(Pressable, { accessibilityRole: "button", accessibilityLabel: "Cancel recording", onPress: onRemove, style: { padding: 8 }, children: _jsx(MessageIcon, { name: "close", color: c.foreground, size: 18 }) })] })] });
22
+ return _jsxs(View, { style: { width: 88, height: 84, paddingTop: 8, paddingRight: 10 }, children: [_jsxs(View, { style: { width: 76, height: 76, borderRadius: 9, borderWidth: 1, borderColor: c.secondary + '40', backgroundColor: c.background, alignItems: 'center', justifyContent: 'center', gap: 2 }, children: [_jsx(Pressable, { accessibilityRole: "button", accessibilityLabel: playing ? uiText("Pause recorded voice message") : uiText("Play recorded voice message"), disabled: disabled, onPress: onToggle, style: { width: 44, height: 38, alignItems: 'center', justifyContent: 'center' }, children: _jsx(View, { style: { width: 28, height: 28, borderRadius: 14, backgroundColor: c.accent, alignItems: 'center', justifyContent: 'center' }, children: _jsx(MessageIcon, { name: playing ? 'pause' : 'play', color: "#fff", size: 18 }) }) }), _jsx(Text, { style: { fontSize: 10, fontWeight: '800', color: c.foreground }, children: duration(positionMs >= durationMs ? durationMs : durationMs - positionMs) })] }), _jsx(Pressable, { accessibilityRole: "button", accessibilityLabel: uiText("Remove voice message"), disabled: disabled, onPress: onRemove, style: { position: 'absolute', right: 0, top: 0, width: 30, height: 30, alignItems: 'center', justifyContent: 'center' }, children: _jsx(View, { style: { width: 22, height: 22, borderRadius: 11, backgroundColor: c.foreground, alignItems: 'center', justifyContent: 'center' }, children: _jsx(MessageIcon, { name: "close", color: c.background, size: 14 }) }) }), !!error && _jsx(Text, { accessibilityRole: "alert", style: { color: c.foreground, fontSize: 11 }, children: error })] });
23
+ return _jsxs(View, { style: { flex: 1, minWidth: 0, gap: 3 }, children: [_jsxs(View, { style: { flexDirection: 'row', alignItems: 'center', gap: 7, minHeight: 44 }, children: [_jsx(Pressable, { accessibilityRole: "button", accessibilityLabel: playing ? uiText("Pause recorded voice message") : uiText("Play recorded voice message"), disabled: disabled, onPress: onToggle, style: { width: 32, height: 32, borderRadius: 16, backgroundColor: c.accent, alignItems: 'center', justifyContent: 'center' }, children: _jsx(MessageIcon, { name: playing ? 'pause' : 'play', color: "#fff", size: 17 }) }), appearance.showWaveform !== false ? _jsx(Waveform, { samples: waveform, color: c.accent }) : _jsx(View, { style: { flex: 1 } }), _jsx(Text, { style: { fontSize: 12, fontWeight: '800', color: c.incomingText, fontVariant: ['tabular-nums'] }, children: duration(positionMs >= durationMs ? durationMs : durationMs - positionMs) }), _jsx(Pressable, { accessibilityRole: "button", accessibilityLabel: uiText("Remove voice message"), disabled: disabled, onPress: onRemove, style: { width: 30, height: 44, alignItems: 'center', justifyContent: 'center' }, children: _jsx(MessageIcon, { name: "close", color: c.incomingText, size: 16 }) })] }), !!error && _jsxs(View, { style: { flexDirection: 'row', alignItems: 'center', gap: 8, paddingHorizontal: 8 }, children: [_jsx(Text, { accessibilityRole: "alert", style: { color: c.foreground, fontSize: 12, flex: 1 }, children: error }), onRemove && _jsx(Pressable, { accessibilityRole: "button", accessibilityLabel: uiText("Cancel recording"), onPress: onRemove, style: { padding: 8 }, children: _jsx(MessageIcon, { name: "close", color: c.foreground, size: 18 }) })] })] });
20
24
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@robono/messaging-react-native",
3
- "version": "0.8.0",
3
+ "version": "0.9.0",
4
4
  "type": "module",
5
5
  "sideEffects": false,
6
6
  "description": "Optional, composable direct and group messaging screens for Robono.",
@@ -36,7 +36,8 @@
36
36
  "./expo-notifications": {
37
37
  "types": "./dist/expo-notifications.d.ts",
38
38
  "import": "./dist/expo-notifications.js"
39
- }
39
+ },
40
+ "./setup": "./setup.plugin.cjs"
40
41
  },
41
42
  "files": [
42
43
  "dist",
@@ -46,14 +47,15 @@
46
47
  "plugin",
47
48
  "CHANGELOG.md",
48
49
  "SBOM.spdx.json",
49
- "licenses"
50
+ "licenses",
51
+ "setup.plugin.cjs"
50
52
  ],
51
53
  "scripts": {
52
54
  "build": "tsc -p tsconfig.json",
53
55
  "prepack": "npm run build && node ../../scripts/generate-sdk-file-sbom.mjs"
54
56
  },
55
57
  "peerDependencies": {
56
- "@robono/messaging": "^0.5.0",
58
+ "@robono/messaging": "^0.6.0",
57
59
  "react": ">=18.2 <20",
58
60
  "react-native": ">=0.76 <1",
59
61
  "expo-audio": ">=55 <57",
@@ -65,7 +67,7 @@
65
67
  "expo-crypto": ">=55 <57",
66
68
  "react-native-safe-area-context": ">=5 <6",
67
69
  "react-native-svg": ">=15 <16",
68
- "@robono/calling-react-native": "^0.6.0",
70
+ "@robono/calling-react-native": "^0.7.0",
69
71
  "expo-clipboard": ">=8 <57",
70
72
  "expo-image-manipulator": ">=14 <57",
71
73
  "expo-video-thumbnails": ">=14 <57",
@@ -73,10 +75,10 @@
73
75
  "expo-blur": ">=55 <57",
74
76
  "expo-notifications": ">=55 <57",
75
77
  "expo-secure-store": ">=55 <57",
76
- "@robono/location-react-native": "^0.2.0",
78
+ "@robono/location-react-native": "^0.3.0",
77
79
  "expo-location": ">=55 <57",
78
80
  "expo-constants": ">=55 <57",
79
- "@robono/location": "^0.2.0",
81
+ "@robono/location": "^0.3.0",
80
82
  "expo-haptics": ">=55 <57",
81
83
  "@robono/calling": "^0.4.0"
82
84
  },
@@ -0,0 +1,20 @@
1
+ const {withPlugins,createRunOncePlugin}=require('expo/config-plugins');
2
+ const fs=require('node:fs');
3
+ const path=require('node:path');
4
+
5
+ /** One composition point for hosted Messenger. Explicit host plugin choices win. */
6
+ module.exports=createRunOncePlugin(config=>{
7
+ const existing=new Set((config.plugins??[]).map(p=>Array.isArray(p)?p[0]:p));
8
+ const plugins=[
9
+ ['expo-sensors',{motionPermission:'Allow this app to share device movement with the people you choose. This does not share your location.'}],
10
+ ['expo-location',{locationWhenInUsePermission:'Allow location access when you choose to share weather or your location.'}],
11
+ ['expo-audio',{microphonePermission:'Allow microphone access for voice messages and calls.',enableBackgroundRecording:false}],
12
+ ['expo-image-picker',{cameraPermission:'Allow camera access to take photos and videos for messages.'}],
13
+ 'expo-notifications','@robono/messaging-react-native',
14
+ ['@robono/calling-react-native',{video:true,screenSharing:true}],
15
+ ];
16
+ const root=config._internal?.projectRoot;
17
+ const accessFiles={ios:'.robono/maps-ios.json',android:'.robono/maps-android.json'};
18
+ if(root&&Object.values(accessFiles).every(file=>fs.existsSync(path.join(root,file))))plugins.push(['@robono/location-react-native',{accessFiles,background:true}]);
19
+ return withPlugins(config,plugins.filter(p=>!existing.has(Array.isArray(p)?p[0]:p)));
20
+ },'@robono/messaging-react-native/setup',require('./package.json').version);