@robono/messaging-react-native 0.7.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.
- package/CHANGELOG.md +9 -1
- package/README.md +40 -37
- package/SBOM.spdx.json +235 -163
- package/dist/account.js +5 -3
- package/dist/attachment-menu.js +3 -1
- package/dist/call-message.js +6 -4
- package/dist/calling.js +3 -1
- package/dist/conversation-details.js +9 -6
- package/dist/expo-connected-calling.d.ts +4 -2
- package/dist/expo-connected-calling.js +19 -4
- package/dist/expo-extras.js +4 -1
- package/dist/expo-media.js +27 -5
- package/dist/expo-messenger.d.ts +42 -4
- package/dist/expo-messenger.js +57 -7
- package/dist/expo-notifications.d.ts +2 -0
- package/dist/expo-notifications.js +8 -3
- package/dist/expo-outbox.js +22 -1
- package/dist/expo-screen.d.ts +1 -1
- package/dist/expo-screen.js +23 -14
- package/dist/expo-social.js +3 -1
- package/dist/expo.d.ts +1 -1
- package/dist/expo.js +1 -1
- package/dist/extras.js +9 -6
- package/dist/forward.js +6 -4
- package/dist/group-editor.js +7 -5
- package/dist/header-calls.js +7 -5
- package/dist/inbox-search.js +4 -2
- package/dist/inbox.js +9 -7
- package/dist/index.d.ts +6 -3
- package/dist/index.js +91 -61
- package/dist/localization.d.ts +24 -0
- package/dist/localization.js +12 -0
- package/dist/maps-connection.js +3 -1
- package/dist/media-ui.js +13 -7
- package/dist/message-menu.js +3 -1
- package/dist/native-outbox.d.ts +9 -1
- package/dist/native-outbox.js +7 -0
- package/dist/polls.js +18 -14
- package/dist/popup.js +6 -3
- package/dist/quick-send.js +4 -2
- package/dist/safety.js +9 -7
- package/dist/saved.js +5 -3
- package/dist/scheduled.js +17 -13
- package/dist/search.js +5 -3
- package/dist/social.js +7 -4
- package/dist/translation-settings.js +5 -3
- package/dist/voice-composer.js +9 -5
- package/package.json +9 -7
- package/setup.plugin.cjs +20 -0
|
@@ -10,7 +10,7 @@ export async function connectExpoMessagingNotifications(options) {
|
|
|
10
10
|
const endpoint = new URL(options.url);
|
|
11
11
|
if (endpoint.protocol !== 'https:' || endpoint.username || endpoint.password)
|
|
12
12
|
throw Error('Use your authenticated HTTPS notification endpoint.');
|
|
13
|
-
let disposed = false, closing = false, registerQueue = Promise.resolve(), accountScope;
|
|
13
|
+
let disposed = false, closing = false, registerQueue = Promise.resolve(), accountScope, lastToken;
|
|
14
14
|
const key = 'robono.messaging.device.' + await Crypto.digestStringAsync(Crypto.CryptoDigestAlgorithm.SHA256, endpoint.origin + endpoint.pathname);
|
|
15
15
|
let deviceId = await SecureStore.getItemAsync(key);
|
|
16
16
|
if (!deviceId) {
|
|
@@ -33,16 +33,21 @@ export async function connectExpoMessagingNotifications(options) {
|
|
|
33
33
|
accountScope = status.accountScope;
|
|
34
34
|
const router = createMessagingNotificationRouter({ accountScope: status.accountScope, authorize: options.authorize, open: options.open, refresh: options.refresh });
|
|
35
35
|
const register = (token) => {
|
|
36
|
+
lastToken = token;
|
|
36
37
|
const work = registerQueue.then(async () => {
|
|
37
38
|
if (disposed || closing)
|
|
38
39
|
return;
|
|
39
|
-
const
|
|
40
|
+
const system = await options.systemCalls?.registration();
|
|
41
|
+
const data = await request({ operation: 'register-device', calling: !!options.onCall, ...system, deviceId, provider: Platform.OS === 'ios' ? 'apns' : 'fcm', token: String(token.data) });
|
|
40
42
|
if (data.accountScope !== status.accountScope)
|
|
41
43
|
throw Error('The signed-in account changed. Reconnect notifications for the new account.');
|
|
42
44
|
});
|
|
43
45
|
registerQueue = work.catch(() => { });
|
|
44
46
|
return work;
|
|
45
47
|
};
|
|
48
|
+
if (options.systemCalls)
|
|
49
|
+
await options.systemCalls.configure(status.accountScope, () => { if (lastToken && !closing && !disposed)
|
|
50
|
+
void register(lastToken).catch(options.onError); });
|
|
46
51
|
const tokenListener = Notifications.addPushTokenListener(token => { void register(token).catch(options.onError); });
|
|
47
52
|
const handle = async (data, opened = false) => { if (!data || disposed || closing)
|
|
48
53
|
return false; if (data.accountScope === accountScope && ['calling.incoming', 'calling.dismissed', 'calling.group_incoming', 'calling.group_dismissed'].includes(String(data.event)) && options.onCall) {
|
|
@@ -74,7 +79,7 @@ export async function connectExpoMessagingNotifications(options) {
|
|
|
74
79
|
await response(await Notifications.getLastNotificationResponseAsync());
|
|
75
80
|
})();
|
|
76
81
|
void ready.catch(options.onError);
|
|
77
|
-
const dispose = () => { disposed = true; router.dispose(); tokenListener.remove(); received.remove(); opened.remove(); };
|
|
82
|
+
const dispose = () => { disposed = true; router.dispose(); tokenListener.remove(); received.remove(); opened.remove(); void options.systemCalls?.disconnect().catch(options.onError); };
|
|
78
83
|
return {
|
|
79
84
|
ready,
|
|
80
85
|
/** Call only after the user chooses Enable notifications in your interface. */
|
package/dist/expo-outbox.js
CHANGED
|
@@ -12,6 +12,27 @@ const root = async (scope) => { if (!FS.documentDirectory)
|
|
|
12
12
|
const folder = async (scope, cid) => (await root(scope)) + await digest(cid) + '/';
|
|
13
13
|
const asset = async (scope, cid, id) => (await folder(scope, cid)) + 'file-' + await digest(id);
|
|
14
14
|
const files = {
|
|
15
|
+
async conversations(scope) {
|
|
16
|
+
const dir = await root(scope);
|
|
17
|
+
if (!(await FS.getInfoAsync(dir)).exists)
|
|
18
|
+
return [];
|
|
19
|
+
const ids = [];
|
|
20
|
+
for (const folderName of await FS.readDirectoryAsync(dir)) {
|
|
21
|
+
if (!/^[a-f0-9]{64}$/.test(folderName))
|
|
22
|
+
continue;
|
|
23
|
+
const folder = dir + folderName + '/';
|
|
24
|
+
const names = (await FS.readDirectoryAsync(folder)).filter(name => /^state-\d+\.json$/.test(name)).sort((a, b) => Number(b.slice(6, -5)) - Number(a.slice(6, -5)));
|
|
25
|
+
for (const name of names)
|
|
26
|
+
try {
|
|
27
|
+
const state = JSON.parse(await FS.readAsStringAsync(folder + name));
|
|
28
|
+
if (typeof state.conversationId === 'string' && Array.isArray(state.messages) && (state.messages.some((m) => m.retry) || Array.isArray(state.actions) && state.actions[0]?.retry))
|
|
29
|
+
ids.push(state.conversationId);
|
|
30
|
+
break;
|
|
31
|
+
}
|
|
32
|
+
catch { }
|
|
33
|
+
}
|
|
34
|
+
return [...new Set(ids)];
|
|
35
|
+
},
|
|
15
36
|
async read(scope, cid) {
|
|
16
37
|
const dir = await folder(scope, cid);
|
|
17
38
|
if (!(await FS.getInfoAsync(dir)).exists)
|
|
@@ -35,7 +56,7 @@ const files = {
|
|
|
35
56
|
const names = (await FS.readDirectoryAsync(dir)).filter(name => /^state-\d+\.json$/.test(name)).sort((a, b) => Number(b.slice(6, -5)) - Number(a.slice(6, -5)));
|
|
36
57
|
const sequence = names.length ? Number(names[0].slice(6, -5)) + 1 : 1, temp = dir + 'pending-' + Crypto.randomUUID() + '.tmp';
|
|
37
58
|
try {
|
|
38
|
-
await FS.writeAsStringAsync(temp, JSON.stringify(state));
|
|
59
|
+
await FS.writeAsStringAsync(temp, JSON.stringify({ ...state, conversationId: cid }));
|
|
39
60
|
await FS.moveAsync({ from: temp, to: dir + 'state-' + sequence + '.json' });
|
|
40
61
|
}
|
|
41
62
|
finally {
|
package/dist/expo-screen.d.ts
CHANGED
|
@@ -39,4 +39,4 @@ export interface ExpoMessagingScreenProps extends Omit<MessagingScreenProps, 'pi
|
|
|
39
39
|
resolveAttachmentChoice?: (id: string, signal: AbortSignal) => Promise<LocalMessagingMedia | null>;
|
|
40
40
|
}
|
|
41
41
|
/** Optional ready-made recording, preview, upload and download tools for both mobile platforms. */
|
|
42
|
-
export declare function ExpoMessagingScreen(
|
|
42
|
+
export declare function ExpoMessagingScreen(props: ExpoMessagingScreenProps): React.JSX.Element;
|
package/dist/expo-screen.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { RobonoLocaleProvider } from './localization.js';
|
|
3
|
+
import { useRobonoLocale } from './localization.js';
|
|
2
4
|
import * as Haptics from 'expo-haptics';
|
|
3
5
|
import { Vibration } from 'react-native';
|
|
4
6
|
import { MessagingPopupHeader, messagingPopupBackdrop } from './popup.js';
|
|
@@ -68,6 +70,7 @@ async function acquireVoice(scope, audio) {
|
|
|
68
70
|
return scope.alive && scope.epoch === epoch && !!scope.release;
|
|
69
71
|
}
|
|
70
72
|
function VoicePlayer({ file, audio, design, inline, draft, autoPlay = false }) {
|
|
73
|
+
const { text: uiText, locale: uiLocale, number: uiNumber, count: uiCount } = useRobonoLocale();
|
|
71
74
|
const [playing, setPlaying] = useState(false), [position, setPosition] = useState(0), [error, setError] = useState('');
|
|
72
75
|
const source = useMemo(() => file.stream ? { uri: file.uri, headers: file.stream.headers } : file.uri, [file]);
|
|
73
76
|
const player = useAudioPlayer(source, { updateInterval: 100, downloadFirst: false, preferredForwardBufferDuration: 5, keepAudioSessionActive: true });
|
|
@@ -110,7 +113,7 @@ function VoicePlayer({ file, audio, design, inline, draft, autoPlay = false }) {
|
|
|
110
113
|
}
|
|
111
114
|
catch { } await releaseVoice(scope); await scope.acquiring?.catch(() => { }); await releaseVoice(scope); };
|
|
112
115
|
const fail = (e) => { cancelled = true; autoPreparing = false; scope.busy = false; clearTimeout(startupTimeout); if (scope.alive)
|
|
113
|
-
setError(e instanceof Error ? e.message :
|
|
116
|
+
setError(e instanceof Error ? e.message : uiText("Playback failed.")); scope.epoch++; void releaseVoice(scope); };
|
|
114
117
|
// Native file loading and audio-session preparation are independent. Start
|
|
115
118
|
// both now; playback still requires both and an active app.
|
|
116
119
|
const begin = () => {
|
|
@@ -217,7 +220,7 @@ function VoicePlayer({ file, audio, design, inline, draft, autoPlay = false }) {
|
|
|
217
220
|
}
|
|
218
221
|
catch (e) {
|
|
219
222
|
if (scope.alive)
|
|
220
|
-
setError(e instanceof Error ? e.message :
|
|
223
|
+
setError(e instanceof Error ? e.message : uiText("Playback failed."));
|
|
221
224
|
await releaseVoice(scope);
|
|
222
225
|
}
|
|
223
226
|
finally {
|
|
@@ -230,9 +233,10 @@ function VoicePlayer({ file, audio, design, inline, draft, autoPlay = false }) {
|
|
|
230
233
|
if (draft)
|
|
231
234
|
return _jsx(VoiceDraftPlayback, { compact: draft.compact, appearance: design, playing: playing, positionMs: position, durationMs: duration, waveform: draft.waveform, onToggle: () => void toggle(), onRemove: draft.onRemove, disabled: draft.disabled, error: error });
|
|
232
235
|
return _jsx(VoicePlayback, { mine: inline?.mine, heard: inline?.heard, onPlayed: inline?.onPlayed, background: inline?.background, ink: inline?.ink ?? c.foreground, accent: inline?.accent ?? c.accent, waveform: inline?.waveform, showWaveform: inline?.showWaveform, footer: inline?.footer, onLongPress: inline?.onLongPress, durationMs: duration, positionMs: position, playing: playing, loading: !!file.stream && !loaded && !error, error: error, onToggle: () => void toggle(), onSeek: ms => { lastPosition.current = Math.max(0, Math.min(duration, ms)); void player.seekTo(lastPosition.current / 1000).catch(() => { if (!recoverStream())
|
|
233
|
-
setError(
|
|
236
|
+
setError(uiText("Could not seek. Try again.")); }); } });
|
|
234
237
|
}
|
|
235
238
|
function VideoPlayer({ file, audio }) {
|
|
239
|
+
const { text: uiText, locale: uiLocale, number: uiNumber, count: uiCount } = useRobonoLocale();
|
|
236
240
|
const mediaReady = useContext(MediaReadiness), recover = useContext(StreamRecovery);
|
|
237
241
|
const source = useMemo(() => file.stream ? { uri: file.uri, headers: file.stream.headers, contentType: 'progressive', useCaching: false } : file.uri, [file]);
|
|
238
242
|
const player = useVideoPlayer(source, p => { p.loop = false; p.audioMixingMode = 'mixWithOthers'; if (file.stream)
|
|
@@ -271,7 +275,7 @@ function VideoPlayer({ file, audio }) {
|
|
|
271
275
|
desired.current = false;
|
|
272
276
|
player.pause();
|
|
273
277
|
setReady(false);
|
|
274
|
-
setError(
|
|
278
|
+
setError(uiText("Playback paused for a call. Close and reopen the video to continue."));
|
|
275
279
|
void release?.().catch(() => { });
|
|
276
280
|
release = undefined;
|
|
277
281
|
} }, 100);
|
|
@@ -287,14 +291,14 @@ function VideoPlayer({ file, audio }) {
|
|
|
287
291
|
});
|
|
288
292
|
const listener = player.addListener('statusChange', s => { if (s.status === 'error') {
|
|
289
293
|
if (!recoverStream())
|
|
290
|
-
setError(
|
|
294
|
+
setError(uiText("This video could not be played on this phone."));
|
|
291
295
|
}
|
|
292
296
|
else
|
|
293
297
|
begin(); });
|
|
294
298
|
if (player.status === 'error')
|
|
295
299
|
recoverStream();
|
|
296
300
|
const timeout = setTimeout(() => { if (alive && desired.current && player.currentTime === 0 && player.status !== 'readyToPlay' && !recoverStream())
|
|
297
|
-
setError(
|
|
301
|
+
setError(uiText("This video could not be loaded. Try again.")); }, 15000);
|
|
298
302
|
const app = AppState.addEventListener('change', s => { if (s !== 'active') {
|
|
299
303
|
desired.current = false;
|
|
300
304
|
player.pause();
|
|
@@ -307,25 +311,27 @@ function VideoPlayer({ file, audio }) {
|
|
|
307
311
|
return _jsxs(View, { style: { flex: 1, minHeight: 0 }, children: [_jsx(MessengerVideoView, { testID: "messenger-video-player", onFirstFrameRender: () => mediaReady('video-frame', file.uri), player: player, nativeControls: ready, style: { height: '100%', width: '100%' } }), !!error && _jsx(Text, { accessibilityRole: "alert", style: { position: 'absolute', bottom: 12, left: 12, right: 12, color: '#fff', backgroundColor: '#000c', padding: 12 }, children: error })] });
|
|
308
312
|
}
|
|
309
313
|
function FileViewer({ file, kind, audio, design }) {
|
|
314
|
+
const { text: uiText, locale: uiLocale, number: uiNumber, count: uiCount } = useRobonoLocale();
|
|
310
315
|
const [error, setError] = useState('');
|
|
311
316
|
if (kind === 'voice')
|
|
312
317
|
return _jsx(VoicePlayer, { file: file, audio: audio, design: design });
|
|
313
318
|
if (kind === 'video')
|
|
314
319
|
return _jsx(VideoPlayer, { file: file, audio: audio, design: design });
|
|
315
320
|
if (kind === 'image')
|
|
316
|
-
return _jsxs(View, { children: [_jsx(Image, { source: { uri: file.uri }, resizeMode: "contain", accessibilityLabel: file.fileName, onError: () => setError(
|
|
317
|
-
return _jsxs(View, { children: [_jsx(Control, { label: "File options", icon: "open-in-new", color: design.colors.accent, onPress: () => {
|
|
321
|
+
return _jsxs(View, { children: [_jsx(Image, { source: { uri: file.uri }, resizeMode: "contain", accessibilityLabel: file.fileName, onError: () => setError(uiText("This image could not be displayed on this phone.")), style: { height: 300, width: '100%' } }), !!error && _jsx(Text, { style: { color: design.colors.foreground }, children: error })] });
|
|
322
|
+
return _jsxs(View, { children: [_jsx(Control, { label: uiText("File options"), icon: "open-in-new", color: design.colors.accent, onPress: () => {
|
|
318
323
|
void (async () => { try {
|
|
319
324
|
if (!await Sharing.isAvailableAsync())
|
|
320
325
|
throw Error('No file-opening menu is available.');
|
|
321
326
|
await Sharing.shareAsync(file.uri, { mimeType: file.mimeType, dialogTitle: file.fileName });
|
|
322
327
|
}
|
|
323
328
|
catch (e) {
|
|
324
|
-
setError(e instanceof Error ? e.message :
|
|
329
|
+
setError(e instanceof Error ? e.message : uiText("The file could not be opened."));
|
|
325
330
|
} })();
|
|
326
331
|
} }), !!error && _jsx(Text, { accessibilityRole: "alert", style: { color: design.colors.foreground }, children: error })] });
|
|
327
332
|
}
|
|
328
333
|
export function MessagingMediaOverlay({ controller, audio, appearance, inlineVoice = false, inlineRecorder = false, viewerActions, onDismiss }) {
|
|
334
|
+
const { text: uiText, locale: uiLocale, number: uiNumber, count: uiCount } = useRobonoLocale();
|
|
329
335
|
const state = useSyncExternalStore(controller.subscribe, controller.getSnapshot, controller.getSnapshot);
|
|
330
336
|
const c = appearance.colors;
|
|
331
337
|
const mediaReady = useContext(MediaReadiness);
|
|
@@ -338,24 +344,27 @@ export function MessagingMediaOverlay({ controller, audio, appearance, inlineVoi
|
|
|
338
344
|
return null;
|
|
339
345
|
if ((typeof inlineVoice === 'function' ? inlineVoice() : inlineVoice) && state.kind === 'voice' && ['opening', 'viewing', 'failed'].includes(state.phase) && !review)
|
|
340
346
|
return null;
|
|
341
|
-
const viewer = state.file && (review || state.phase === 'viewing') ? _jsx(MediaViewer, { expanded: expanded, onExpandedChange: setExpanded, kind: state.kind, uri: state.file.uri, title: review ?
|
|
342
|
-
return; saving.current = true; setDownloading(true); setFileError(''); void controller.exportFile(async (file) => { await saveMessagingAttachment(file, state.kind); mediaReady('file-opened', state.file.uri); }).catch(e => setFileError(e instanceof Error ? e.message :
|
|
347
|
+
const viewer = state.file && (review || state.phase === 'viewing') ? _jsx(MediaViewer, { expanded: expanded, onExpandedChange: setExpanded, kind: state.kind, uri: state.file.uri, title: review ? uiText("Preview attachment") : state.kind === 'image' ? uiText("Photo") : state.kind === 'video' ? uiText("Video") : state.kind === 'voice' ? uiText("Voice message") : uiText("File"), subtitle: state.file.fileName, fileName: state.file.fileName, byteSize: state.file.byteSize, error: state.error ?? fileError, onClose: () => { setFileError(''); void controller.cancel(); }, reviewAction: review ? (state.phase === 'failed' ? 'Retry upload' : 'Add attachment') : undefined, onReview: review ? () => void controller.send() : undefined, ...(!review ? viewerActions?.() : {}), downloading: downloading, onDownload: !review && appearance.showDownloadButton === true && ['image', 'video', 'document'].includes(state.kind) ? () => { if (saving.current || !state.file)
|
|
348
|
+
return; saving.current = true; setDownloading(true); setFileError(''); void controller.exportFile(async (file) => { await saveMessagingAttachment(file, state.kind); mediaReady('file-opened', state.file.uri); }).catch(e => setFileError(e instanceof Error ? e.message : uiText("Could not save the attachment."))).finally(() => { saving.current = false; setDownloading(false); }); } : undefined, onShare: !review ? () => { void controller.exportFile(async (file) => { await Sharing.shareAsync(file.uri, { mimeType: file.mimeType, dialogTitle: file.fileName }); mediaReady('file-opened', state.file.uri); }).catch(() => setFileError(uiText("Could not share this attachment."))); } : undefined, onFileAction: () => { void (async () => { try {
|
|
343
349
|
if (!await Sharing.isAvailableAsync())
|
|
344
350
|
throw Error('File opening is unavailable.');
|
|
345
351
|
await Sharing.shareAsync(state.file.uri, { mimeType: state.file.mimeType, dialogTitle: state.file.fileName });
|
|
346
352
|
mediaReady('file-opened', state.file.uri);
|
|
347
353
|
}
|
|
348
354
|
catch (e) {
|
|
349
|
-
setFileError(e instanceof Error ? e.message :
|
|
355
|
+
setFileError(e instanceof Error ? e.message : uiText("Could not open the file."));
|
|
350
356
|
} })(); }, children: state.kind === 'video' ? _jsx(VideoPlayer, { file: state.file, audio: audio, design: appearance }, state.file.uri) : state.kind === 'voice' ? _jsx(View, { style: { flex: 1, justifyContent: 'center', alignItems: 'center', padding: 20 }, children: _jsx(VoicePlayer, { file: state.file, audio: audio, design: { ...appearance, colors: { ...appearance.colors, foreground: '#ffffff', accent: '#8ab4f8' } } }) }) : null }) : null;
|
|
351
357
|
// A native modal has its own window; measure its safe area inside that window.
|
|
352
358
|
return _jsx(Modal, { transparent: true, visible: state.phase !== 'idle' && state.phase !== 'choosing', onDismiss: onDismiss, animationType: "fade", onRequestClose: () => { if (expanded)
|
|
353
359
|
setExpanded(false);
|
|
354
360
|
else
|
|
355
|
-
void controller.cancel(); }, children: _jsx(SafeAreaProvider, { style: { flex: 1, backgroundColor: state.kind === 'image' || state.kind === 'video' ? 'rgba(0,0,0,0.94)' : messagingPopupBackdrop }, children: _jsx(SafeAreaView, { edges: expanded ? [] : ['top', 'bottom', 'left', 'right'], style: { flex: 1 }, children: _jsx(View, { style: { flex: 1, margin: state.kind === 'image' || state.kind === 'video' ? 0 : 8, borderRadius: state.kind === 'image' || state.kind === 'video' ? 0 : 18, overflow: 'hidden', backgroundColor: state.kind === 'image' || state.kind === 'video' ? 'transparent' : '#000000' }, children: viewer ?? (_jsx(View, { style: { flex: 1, backgroundColor: c.background }, children: _jsxs(ScrollView, { contentContainerStyle: { padding: 8, paddingTop: 0, gap: 12, flexGrow: 1 }, children: [_jsx(MessagingPopupHeader, { title: state.kind === 'voice' ?
|
|
361
|
+
void controller.cancel(); }, children: _jsx(SafeAreaProvider, { style: { flex: 1, backgroundColor: state.kind === 'image' || state.kind === 'video' ? 'rgba(0,0,0,0.94)' : messagingPopupBackdrop }, children: _jsx(SafeAreaView, { edges: expanded ? [] : ['top', 'bottom', 'left', 'right'], style: { flex: 1 }, children: _jsx(View, { style: { flex: 1, margin: state.kind === 'image' || state.kind === 'video' ? 0 : 8, borderRadius: state.kind === 'image' || state.kind === 'video' ? 0 : 18, overflow: 'hidden', backgroundColor: state.kind === 'image' || state.kind === 'video' ? 'transparent' : '#000000' }, children: viewer ?? (_jsx(View, { style: { flex: 1, backgroundColor: c.background }, children: _jsxs(ScrollView, { contentContainerStyle: { padding: 8, paddingTop: 0, gap: 12, flexGrow: 1 }, children: [_jsx(MessagingPopupHeader, { title: state.kind === 'voice' ? uiText("Voice message") : state.kind === 'image' ? uiText("Photo") : state.kind === 'video' ? uiText("Video") : uiText("File"), foreground: c.foreground, closeLabel: uiText("Close attachment"), onClose: () => void controller.cancel() }), state.phase === 'recording' ? _jsxs(View, { style: { alignItems: 'center', gap: 20, paddingVertical: 32 }, children: [_jsx(MaterialIcons, { name: "mic", size: 48, color: c.accent }), _jsx(Text, { style: { color: c.foreground, fontSize: 32, fontVariant: ['tabular-nums'] }, children: time(state.elapsedMs) }), _jsx(Text, { style: { color: c.secondary }, children: uiText('{time} remaining', { time: time(Math.max(0, state.maxDurationMs - state.elapsedMs)) }) }), _jsx(Control, { label: uiText("Stop recording"), icon: "stop", color: c.accent, onPress: () => void controller.finishRecording() })] }) : null, ['choosing', 'preparing', 'uploading', 'opening'].includes(state.phase) && _jsx(Text, { accessibilityRole: "progressbar", accessibilityValue: { min: 0, max: 100, now: Math.round(state.progress * 100) }, style: { color: c.foreground, fontSize: 18 }, children: state.phase === 'uploading' ? (state.processing ? 'Checking attachment… You can cancel.' : `Uploading… ${Math.round(state.progress * 100)}%`) : state.phase === 'opening' ? (state.progress > 0 ? `Opening… ${Math.round(state.progress * 100)}%` : 'Opening…') : state.phase === 'choosing' ? uiText("Choose an attachment.") : uiText("Preparing recording…") }), state.file && _jsxs(_Fragment, { children: [_jsx(Text, { style: { color: c.foreground, fontSize: 16 }, children: state.file.fileName }), _jsx(Text, { style: { color: c.secondary }, children: size(state.file.byteSize) }), (review || state.phase === 'viewing') && _jsx(FileViewer, { file: state.file, kind: state.kind, audio: audio, design: appearance }, state.file.uri)] }), state.error && _jsx(Text, { accessibilityRole: "alert", style: { color: c.foreground }, children: state.error }), review && (state.phase === 'review' || state.retryUpload) && _jsx(Control, { label: state.phase === 'failed' ? uiText("Retry upload") : uiText("Add attachment"), icon: state.phase === 'failed' ? 'refresh' : 'send', color: c.accent, onPress: () => void controller.send() })] }) })) }) }) }) });
|
|
356
362
|
}
|
|
357
363
|
/** Optional ready-made recording, preview, upload and download tools for both mobile platforms. */
|
|
358
|
-
export function ExpoMessagingScreen(
|
|
364
|
+
export function ExpoMessagingScreen(props) {
|
|
365
|
+
return _jsx(RobonoLocaleProvider, { value: props.interfaceLocale, children: _jsx(ExpoMessagingScreenContent, { ...props }) });
|
|
366
|
+
}
|
|
367
|
+
function ExpoMessagingScreenContent({ storage, audioSession, renderConversation, preloadRecentMedia = true, ...props }) {
|
|
359
368
|
useEffect(() => { void props.client.reportUsage({ platform: Platform.OS === 'ios' ? 'ios' : Platform.OS === 'android' ? 'android' : 'other', components: ['media-tools'] }).catch(() => { }); }, [props.client]);
|
|
360
369
|
const [requestedMessageAction, setRequestedMessageAction] = useState();
|
|
361
370
|
const afterViewer = useRef(undefined);
|
package/dist/expo-social.js
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useRobonoLocale } from './localization.js';
|
|
2
3
|
import { Linking, Text, UIManager } from 'react-native';
|
|
3
4
|
// Expo builds include this native module when the documented peer is installed.
|
|
4
5
|
const available = !!UIManager.getViewManagerConfig('RNCWebView');
|
|
5
6
|
const WebView = available ? require('react-native-webview').WebView : null;
|
|
6
7
|
export function ExpoSocialPlayer({ html, origin, onError }) {
|
|
8
|
+
const { text: uiText, locale: uiLocale, number: uiNumber, count: uiCount } = useRobonoLocale();
|
|
7
9
|
if (!WebView)
|
|
8
|
-
return _jsx(Text, { children: "Update the app to play this preview." });
|
|
10
|
+
return _jsx(Text, { children: uiText("Update the app to play this preview.") });
|
|
9
11
|
const open = (url) => { try {
|
|
10
12
|
const u = new URL(url);
|
|
11
13
|
if (u.protocol === 'https:' && !u.username && !u.password)
|
package/dist/expo.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export * from './expo-screen.js';
|
|
2
|
-
export { ExpoMessenger, createExpoMessenger, type ExpoMessengerProps } from './expo-messenger.js';
|
|
2
|
+
export { ExpoMessenger, createExpoMessenger, createHostedExpoMessenger, type ExpoMessengerProps } from './expo-messenger.js';
|
package/dist/expo.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export * from './expo-screen.js';
|
|
2
|
-
export { ExpoMessenger, createExpoMessenger } from './expo-messenger.js';
|
|
2
|
+
export { ExpoMessenger, createExpoMessenger, createHostedExpoMessenger } from './expo-messenger.js';
|
package/dist/extras.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
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 { forwardRef, useEffect, useImperativeHandle, useRef, useState } from 'react';
|
|
3
4
|
import { ActivityIndicator, Image, Linking, Platform, Pressable, ScrollView, Text, View } from 'react-native';
|
|
4
|
-
import { currentTrackers, trackerFresh,
|
|
5
|
+
import { currentTrackers, trackerFresh, weatherTemperature } from '@robono/messaging';
|
|
5
6
|
import { MessagingPopup, messagingPopupColors } from './popup.js';
|
|
6
7
|
import { MessageIcon } from './icons.js';
|
|
7
8
|
import WeatherArtwork from './visual/WeatherArtwork.js';
|
|
@@ -26,6 +27,7 @@ else
|
|
|
26
27
|
await Linking.openURL(`https://www.google.com/maps/search/?api=1&query=${query}`); }
|
|
27
28
|
export function ExtraPopup({ title, onClose, background, foreground, children, expanded = true }) { return _jsx(MessagingPopup, { expanded: expanded, title: title, foreground: foreground, background: background, onClose: onClose, children: children }); }
|
|
28
29
|
export const MessageExtra = forwardRef(function MessageExtra({ addressBubbleHeight = 44, extra, messageId, userId, extras, ink, accent, background, foreground, footer, renderAddress, Map, onCall, onLongPress }, ref) {
|
|
30
|
+
const { text: uiText, locale: uiLocale, number: uiNumber, count: uiCount } = useRobonoLocale();
|
|
29
31
|
const [mapHeight, setMapHeight] = useState(230), [pendingShare, setPendingShare] = useState(false);
|
|
30
32
|
const resolution = useRef(null), shareIntent = useRef(false);
|
|
31
33
|
const [open, setOpen] = useState(false), [fahrenheit, setFahrenheit] = useState(extra.weather?.fahrenheit ?? false), [imperial, setImperial] = useState(false), [now, setNow] = useState(Date.now()), [error, setError] = useState(''), [busy, setBusy] = useState(false), [selected, setSelected] = useState();
|
|
@@ -39,7 +41,7 @@ export const MessageExtra = forwardRef(function MessageExtra({ addressBubbleHeig
|
|
|
39
41
|
catch (e) {
|
|
40
42
|
shareIntent.current = false;
|
|
41
43
|
setPendingShare(false);
|
|
42
|
-
setError(e instanceof Error ? e.message :
|
|
44
|
+
setError(e instanceof Error ? e.message : uiText("Please try again."));
|
|
43
45
|
}
|
|
44
46
|
finally {
|
|
45
47
|
setBusy(false);
|
|
@@ -60,10 +62,11 @@ export const MessageExtra = forwardRef(function MessageExtra({ addressBubbleHeig
|
|
|
60
62
|
}
|
|
61
63
|
} await extras.startTracking(messageId); setPendingShare(false); };
|
|
62
64
|
const avatarSize = Math.max(8, Math.min(32, addressBubbleHeight - 34));
|
|
63
|
-
const addressAction = _jsxs(View, { testID: "address-tracking", style: { flexShrink: 0, maxWidth: 136, height: trackers.length ? addressBubbleHeight : 32, justifyContent: 'center', alignItems: 'center', gap: 2, overflow: 'hidden' }, children: [_jsxs(Pressable, { accessibilityRole: "button", accessibilityLabel: mine ?
|
|
64
|
-
const
|
|
65
|
-
|
|
65
|
+
const addressAction = _jsxs(View, { testID: "address-tracking", style: { flexShrink: 0, maxWidth: 136, height: trackers.length ? addressBubbleHeight : 32, justifyContent: 'center', alignItems: 'center', gap: 2, overflow: 'hidden' }, children: [_jsxs(Pressable, { accessibilityRole: "button", accessibilityLabel: mine ? uiText("End tracking") : uiText("Allow tracking"), accessibilityHint: uiText("Share your live location with this chat for 15 minutes"), disabled: busy || !mine && !extras.getSnapshot().available.tracking, hitSlop: { top: 6, bottom: 6, left: 0, right: 0 }, onPress: event => { event.stopPropagation(); void act(share); }, accessibilityState: { busy, disabled: busy || !mine && !extras.getSnapshot().available.tracking }, style: ({ pressed }) => ({ minHeight: 32, paddingHorizontal: 10, paddingVertical: 5, borderRadius: 16, borderWidth: 1, borderColor: accent + (mine ? 'b0' : '70'), backgroundColor: accent + (pressed ? '30' : mine ? '20' : '0d'), flexDirection: 'row', alignItems: 'center', justifyContent: 'center', gap: 6, opacity: (busy || !mine && !extras.getSnapshot().available.tracking) ? 0.45 : 1 }), children: [busy ? _jsx(ActivityIndicator, { size: 16, color: accent }) : _jsx(MessageIcon, { name: mine ? 'stop' : 'location-outline', size: 16, color: accent }), _jsx(Text, { numberOfLines: 1, style: { color: foreground, fontSize: 12, lineHeight: 18, fontWeight: '600' }, children: mine ? uiText("End tracking") : uiText("Allow tracking") })] }), trackers.length > 0 && _jsx(View, { testID: "tracking-map-avatars", style: { flexDirection: 'row', flexWrap: 'nowrap', justifyContent: 'center', gap: 2 }, children: trackers.slice(0, 4).map(t => _jsx(Pressable, { accessibilityRole: "button", accessibilityLabel: t.userId === userId ? uiText('Show your location on map') : uiText('Show {name} on map', { name: t.name }), onPress: event => { event.stopPropagation(); setSelected(t.userId); setOpen(true); }, style: ({ pressed }) => ({ width: avatarSize, height: avatarSize, borderRadius: avatarSize / 2, alignItems: 'center', justifyContent: 'center', backgroundColor: accent + (pressed ? '30' : '12'), borderWidth: 1, borderColor: accent + '70' }), children: t.avatarUrl ? _jsx(Image, { source: { uri: t.avatarUrl }, style: { width: avatarSize - 4, height: avatarSize - 4, borderRadius: avatarSize / 2 } }) : _jsx(Text, { style: { color: foreground, fontSize: Math.min(18, avatarSize * .55), fontWeight: '600' }, children: t.name.slice(0, 1).toUpperCase() }) }, t.sessionId)) })] });
|
|
66
|
+
const trackingETA = (t) => trackerFresh(t, now) && t.etaSeconds !== null && Number.isFinite(t.etaSeconds) ? uiText('{count} min', { count: uiNumber(Math.max(1, Math.ceil(t.etaSeconds / 60))) }) : uiText('Updating…');
|
|
67
|
+
const addressParticipants = trackers.length > 0 && _jsx(View, { testID: "address-participants", pointerEvents: "none", style: { alignSelf: 'stretch' }, children: _jsx(Text, { style: { color: ink, fontSize: 13, lineHeight: 18 }, children: trackers.map(t => `${t.userId === userId ? uiText('You') : t.name} · ${trackingETA(t)}`).join(' · ') }) });
|
|
68
|
+
return _jsxs(_Fragment, { children: [weather && _jsxs(Pressable, { testID: "weather-card", accessibilityRole: "button", accessibilityLabel: uiText("Open weather"), onLongPress: onLongPress, onPress: () => setOpen(true), style: { width: 268, maxWidth: '100%', paddingHorizontal: 4, paddingVertical: 2 }, children: [_jsxs(View, { style: row, children: [_jsx(ForecastIcon, { period: weather.current, size: 34 }), _jsxs(View, { style: { flex: 1 }, children: [_jsxs(Text, { style: title, children: [weatherTemperature(weather.current.c, weather.fahrenheit ?? false), " \u00B7 ", weather.current.condition] }), _jsx(Text, { style: { color: ink, fontSize: 12, opacity: .75 }, children: weather.place })] })] }), _jsxs(View, { style: { ...row, justifyContent: 'space-between', marginTop: 2 }, children: [_jsx(Text, { style: { color: ink, fontSize: 11 }, children: weather.current.detail }), footer] })] }), address && (renderAddress ? renderAddress(addressAction, addressParticipants) : _jsxs(_Fragment, { children: [addressAction, addressParticipants] })), call && _jsxs(Pressable, { testID: "call-card", accessibilityRole: "button", accessibilityLabel: uiText("Open call details"), onLongPress: onLongPress, onPress: () => setOpen(true), style: { ...row, width: 268, maxWidth: '100%', padding: 4 }, children: [_jsx(MessageIcon, { name: call.kind === 'video' ? 'video' : 'call', color: call.outcome === 'missed' ? '#c94040' : accent, size: 26 }), _jsxs(View, { style: { flex: 1 }, children: [_jsx(Text, { style: title, children: call.outcome === 'missed' ? (call.kind === 'video' ? uiText('Missed video call') : uiText('Missed voice call')) : call.direction === 'incoming' ? (call.kind === 'video' ? uiText('Incoming video call') : uiText('Incoming voice call')) : (call.kind === 'video' ? uiText('Outgoing video call') : uiText('Outgoing voice call')) }), _jsx(Text, { style: { color: ink, fontSize: 12 }, children: call.outcome === 'completed' ? `${Math.floor(call.durationSeconds / 60)}:${String(Math.floor(call.durationSeconds % 60)).padStart(2, '0')}` : call.outcome })] }), footer] }), open && _jsxs(ExtraPopup, { expanded: !!address || !!weather, title: weather ? uiText("Weather") : address ? uiText("Live tracking") : uiText("Call details"), onClose: () => { shareIntent.current = false; setPendingShare(false); resolution.current = null; setError(''); setOpen(false); }, background: background, foreground: foreground, children: [!!error && _jsxs(View, { style: { paddingVertical: 8 }, children: [_jsx(Text, { accessibilityRole: "alert", style: text, children: error }), address && !address.destination && _jsx(Pressable, { accessibilityRole: "button", disabled: busy, onPress: () => void act(async () => { await extras.resolveAddress(messageId); }), style: { minHeight: 44, justifyContent: 'center' }, children: _jsx(Text, { style: { color: accent }, children: uiText("Try again") }) })] }), address && _jsxs(View, { style: { flex: 1, minHeight: 0 }, children: [!address.destination && !!address.candidates?.length && _jsxs(View, { style: { gap: 8, paddingVertical: 12 }, children: [_jsx(Text, { style: { ...text, fontSize: 17, fontWeight: '600' }, children: uiText("Choose the destination") }), address.candidates.map((place, index) => _jsx(Pressable, { accessibilityRole: "button", disabled: busy, onPress: () => void act(async () => { await extras.resolveAddress(messageId, index); if (pendingShare && shareIntent.current) {
|
|
66
69
|
await extras.startTracking(messageId);
|
|
67
70
|
setPendingShare(false);
|
|
68
|
-
} }), style: { minHeight: 48, padding: 12, borderRadius: 10, borderWidth: 1, borderColor: accent }, children: _jsx(Text, { style: { ...text, color: accent }, children: place.address }) }, index))] }), _jsx(Pressable, { accessibilityRole: "link", accessibilityLabel: "Open destination in maps", onPress: () => void act(() => openChatAddress(address.text)), children: _jsx(Text, { style: { ...text, color: accent, marginBottom: 10 }, numberOfLines: 2, children: address.text }) }), _jsx(View, { testID: "tracking-map-area", onLayout: event => setMapHeight(Math.max(1, Math.round(event.nativeEvent.layout.height))), style: { flex: 1, minHeight: 120, overflow: 'hidden' }, children: address.destination && Map ? _jsx(Map, { imperial: imperial, now: now, height: mapHeight, destination: address.destination, trackers: trackers.filter(t => trackerFresh(t, now)), accent: accent, selectedUserId: selected, onSelect: setSelected }) : _jsx(Text, { style: small, children: busy ?
|
|
71
|
+
} }), style: { minHeight: 48, padding: 12, borderRadius: 10, borderWidth: 1, borderColor: accent }, children: _jsx(Text, { style: { ...text, color: accent }, children: place.address }) }, index))] }), _jsx(Pressable, { accessibilityRole: "link", accessibilityLabel: uiText("Open destination in maps"), onPress: () => void act(() => openChatAddress(address.text)), children: _jsx(Text, { style: { ...text, color: accent, marginBottom: 10 }, numberOfLines: 2, children: address.text }) }), _jsx(View, { testID: "tracking-map-area", onLayout: event => setMapHeight(Math.max(1, Math.round(event.nativeEvent.layout.height))), style: { flex: 1, minHeight: 120, overflow: 'hidden' }, children: address.destination && Map ? _jsx(Map, { imperial: imperial, now: now, height: mapHeight, destination: address.destination, trackers: trackers.filter(t => trackerFresh(t, now)), accent: accent, selectedUserId: selected, onSelect: setSelected }) : _jsx(Text, { style: small, children: busy ? uiText("Finding the destination…") : address.candidates?.length ? uiText("Choose the matching destination above.") : uiText("The map is unavailable. Check Maps setup or try again.") }) }), _jsxs(View, { style: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between' }, children: [_jsx(Text, { style: { color: messagingPopupColors.foreground, fontSize: 14 }, children: uiText("Sharing ends after 15 minutes.") }), _jsx(Pressable, { accessibilityRole: "button", accessibilityLabel: uiText("Change speed and distance units"), onPress: () => setImperial(v => !v), style: { minHeight: 44, paddingHorizontal: 4, justifyContent: 'center' }, children: _jsx(Text, { style: { color: accent, fontSize: 17, fontWeight: '600' }, children: imperial ? uiText("km/h · km") : uiText("mph · mi") }) })] }), _jsxs(ScrollView, { style: { flexGrow: 0, maxHeight: 156 }, children: [!trackers.length && _jsx(Text, { style: text, children: uiText("No one is sharing their location.") }), trackers.map(t => _jsxs(Pressable, { onPress: () => setSelected(t.userId), accessibilityRole: "button", accessibilityLabel: uiText('Locate {name}', { name: t.name }), style: { minHeight: 74, paddingVertical: 8, borderBottomWidth: 1, borderColor: messagingPopupColors.foreground + '18' }, children: [_jsxs(View, { style: { ...row, justifyContent: 'space-between' }, children: [_jsx(Text, { style: { color: messagingPopupColors.foreground, fontSize: 21, lineHeight: 27, fontWeight: '600', flexShrink: 1 }, children: t.userId === userId ? uiText("You") : t.name }), _jsx(Text, { style: { color: accent, fontSize: 25, lineHeight: 30, fontWeight: '700' }, children: trackingETA(t) })] }), _jsxs(View, { style: { ...row, justifyContent: 'space-between', alignItems: 'flex-start' }, children: [_jsx(Text, { style: { color: messagingPopupColors.foreground, fontSize: 19, lineHeight: 26, flex: 1 }, children: trackerFresh(t, now) ? t.speedMps === null ? uiText("Speed unavailable") : Math.round(t.speedMps * (imperial ? 2.23694 : 3.6)) + (imperial ? ' mph' : ' km/h') : uiText("Waiting for a fresh location") }), trackerFresh(t, now) && _jsx(Text, { testID: "tracking-distance", style: { color: messagingPopupColors.foreground, fontSize: 19, lineHeight: 26, textAlign: 'right', flexShrink: 1 }, children: t.distanceMeters === null ? uiText("Distance unavailable") : uiText('{distance} {unit} away', { distance: uiNumber(t.distanceMeters / (imperial ? 1609.344 : 1000), { minimumFractionDigits: 1, maximumFractionDigits: 1 }), unit: imperial ? uiText('mi') : uiText('km') }) })] })] }, t.sessionId))] })] }), !address && _jsxs(ScrollView, { style: weather ? { flex: 1, minHeight: 0 } : { flexShrink: 1 }, children: [weather && (weather.expiresAt <= now ? _jsx(Text, { style: text, children: uiText("Weather details expired.") }) : _jsxs(_Fragment, { children: [_jsx(Text, { style: small, children: weather.place }), _jsxs(View, { style: { ...row, marginVertical: 8 }, children: [_jsx(ForecastIcon, { period: weather.current, size: 36 }), _jsxs(Text, { style: { ...text, flex: 1, fontWeight: '600' }, children: [weatherTemperature(weather.current.c, fahrenheit), " \u00B7 ", weather.current.condition] }), _jsx(Pressable, { accessibilityRole: "button", accessibilityLabel: uiText("Change temperature unit"), onPress: () => setFahrenheit(v => !v), style: { minWidth: 44, minHeight: 44, alignItems: 'center', justifyContent: 'center' }, children: _jsx(Text, { style: { color: accent, fontSize: 16, fontWeight: '600' }, children: fahrenheit ? uiText("°C") : uiText("°F") }) })] }), weather.alert && _jsx(Text, { style: { ...text, color: accent }, children: weather.alert }), _jsxs(View, { style: { marginTop: 20, paddingTop: 16, borderTopWidth: 1, borderTopColor: messagingPopupColors.foreground + '24' }, children: [_jsx(Text, { style: { ...text, fontWeight: '600' }, children: uiText("Next three hours") }), _jsx(View, { style: { ...row, marginTop: 12, alignItems: 'flex-start' }, children: weather.hours.slice(0, 3).map(p => _jsxs(View, { style: { flex: 1, gap: 4 }, children: [_jsx(Text, { style: small, children: p.label }), _jsxs(View, { style: row, children: [_jsx(ForecastIcon, { period: p, size: 32 }), _jsx(Text, { style: text, children: weatherTemperature(p.c, fahrenheit) })] }), _jsx(Text, { style: small, children: p.detail })] }, p.at)) })] }), _jsxs(View, { style: { marginTop: 20, paddingTop: 16, borderTopWidth: 1, borderTopColor: messagingPopupColors.foreground + '24' }, children: [_jsx(Text, { style: { ...text, fontWeight: '600', marginBottom: 8 }, children: uiText("Three-day forecast") }), weather.days.slice(0, 3).map(p => _jsxs(View, { style: { ...row, paddingVertical: 8 }, children: [_jsx(Text, { style: { ...text, width: 45 }, children: p.label }), _jsx(ForecastIcon, { period: p, size: 32 }), _jsxs(View, { style: { flex: 1 }, children: [_jsx(Text, { style: text, children: p.condition }), _jsx(Text, { style: small, children: p.detail })] }), _jsxs(Text, { style: text, children: [weatherTemperature(p.c, fahrenheit), p.lowC !== undefined ? ' / ' + weatherTemperature(p.lowC, fahrenheit) : ''] })] }, p.at))] }), !!weather.attribution && _jsx(Text, { style: { ...small, marginTop: 8 }, children: weather.attribution })] })), call && _jsxs(_Fragment, { children: [_jsxs(Text, { style: { ...text, fontWeight: '600', marginVertical: 8 }, children: [call.kind === 'video' ? uiText('Video call') : uiText('Voice call'), " \u00B7 ", uiText(call.outcome)] }), _jsx(Text, { style: text, children: new Date(call.startedAt).toLocaleString(uiLocale) }), call.durationSeconds > 0 && _jsx(Text, { style: { ...text, marginTop: 8 }, children: uiText('{minutes} min {seconds} sec', { minutes: Math.floor(call.durationSeconds / 60), seconds: Math.floor(call.durationSeconds % 60) }) }), call.endedByWarning && _jsx(Text, { style: { ...small, marginTop: 10 }, children: uiText("Ended after the network’s time warning.") }), onCall && _jsx(Pressable, { accessibilityRole: "button", accessibilityLabel: uiText("Call again"), onPress: () => { setOpen(false); onCall(call.kind); }, style: { minHeight: 44, justifyContent: 'center', marginTop: 10 }, children: _jsx(Text, { style: { color: accent, fontWeight: '600' }, children: uiText("Call again") }) })] })] })] }), !!error && !open && _jsx(ExtraPopup, { title: uiText("Message"), onClose: () => setError(''), background: background, foreground: foreground, children: _jsx(Text, { accessibilityRole: "alert", style: text, children: error }) })] });
|
|
69
72
|
});
|
package/dist/forward.js
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } 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 { ActivityIndicator, Pressable, ScrollView, Text, View } from 'react-native';
|
|
5
6
|
import { MessagingPopup } from './popup.js';
|
|
6
7
|
/** Messenger's compact destination picker, retaining per-message retry identities. */
|
|
7
8
|
export function MessagingForward({ messages, client, appearance, participants = {}, userId, onClose }) {
|
|
9
|
+
const { text: uiText, locale: uiLocale, number: uiNumber, count: uiCount } = useRobonoLocale();
|
|
8
10
|
const [page, setPage] = useState(null), [loading, setLoading] = useState(false), [busy, setBusy] = useState(false), [error, setError] = useState('');
|
|
9
11
|
const live = useRef(true), inFlight = useRef(false), attempts = useRef(new Map()), done = useRef(new Set());
|
|
10
12
|
const c = { ...appearance.colors, ...messagingPopupColors };
|
|
@@ -17,7 +19,7 @@ export function MessagingForward({ messages, client, appearance, participants =
|
|
|
17
19
|
}
|
|
18
20
|
catch (e) {
|
|
19
21
|
if (live.current)
|
|
20
|
-
setError(e instanceof Error ? e.message :
|
|
22
|
+
setError(e instanceof Error ? e.message : uiText("Chats could not be loaded."));
|
|
21
23
|
}
|
|
22
24
|
finally {
|
|
23
25
|
inFlight.current = false;
|
|
@@ -49,7 +51,7 @@ export function MessagingForward({ messages, client, appearance, participants =
|
|
|
49
51
|
}
|
|
50
52
|
catch (e) {
|
|
51
53
|
if (live.current)
|
|
52
|
-
setError(e instanceof Error ? e.message :
|
|
54
|
+
setError(e instanceof Error ? e.message : uiText("The message could not be forwarded. Try again."));
|
|
53
55
|
}
|
|
54
56
|
finally {
|
|
55
57
|
inFlight.current = false;
|
|
@@ -57,6 +59,6 @@ export function MessagingForward({ messages, client, appearance, participants =
|
|
|
57
59
|
setBusy(false);
|
|
58
60
|
}
|
|
59
61
|
}
|
|
60
|
-
return _jsxs(MessagingPopup, { contentWidth: 640, title: "Forward to", foreground: c.foreground, closeLabel: "Close forwarding", closeDisabled: busy, background: c.background, onClose: () => { if (!busy)
|
|
61
|
-
onClose(); }, children: [_jsx(ScrollView, { style: { flex: 1, minHeight: 0, marginTop: 8 }, children: page?.conversations.filter(row => row.canSend).map(row => { const name = row.kind === 'group' ? row.name ?? 'Group' : row.contactId === userId ? 'You' : participants[row.contactId ?? '']?.name ?? row.contactId ?? 'Chat'; return _jsx(Pressable, { accessibilityRole: "button", accessibilityLabel: 'Forward to '
|
|
62
|
+
return _jsxs(MessagingPopup, { contentWidth: 640, title: uiText("Forward to"), foreground: c.foreground, closeLabel: uiText("Close forwarding"), closeDisabled: busy, background: c.background, onClose: () => { if (!busy)
|
|
63
|
+
onClose(); }, children: [_jsx(ScrollView, { style: { flex: 1, minHeight: 0, marginTop: 8 }, children: page?.conversations.filter(row => row.canSend).map(row => { const name = row.kind === 'group' ? row.name ?? uiText('Group') : row.contactId === userId ? uiText('You') : participants[row.contactId ?? '']?.name ?? row.contactId ?? uiText('Chat'); return _jsx(Pressable, { accessibilityRole: "button", accessibilityLabel: uiText('Forward to {name}', { name }), disabled: busy || loading, onPress: () => void send(row), style: { minHeight: 52, justifyContent: 'center', paddingVertical: 12, paddingHorizontal: 8, borderBottomWidth: 1, borderBottomColor: '#ffffff14' }, children: _jsx(Text, { style: { color: c.foreground, fontSize: 16 }, children: name }) }, row.id); }) }), loading || busy ? _jsx(ActivityIndicator, { color: c.accent, style: { margin: 12 } }) : page && !page.conversations.some(row => row.canSend) && _jsx(Text, { style: { color: c.secondary, marginVertical: 12 }, children: uiText("No chats available.") }), !!error && _jsx(Text, { accessibilityRole: "alert", style: { color: c.foreground, marginTop: 8 }, children: error }), _jsxs(View, { style: { flexDirection: 'row', justifyContent: 'flex-end', gap: 20, marginTop: 8 }, children: [!page && error && _jsx(Pressable, { accessibilityRole: "button", onPress: () => void load(false), style: { minHeight: 44, justifyContent: 'center' }, children: _jsx(Text, { style: { color: c.accent }, children: uiText("Try again") }) }), page?.hasMore && _jsx(Pressable, { accessibilityRole: "button", disabled: loading || busy, onPress: () => void load(true), style: { minHeight: 44, justifyContent: 'center' }, children: _jsx(Text, { style: { color: c.accent }, children: uiText("More chats") }) })] })] });
|
|
62
64
|
}
|
package/dist/group-editor.js
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useRobonoLocale } from './localization.js';
|
|
2
3
|
import { useEffect, useMemo, useRef, useState } from 'react';
|
|
3
4
|
import { ActivityIndicator, FlatList, Image, Pressable, Text, TextInput, View } from 'react-native';
|
|
4
5
|
import { MessageIcon } from './icons.js';
|
|
5
6
|
import { MessagingPopup, messagingPopupColors as c } from './popup.js';
|
|
6
7
|
/** Messenger's name, group type and contact checklist; changes use member deltas. */
|
|
7
8
|
export function MessagingGroupEditor({ client, inbox, userId, contacts, conversation, groups, accent, onClose, onSaved }) {
|
|
9
|
+
const { text: uiText, locale: uiLocale, number: uiNumber, count: uiCount } = useRobonoLocale();
|
|
8
10
|
const editing = !!conversation, conversationId = client.getSnapshot().conversationId;
|
|
9
11
|
const [name, setName] = useState(conversation?.name ?? ''), [supergroup, setSupergroup] = useState(conversation?.supergroup ?? false);
|
|
10
12
|
const [revision, setRevision] = useState(conversation?.revision ?? 0), [count, setCount] = useState(conversation?.memberCount ?? 0);
|
|
@@ -34,7 +36,7 @@ export function MessagingGroupEditor({ client, inbox, userId, contacts, conversa
|
|
|
34
36
|
catch (e) {
|
|
35
37
|
if (version === generation.current) {
|
|
36
38
|
setReady(false);
|
|
37
|
-
setError(e instanceof Error ? e.message :
|
|
39
|
+
setError(e instanceof Error ? e.message : uiText("Group members could not be loaded."));
|
|
38
40
|
}
|
|
39
41
|
}
|
|
40
42
|
finally {
|
|
@@ -49,7 +51,7 @@ export function MessagingGroupEditor({ client, inbox, userId, contacts, conversa
|
|
|
49
51
|
const chosen = (id) => !removed.has(id) && (original.has(id) || added.has(id));
|
|
50
52
|
const toggle = (id) => {
|
|
51
53
|
if (!chosen(id) && membersCount >= max) {
|
|
52
|
-
setError(
|
|
54
|
+
setError(uiText("This group can have up to {value1} members.", { value1: max.toLocaleString(uiLocale) }));
|
|
53
55
|
return;
|
|
54
56
|
}
|
|
55
57
|
if (original.has(id))
|
|
@@ -86,13 +88,13 @@ export function MessagingGroupEditor({ client, inbox, userId, contacts, conversa
|
|
|
86
88
|
}
|
|
87
89
|
}
|
|
88
90
|
catch (e) {
|
|
89
|
-
setError(e instanceof Error ? e.message :
|
|
91
|
+
setError(e instanceof Error ? e.message : uiText("The group could not be saved."));
|
|
90
92
|
}
|
|
91
93
|
finally {
|
|
92
94
|
setBusy(false);
|
|
93
95
|
}
|
|
94
96
|
}
|
|
95
97
|
const locked = busy || loading || !ready, canSave = !locked && !!name.trim() && membersCount >= 2 && membersCount <= max;
|
|
96
|
-
return _jsxs(MessagingPopup, { title: editing ?
|
|
97
|
-
onClose(); }, form: true, children: [_jsx(FlatList, { data: people, keyExtractor: id => id, keyboardShouldPersistTaps: "handled", style: { flex: 1 }, contentContainerStyle: { paddingHorizontal: 8, paddingBottom: 16 }, initialNumToRender: 15, ListHeaderComponent: _jsxs(View, { style: { gap: 12, paddingTop: 12 }, children: [_jsx(TextInput, { accessibilityLabel: "Group name", placeholder: "Group name", placeholderTextColor: c.secondary, maxLength: 100, value: name, onChangeText: setName, editable: !locked, style: { color: c.foreground, fontSize: 21, minHeight: 48, paddingHorizontal: 4, borderBottomWidth: 1, borderBottomColor: '#ffffff30' } }), groups?.supergroupsEnabled && _jsx(View, { style: { flexDirection: 'row', gap: 20 }, children: [false, true].map(value => _jsxs(Pressable, { accessibilityRole: "radio", accessibilityLabel: value ?
|
|
98
|
+
return _jsxs(MessagingPopup, { title: editing ? uiText("Edit group") : uiText("New group"), background: c.background, closeDisabled: busy, onClose: () => { if (!busy)
|
|
99
|
+
onClose(); }, form: true, children: [_jsx(FlatList, { data: people, keyExtractor: id => id, keyboardShouldPersistTaps: "handled", style: { flex: 1 }, contentContainerStyle: { paddingHorizontal: 8, paddingBottom: 16 }, initialNumToRender: 15, ListHeaderComponent: _jsxs(View, { style: { gap: 12, paddingTop: 12 }, children: [_jsx(TextInput, { accessibilityLabel: uiText("Group name"), placeholder: uiText("Group name"), placeholderTextColor: c.secondary, maxLength: 100, value: name, onChangeText: setName, editable: !locked, style: { color: c.foreground, fontSize: 21, minHeight: 48, paddingHorizontal: 4, borderBottomWidth: 1, borderBottomColor: '#ffffff30' } }), groups?.supergroupsEnabled && _jsx(View, { style: { flexDirection: 'row', gap: 20 }, children: [false, true].map(value => _jsxs(Pressable, { accessibilityRole: "radio", accessibilityLabel: value ? uiText("Supergroup") : uiText("Standard group"), accessibilityState: { checked: supergroup === value, disabled: locked }, disabled: locked, onPress: () => setSupergroup(value), style: { minHeight: 44, flexDirection: 'row', alignItems: 'center', gap: 8 }, children: [_jsx(MessageIcon, { name: supergroup === value ? 'radio-button-on' : 'radio-button-off', color: accent, size: 22 }), _jsx(Text, { style: { color: c.foreground, fontSize: 16 }, children: value ? uiText("Supergroup") : uiText("Group") })] }, String(value))) }), _jsx(Text, { style: { color: c.secondary, fontSize: 14 }, children: uiText('{count} / {limit} members', { count: membersCount, limit: max }) }), _jsx(Text, { style: { color: c.foreground, fontSize: 17 }, children: uiText("You · Group owner") }), _jsx(TextInput, { accessibilityLabel: uiText("Search group contacts"), placeholder: uiText("Search contacts"), placeholderTextColor: c.secondary, value: query, onChangeText: setQuery, style: { color: c.foreground, fontSize: 16, minHeight: 44, borderBottomWidth: 1, borderBottomColor: '#ffffff30' } })] }), renderItem: ({ item: id }) => _jsxs(Pressable, { accessibilityRole: "checkbox", accessibilityLabel: contacts[id]?.name ?? uiText("Member"), accessibilityState: { checked: chosen(id), disabled: locked }, disabled: locked, onPress: () => toggle(id), style: { minHeight: 56, flexDirection: 'row', alignItems: 'center', gap: 10 }, children: [contacts[id]?.avatarUrl ? _jsx(Image, { source: { uri: contacts[id].avatarUrl }, style: { width: 36, height: 36, borderRadius: 18 } }) : _jsx(View, { style: { width: 36, height: 36, borderRadius: 18, backgroundColor: c.incoming, alignItems: 'center', justifyContent: 'center' }, children: _jsx(Text, { style: { color: accent, fontSize: 18 }, children: (contacts[id]?.name ?? 'Member').slice(0, 1) }) }), _jsx(Text, { style: { flex: 1, color: c.foreground, fontSize: 16 }, children: contacts[id]?.name ?? uiText("Member") }), _jsx(MessageIcon, { name: chosen(id) ? 'checkmark-circle' : 'ellipse-outline', color: chosen(id) ? accent : c.secondary, size: 24 })] }), ListFooterComponent: _jsxs(View, { style: { gap: 12, paddingTop: 12 }, children: [loading && _jsx(ActivityIndicator, { accessibilityLabel: uiText("Loading group members"), color: accent }), next && _jsx(Pressable, { accessibilityRole: "button", disabled: loading || busy, onPress: () => void load(true), style: { minHeight: 44, justifyContent: 'center' }, children: _jsx(Text, { style: { color: accent }, children: uiText("Load more members") }) }), !people.length && !loading && _jsx(Text, { style: { color: c.secondary }, children: query ? uiText("No matching contacts.") : uiText("Your app has not supplied any contacts to invite.") }), _jsxs(Text, { style: { color: c.secondary, fontSize: 13, lineHeight: 19 }, children: [uiText("Only the group owner can change the name or members."), groups?.historyForNewMembers === 'all' ? uiText("New members can see retained group history.") : uiText("New members see messages sent after they join.")] }), !!error && _jsx(Text, { accessibilityRole: "alert", style: { color: c.foreground, fontSize: 15 }, children: error })] }) }), _jsx(Pressable, { accessibilityRole: "button", accessibilityLabel: editing ? uiText("Save group") : uiText("Create group"), disabled: !canSave, onPress: () => void save(), style: { minHeight: 48, margin: 8, borderRadius: 12, backgroundColor: accent, alignItems: 'center', justifyContent: 'center', opacity: canSave ? 1 : .4 }, children: busy ? _jsx(ActivityIndicator, { color: "#000" }) : _jsx(Text, { style: { color: '#000', fontSize: 17, fontWeight: '600' }, children: editing ? uiText("Save") : uiText("Create group") }) })] });
|
|
98
100
|
}
|
package/dist/header-calls.js
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
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 { useEffect, useRef, useState } from 'react';
|
|
3
4
|
import { Keyboard, Linking, Platform, Pressable, Text, View } from 'react-native';
|
|
4
5
|
import { MessageIcon } from './icons.js';
|
|
5
6
|
import { MessagingPopup } from './popup.js';
|
|
6
7
|
/** Matches Messenger: video is separate; the phone button chooses internet/carrier. */
|
|
7
8
|
export function MessagingHeaderCalls({ actions, name, group, color }) {
|
|
9
|
+
const { text: uiText, locale: uiLocale, number: uiNumber, count: uiCount } = useRobonoLocale();
|
|
8
10
|
const [visible, setVisible] = useState(false), [error, setError] = useState('');
|
|
9
11
|
const pending = useRef(null), latest = useRef(actions);
|
|
10
12
|
latest.current = actions;
|
|
@@ -24,7 +26,7 @@ export function MessagingHeaderCalls({ actions, name, group, color }) {
|
|
|
24
26
|
await current?.[kind]?.();
|
|
25
27
|
}
|
|
26
28
|
catch {
|
|
27
|
-
setError(
|
|
29
|
+
setError(uiText("The call could not start. Please try again."));
|
|
28
30
|
} };
|
|
29
31
|
const dismissed = () => { const kind = pending.current; pending.current = null; if (kind)
|
|
30
32
|
requestAnimationFrame(() => void invoke(kind)); };
|
|
@@ -37,10 +39,10 @@ export function MessagingHeaderCalls({ actions, name, group, color }) {
|
|
|
37
39
|
} }; document.addEventListener('keydown', close, true); return () => document.removeEventListener('keydown', close, true); }, [visible]);
|
|
38
40
|
useEffect(() => { pending.current = null; setVisible(false); setError(''); }, [name, group]);
|
|
39
41
|
const choose = (kind) => { pending.current = kind; setVisible(false); };
|
|
40
|
-
const button = (label, icon, press) => _jsx(Pressable, { accessibilityRole: "button", accessibilityLabel: label, onPress: press, style: { width: icon === 'video' ? 42 : 44, height: icon === 'video' ? 42 : 44, alignItems: 'center', justifyContent: 'center' }, children: _jsx(MessageIcon, { name: icon, size: 23, color: color }) });
|
|
41
|
-
const row = (kind, label, detail) => _jsxs(Pressable, { accessibilityRole: "button", accessibilityLabel: label, onPress: () => choose(kind), style: ({ pressed }) => ({ flexDirection: 'row', alignItems: 'center', gap: 16, minHeight: 80, padding: 12, borderRadius: 18, backgroundColor: pressed ? '#1D1D1D' : 'transparent' }), children: [_jsx(View, { style: { width: 48, height: 48, borderRadius: 24, backgroundColor: '#1D1D1D', alignItems: 'center', justifyContent: 'center' }, children: _jsx(MessageIcon, { name: kind === 'phone' ? 'phone-portrait-outline' : 'phone', color: "#F7F7F7", size: 24 }) }), _jsxs(View, { style: { flex: 1, gap: 4 }, children: [_jsx(Text, { style: { color: '#F7F7F7', fontSize: 18, fontWeight: '600' }, children: label }), _jsx(Text, { style: { color: '#A3A3A3', fontSize: 15 }, children: detail })] })] });
|
|
42
|
-
return _jsxs(_Fragment, { children: [actions?.video && button(actions.videoLabel ?? 'Video call', 'video', () => void invoke('video')), (actions?.voice || validPhone) && button('Call '
|
|
42
|
+
const button = (label, icon, press) => _jsx(Pressable, { accessibilityRole: "button", accessibilityLabel: uiText(label), onPress: press, style: { width: icon === 'video' ? 42 : 44, height: icon === 'video' ? 42 : 44, alignItems: 'center', justifyContent: 'center' }, children: _jsx(MessageIcon, { name: icon, size: 23, color: color }) });
|
|
43
|
+
const row = (kind, label, detail) => _jsxs(Pressable, { accessibilityRole: "button", accessibilityLabel: uiText(label), onPress: () => choose(kind), style: ({ pressed }) => ({ flexDirection: 'row', alignItems: 'center', gap: 16, minHeight: 80, padding: 12, borderRadius: 18, backgroundColor: pressed ? '#1D1D1D' : 'transparent' }), children: [_jsx(View, { style: { width: 48, height: 48, borderRadius: 24, backgroundColor: '#1D1D1D', alignItems: 'center', justifyContent: 'center' }, children: _jsx(MessageIcon, { name: kind === 'phone' ? 'phone-portrait-outline' : 'phone', color: "#F7F7F7", size: 24 }) }), _jsxs(View, { style: { flex: 1, gap: 4 }, children: [_jsx(Text, { style: { color: '#F7F7F7', fontSize: 18, fontWeight: '600' }, children: uiText(label) }), _jsx(Text, { style: { color: '#A3A3A3', fontSize: 15 }, children: uiText(detail) })] })] });
|
|
44
|
+
return _jsxs(_Fragment, { children: [actions?.video && button(actions.videoLabel ?? 'Video call', 'video', () => void invoke('video')), (actions?.voice || validPhone) && button(uiText('Call {name}', { name }), 'phone', () => { if (!validPhone) {
|
|
43
45
|
void invoke('voice');
|
|
44
46
|
return;
|
|
45
|
-
} Keyboard.dismiss(); setVisible(true); }), _jsx(MessagingPopup, { visible: visible || !!error, compact: true, title: 'Call '
|
|
47
|
+
} Keyboard.dismiss(); setVisible(true); }), _jsx(MessagingPopup, { visible: visible || !!error, compact: true, title: uiText('Call {name}', { name }), background: "#111111", onDismiss: dismissed, onClose: () => { pending.current = null; setVisible(false); setError(''); }, children: error ? _jsx(Text, { accessibilityRole: "alert", style: { color: '#F7F7F7', fontSize: 16, paddingTop: 12 }, children: error }) : _jsxs(_Fragment, { children: [actions?.voice && row('voice', actions.voiceLabel ?? 'Internet call', 'Wi-Fi or mobile data'), validPhone && row('phone', 'Phone call', validPhone)] }) })] });
|
|
46
48
|
}
|
package/dist/inbox-search.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
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 { useEffect, useRef, useState } from 'react';
|
|
3
4
|
import { ActivityIndicator, FlatList, Pressable, Text, View } from 'react-native';
|
|
4
5
|
/** Main-screen search covers retained messages beyond the current inbox page. */
|
|
5
6
|
export function InboxMessageSearch({ client, query, appearance, name, chats, onSelect }) {
|
|
7
|
+
const { text: uiText, locale: uiLocale, number: uiNumber, count: uiCount } = useRobonoLocale();
|
|
6
8
|
const c = appearance.colors, [rows, setRows] = useState([]), [next, setNext] = useState(null), [busy, setBusy] = useState(false), [error, setError] = useState('');
|
|
7
9
|
const controller = useRef(null), generation = useRef(0);
|
|
8
10
|
async function search(more = false) {
|
|
@@ -22,7 +24,7 @@ export function InboxMessageSearch({ client, query, appearance, name, chats, onS
|
|
|
22
24
|
catch (e) {
|
|
23
25
|
if (version === generation.current && !control.signal.aborted) {
|
|
24
26
|
setRows([]);
|
|
25
|
-
setError(e instanceof Error ? e.message :
|
|
27
|
+
setError(e instanceof Error ? e.message : uiText("Messages could not be searched."));
|
|
26
28
|
}
|
|
27
29
|
}
|
|
28
30
|
finally {
|
|
@@ -38,5 +40,5 @@ export function InboxMessageSearch({ client, query, appearance, name, chats, onS
|
|
|
38
40
|
const timer = setTimeout(() => void search(), 300);
|
|
39
41
|
return () => { clearTimeout(timer); ++generation.current; controller.current?.abort(); };
|
|
40
42
|
}, [client, query]);
|
|
41
|
-
return _jsx(FlatList, { style: { flex: 1 }, data: rows, keyExtractor: row => row.message.conversationId + ':' + row.message.id, keyboardShouldPersistTaps: "handled", ListHeaderComponent: _jsxs(View, { children: [chats.length > 0 && _jsxs(_Fragment, { children: [_jsx(Text, { style: { paddingHorizontal: 16, paddingVertical: 8, color: c.secondary, fontSize: 14, fontWeight: '600' }, children: "Chats" }), chats.map(chat => _jsx(Pressable, { accessibilityRole: "button", onPress: () => onSelect(chat), style: { minHeight: 48, paddingHorizontal: 16, justifyContent: 'center' }, children: _jsx(Text, { style: { color: c.foreground, fontSize: 16 }, children: name(chat) }) }, chat.id))] }), _jsx(Text, { style: { paddingHorizontal: 16, paddingVertical: 12, color: c.secondary, fontSize: 14, fontWeight: '600' }, children: "Messages" })] }), ListEmptyComponent: !busy && !error ? _jsx(Text, { style: { padding: 16, color: c.secondary }, children: "No matching messages." }) : null, renderItem: ({ item }) => _jsxs(Pressable, { accessibilityRole: "button", accessibilityLabel:
|
|
43
|
+
return _jsx(FlatList, { style: { flex: 1 }, data: rows, keyExtractor: row => row.message.conversationId + ':' + row.message.id, keyboardShouldPersistTaps: "handled", ListHeaderComponent: _jsxs(View, { children: [chats.length > 0 && _jsxs(_Fragment, { children: [_jsx(Text, { style: { paddingHorizontal: 16, paddingVertical: 8, color: c.secondary, fontSize: 14, fontWeight: '600' }, children: uiText("Chats") }), chats.map(chat => _jsx(Pressable, { accessibilityRole: "button", onPress: () => onSelect(chat), style: { minHeight: 48, paddingHorizontal: 16, justifyContent: 'center' }, children: _jsx(Text, { style: { color: c.foreground, fontSize: 16 }, children: name(chat) }) }, chat.id))] }), _jsx(Text, { style: { paddingHorizontal: 16, paddingVertical: 12, color: c.secondary, fontSize: 14, fontWeight: '600' }, children: uiText("Messages") })] }), ListEmptyComponent: !busy && !error ? _jsx(Text, { style: { padding: 16, color: c.secondary }, children: uiText("No matching messages.") }) : null, renderItem: ({ item }) => _jsxs(Pressable, { accessibilityRole: "button", accessibilityLabel: uiText("Message in {value1}: {value2}", { value1: name(item.conversation), value2: item.message.text ?? '' }), onPress: () => onSelect(item.conversation, item.message.id), style: { paddingHorizontal: 16, paddingVertical: 12, gap: 5, borderBottomWidth: 1, borderBottomColor: c.foreground + '16' }, children: [_jsxs(View, { style: { flexDirection: 'row', gap: 8 }, children: [_jsx(Text, { numberOfLines: 1, style: { flex: 1, fontSize: 16, fontWeight: '600', color: c.foreground }, children: name(item.conversation) }), _jsx(Text, { style: { fontSize: 12, color: c.secondary }, children: new Date(item.message.createdAt).toLocaleDateString(uiLocale) })] }), _jsx(Text, { numberOfLines: 3, style: { fontSize: 15, lineHeight: 21, color: c.secondary }, children: item.message.text })] }), ListFooterComponent: _jsxs(View, { children: [busy && _jsx(ActivityIndicator, { accessibilityLabel: uiText("Searching all messages"), color: c.accent, style: { padding: 16 } }), !!error && _jsxs(_Fragment, { children: [_jsx(Text, { accessibilityRole: "alert", style: { padding: 16, color: c.foreground }, children: error }), _jsx(Pressable, { accessibilityRole: "button", onPress: () => void search(), style: { minHeight: 44, paddingHorizontal: 16, justifyContent: 'center' }, children: _jsx(Text, { style: { color: c.accent }, children: uiText("Try again") }) })] }), next && !busy && !error && _jsx(Pressable, { accessibilityRole: "button", onPress: () => void search(true), style: { minHeight: 48, alignItems: 'center', justifyContent: 'center' }, children: _jsx(Text, { style: { color: c.accent }, children: uiText("More results") }) })] }) });
|
|
42
44
|
}
|