@robono/messaging-react-native 0.8.0 → 0.10.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 +8 -0
- package/README.md +92 -37
- package/SBOM.spdx.json +577 -217
- package/dist/account.js +7 -4
- package/dist/attachment-menu.js +5 -2
- package/dist/call-message.js +8 -5
- package/dist/calling.js +6 -2
- package/dist/conversation-details.d.ts +1 -0
- package/dist/conversation-details.js +11 -7
- package/dist/custom-message.d.ts +7 -0
- package/dist/custom-message.js +53 -0
- package/dist/customization.d.ts +49 -0
- package/dist/customization.js +1 -0
- package/dist/expo-connected-calling.js +7 -2
- package/dist/expo-extras.js +4 -1
- package/dist/expo-media.js +27 -5
- package/dist/expo-messenger.d.ts +41 -2
- package/dist/expo-messenger.js +86 -10
- package/dist/expo-outbox.js +22 -1
- package/dist/expo-screen.d.ts +1 -1
- package/dist/expo-screen.js +42 -31
- package/dist/expo-social.js +5 -2
- package/dist/expo.d.ts +1 -1
- package/dist/expo.js +1 -1
- package/dist/extras.js +11 -7
- package/dist/forward.js +8 -5
- package/dist/group-editor.js +9 -6
- package/dist/header-calls.js +9 -6
- package/dist/icons.js +2 -1
- package/dist/inbox-search.d.ts +3 -1
- package/dist/inbox-search.js +33 -10
- package/dist/inbox-view.d.ts +21 -0
- package/dist/inbox-view.js +32 -0
- package/dist/inbox.d.ts +6 -1
- package/dist/inbox.js +27 -18
- package/dist/index.d.ts +16 -6
- package/dist/index.js +122 -81
- package/dist/localization.d.ts +24 -0
- package/dist/localization.js +12 -0
- package/dist/maps-connection.js +5 -2
- package/dist/media-ui.js +15 -8
- package/dist/message-menu.js +5 -2
- package/dist/native-outbox.d.ts +10 -2
- package/dist/native-outbox.js +7 -0
- package/dist/polls.js +21 -16
- package/dist/popup.js +8 -4
- package/dist/public-rooms.d.ts +10 -0
- package/dist/public-rooms.js +78 -0
- package/dist/quick-send.js +6 -3
- package/dist/room-moderation.d.ts +22 -0
- package/dist/room-moderation.js +87 -0
- package/dist/safety.js +11 -8
- package/dist/saved.js +7 -4
- package/dist/scheduled.js +19 -14
- package/dist/search.js +7 -4
- package/dist/social.js +9 -5
- package/dist/translation-settings.js +7 -4
- package/dist/typography.d.ts +12 -0
- package/dist/typography.js +31 -0
- package/dist/voice-composer.js +11 -6
- package/package.json +10 -8
- package/setup.plugin.cjs +20 -0
package/dist/expo-messenger.js
CHANGED
|
@@ -1,16 +1,19 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { createInboxViewMemory } from './inbox-view.js';
|
|
3
|
+
import { MessagingPublicRooms } from './public-rooms.js';
|
|
4
|
+
import { RobonoLocaleProvider, useRobonoLocale } from './localization.js';
|
|
2
5
|
import { useMessengerMaps, MessengerMapsNotice } from './maps-connection.js';
|
|
3
6
|
import { MessagingCallingScreen } from './calling.js';
|
|
4
7
|
import { createExpoMessengerCalling, useExpoMessengerCalling, ExpoMessengerCalls } from './expo-connected-calling.js';
|
|
5
8
|
import { isNativeCallAudioActive } from '@robono/calling-react-native/native';
|
|
6
|
-
import { View } from 'react-native';
|
|
9
|
+
import { View, AppState } from 'react-native';
|
|
7
10
|
import { createExpoMaps, RobonoMaps } from '@robono/location-react-native/expo';
|
|
8
11
|
import { createExpoMessagingExtras } from './expo-extras.js';
|
|
9
12
|
import { connectExpoMessagingNotifications } from './expo-notifications.js';
|
|
10
|
-
import { useEffect, useState, useSyncExternalStore } from 'react';
|
|
11
|
-
import { createMessagingTransport, defaultMessagingSettings, MessagingInboxClient } from '@robono/messaging';
|
|
13
|
+
import { useMemo, useLayoutEffect, useEffect, useState, useSyncExternalStore } from 'react';
|
|
14
|
+
import { MessagingAvailabilityClient, connectHostedRobono, createMessagingTransport, defaultMessagingSettings, MessagingInboxClient } from '@robono/messaging';
|
|
12
15
|
import { ExpoMessagingScreen } from './expo-screen.js';
|
|
13
|
-
import { createExpoMessagingClient } from './expo-outbox.js';
|
|
16
|
+
import { createExpoMessagingClient, expoOutbox } from './expo-outbox.js';
|
|
14
17
|
import { createExpoMessagingAudioSession, createExpoMessagingStorage } from './expo-media.js';
|
|
15
18
|
import { MessagingProvider, MessagingAccountMenu } from './index.js';
|
|
16
19
|
import { MessagingInbox } from './inbox.js';
|
|
@@ -25,8 +28,30 @@ export function createExpoMessenger(options) {
|
|
|
25
28
|
const client = createExpoMessagingClient({ ...options, url: endpoint.href });
|
|
26
29
|
const mapConnection = options.maps === false ? undefined : createExpoMaps(options);
|
|
27
30
|
const connectedExtras = options.extras === false ? undefined : createExpoMessagingExtras(options);
|
|
31
|
+
const availability = options.availability === false ? undefined : new MessagingAvailabilityClient({ request: createMessagingTransport({ ...options, url: endpoint.href }) });
|
|
32
|
+
const activityChanged = (state) => { if (state === 'active')
|
|
33
|
+
availability?.start();
|
|
34
|
+
else
|
|
35
|
+
void availability?.stop(); };
|
|
36
|
+
const activitySubscription = availability ? AppState.addEventListener('change', activityChanged) : undefined;
|
|
37
|
+
activityChanged(AppState.currentState);
|
|
28
38
|
const inbox = new MessagingInboxClient({ request: createMessagingTransport({ ...options, url: endpoint.href }) });
|
|
29
39
|
const storage = createExpoMessagingStorage({ url: media.href, getAccessToken: options.getAccessToken, ...(options.fetch ? { fetch: options.fetch } : {}) });
|
|
40
|
+
const outgoing = expoOutbox(client);
|
|
41
|
+
if (outgoing)
|
|
42
|
+
client.setRecoveryAdapter(worker => {
|
|
43
|
+
const abort = new AbortController(), conversationId = worker.getSnapshot().conversationId;
|
|
44
|
+
const release = worker.setAttachmentPreparer(async (content) => {
|
|
45
|
+
if (!content.media || content.kind === 'text' || content.kind === 'poll')
|
|
46
|
+
throw Error('The saved attachment is unavailable.');
|
|
47
|
+
const file = await outgoing.file(conversationId, content.media.assetId);
|
|
48
|
+
if (!file)
|
|
49
|
+
throw Error('The saved attachment is missing. Remove it and choose it again.');
|
|
50
|
+
const media = await storage.upload(file, { conversationId, uploadId: content.media.assetId, kind: content.kind, signal: abort.signal, onProgress: () => { } });
|
|
51
|
+
return { ...content, media };
|
|
52
|
+
});
|
|
53
|
+
return () => { abort.abort(); release(); };
|
|
54
|
+
});
|
|
30
55
|
const calling = options.calling === false ? undefined : createExpoMessengerCalling({ ...options, inbox });
|
|
31
56
|
const audioSession = options.audioSession ?? createExpoMessagingAudioSession({ isCallActive: () => isNativeCallAudioActive() || calling?.isActive() === true || options.isCallActive?.() === true });
|
|
32
57
|
let connection = null, pending = null, closed = false;
|
|
@@ -69,20 +94,71 @@ export function createExpoMessenger(options) {
|
|
|
69
94
|
throw Error('Notifications are not connected yet. Check your Apple/Google setup in Robono.'); if (!await connection.enable())
|
|
70
95
|
throw Error('Allow notifications in your phone’s app settings.'); },
|
|
71
96
|
};
|
|
72
|
-
return { client, inbox, storage, audioSession, notifications, calling, maps: mapConnection?.workspace, ...(connectedExtras ? { extras: connectedExtras.extras, components: { TrackingMap: connectedExtras.TrackingMap } } : {}),
|
|
73
|
-
async signOut() {
|
|
97
|
+
return { client, inbox, storage, audioSession, notifications, calling, availability, maps: mapConnection?.workspace, ...(connectedExtras ? { extras: connectedExtras.extras, components: { TrackingMap: connectedExtras.TrackingMap } } : {}),
|
|
98
|
+
async signOut() { if (closed)
|
|
99
|
+
return; closed = true; activitySubscription?.remove(); const extraCleanup = connectedExtras?.signOut(); inbox.dispose(); const results = await Promise.allSettled([availability?.signOut(), extraCleanup, calling?.dispose(), mapConnection?.signOut(), client.signOut(), (async () => { await pending?.catch(() => { }); await connection?.signOut(); })()]); const failed = results.find(r => r.status === 'rejected'); if (failed?.status === 'rejected')
|
|
74
100
|
throw failed.reason; },
|
|
75
|
-
dispose() {
|
|
101
|
+
dispose() { if (closed)
|
|
102
|
+
return; closed = true; activitySubscription?.remove(); availability?.dispose(); void calling?.dispose().catch(() => { }); mapConnection?.dispose(); void connectedExtras?.dispose().catch(() => { }); connection?.dispose(); inbox.dispose(); client.dispose(); },
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
export async function createHostedExpoMessenger(options) {
|
|
106
|
+
const session = await connectHostedRobono(options);
|
|
107
|
+
if (options.signal?.aborted) {
|
|
108
|
+
await session.signOut();
|
|
109
|
+
throw Error('The messenger was closed.');
|
|
110
|
+
}
|
|
111
|
+
let connection;
|
|
112
|
+
try {
|
|
113
|
+
connection = createExpoMessenger({ ...options, endpoint: session.endpoint, getAccessToken: session.getAccessToken, namespace: session.namespace });
|
|
114
|
+
}
|
|
115
|
+
catch (error) {
|
|
116
|
+
await session.signOut().catch(() => { });
|
|
117
|
+
throw error;
|
|
118
|
+
}
|
|
119
|
+
let signingOut;
|
|
120
|
+
return { ...connection,
|
|
121
|
+
signOut() { return signingOut ??= (async () => { try {
|
|
122
|
+
await connection.signOut();
|
|
123
|
+
}
|
|
124
|
+
finally {
|
|
125
|
+
await session.signOut();
|
|
126
|
+
} })(); },
|
|
127
|
+
dispose() { if (!signingOut) {
|
|
128
|
+
connection.dispose();
|
|
129
|
+
session.dispose();
|
|
130
|
+
} },
|
|
76
131
|
};
|
|
77
132
|
}
|
|
78
133
|
/** Supplied inbox, group creation, conversation, settings and media screens. */
|
|
79
|
-
export function ExpoMessenger(
|
|
134
|
+
export function ExpoMessenger(props) {
|
|
135
|
+
return _jsx(RobonoLocaleProvider, { value: props.interfaceLocale, children: _jsx(ExpoMessengerContent, { ...props }) });
|
|
136
|
+
}
|
|
137
|
+
function ExpoMessengerContent({ inbox, contacts = {}, initialConversation, selectedConversation, onSelectConversation, notifications, maps, calling, ...props }) {
|
|
138
|
+
const inboxMemory = useMemo(createInboxViewMemory, [inbox, props.userId]);
|
|
139
|
+
const { configuration: mapsLocale } = useRobonoLocale();
|
|
80
140
|
const callState = useExpoMessengerCalling(calling);
|
|
81
141
|
useEffect(() => { calling?.updateContacts(contacts); }, [calling, contacts]);
|
|
82
142
|
const chatState = useSyncExternalStore(props.client.subscribe, props.client.getSnapshot, props.client.getSnapshot);
|
|
83
143
|
const groupAllowed = chatState.conversation?.kind !== 'group' || !chatState.conversation.supergroup && (chatState.conversation.memberCount ?? 0) >= 2 && (chatState.conversation.memberCount ?? 0) <= 8;
|
|
84
|
-
const [internal, setInternal] = useState(initialConversation ?? null), [newGroup, setNewGroup] = useState(false);
|
|
144
|
+
const [internal, setInternal] = useState(initialConversation ?? null), [newGroup, setNewGroup] = useState(false), [publicRooms, setPublicRooms] = useState(false);
|
|
85
145
|
const selected = selectedConversation === undefined ? internal : selectedConversation;
|
|
146
|
+
// Declarative navigation loads the selected conversation itself. Internal open()
|
|
147
|
+
// changes the client synchronously, so this never opens it a second time.
|
|
148
|
+
useLayoutEffect(() => { if (!selected) {
|
|
149
|
+
props.client.stop();
|
|
150
|
+
return;
|
|
151
|
+
} if (props.client.getSnapshot().conversationId !== selected.id)
|
|
152
|
+
void props.client.open(selected.id).catch(() => { }); }, [selected?.id, props.client]);
|
|
153
|
+
useEffect(() => {
|
|
154
|
+
const sync = (state) => { if (selected && props.active !== false && (state === null || state === 'active'))
|
|
155
|
+
props.client.start();
|
|
156
|
+
else
|
|
157
|
+
props.client.stop(); };
|
|
158
|
+
sync(AppState.currentState);
|
|
159
|
+
const subscription = AppState.addEventListener('change', sync);
|
|
160
|
+
return () => { subscription.remove(); props.client.stop(); };
|
|
161
|
+
}, [selected?.id, props.client, props.active]);
|
|
86
162
|
const autoCalls = calling && selected && groupAllowed ? { ...(callState.features.voice ? { voice: () => calling.call(selected.id, 'voice', selected.kind === 'group') } : {}), ...(callState.features.video ? { video: () => calling.call(selected.id, 'video', selected.kind === 'group') } : {}), voiceLabel: 'Internet call' } : undefined;
|
|
87
163
|
const setSelected = (row) => { setInternal(row); onSelectConversation?.(row); };
|
|
88
164
|
const [showMaps, setShowMaps] = useState(false), mapConnection = useMessengerMaps(maps), mapsAvailable = mapConnection.available;
|
|
@@ -97,5 +173,5 @@ export function ExpoMessenger({ inbox, contacts = {}, initialConversation, selec
|
|
|
97
173
|
open(row, message); }).catch(() => { }); }, [notifications, inbox]);
|
|
98
174
|
const account = notifications ? { name: props.account?.name ?? 'Your account', ...props.account, onEnableNotifications: () => notifications.enable() } : props.account;
|
|
99
175
|
const contact = selected?.kind === 'group' ? { name: selected.name ?? 'Group' } : contacts[selected?.contactId ?? ''] ?? { name: selected?.contactId ?? 'Chat' };
|
|
100
|
-
return _jsxs(View, { style: { flex: 1, minHeight: 0 }, children: [_jsx(MessengerMapsNotice, { error: mapConnection.error, retry: mapConnection.retry }), showMaps && maps ? _jsx(RobonoMaps, { workspace: maps, contacts: contacts, onClose: () => setShowMaps(false) }) : _jsx(ExpoMessagingScreen, { ...props, account: account, contact: contact, participants: props.participants ?? contacts, extras: selected ? props.extras : undefined, callActions: selected ? { ...autoCalls, ...props.callActions, phoneNumber: contacts[selected.contactId ?? '']?.phoneNumber ?? props.callActions?.phoneNumber } : undefined, onBack: () => { props.client.stop(); setSelected(null); void inbox.refresh().catch(() => { }); }, renderConversation: screen => selected ? _jsx(MessagingCallingScreen, { ...screen, onCall: screen.callActions?.voice, onVideoCall: screen.callActions?.video, callLabel: "Internet call", unansweredCallId: callState.unanswered?.conversationId === selected.id ? callState.unanswered.id : undefined, onDismissCallMessage: calling?.dismissUnanswered }) : _jsxs(MessagingProvider, { ...screen, children: [_jsx(MessagingInbox, { client: inbox, userId: props.userId, contacts: contacts, companyName: props.companyName, appearance: props.appearance, active: props.active !== false, headerActions: _jsx(MessagingAccountMenu, { onMaps: mapsAvailable ? () => setShowMaps(true) : undefined, onNewGroup: inboxState.groupCreationAvailable ? () => setNewGroup(true) : undefined }), onSelect: open, onDeleted: id => props.client.forgetConversation(id) }), newGroup && _jsx(MessagingGroupEditor, { client: props.client, inbox: inbox, userId: props.userId, contacts: contacts, groups: inboxState.groups, accent: (props.appearance ?? inboxState.appearance ?? defaultMessagingSettings().appearance).colors.accent, onClose: () => setNewGroup(false), onSaved: (id, name) => { setNewGroup(false); const row = inbox.getSnapshot().conversations.find(c => c.id === id) ?? { id, kind: 'group', name, contactId: null, lastMessage: null, unreadCount: 0, canSend: true }; open(row); } })] }) }), _jsx(ExpoMessengerCalls, { calling: calling, state: callState })] });
|
|
176
|
+
return _jsxs(View, { style: { flex: 1, minHeight: 0 }, children: [_jsx(MessengerMapsNotice, { error: mapConnection.error, retry: mapConnection.retry }), showMaps && maps ? _jsx(RobonoMaps, { interfaceLocale: mapsLocale, workspace: maps, contacts: contacts, onClose: () => setShowMaps(false) }) : _jsx(ExpoMessagingScreen, { ...props, account: account, contact: contact, participants: props.participants ?? contacts, extras: selected ? props.extras : undefined, callActions: selected ? { ...autoCalls, ...props.callActions, phoneNumber: contacts[selected.contactId ?? '']?.phoneNumber ?? props.callActions?.phoneNumber } : undefined, onBack: () => { props.client.stop(); setSelected(null); void inbox.refresh().catch(() => { }); }, renderConversation: screen => selected ? _jsx(MessagingCallingScreen, { ...screen, onCall: screen.callActions?.voice, onVideoCall: screen.callActions?.video, callLabel: "Internet call", unansweredCallId: callState.unanswered?.conversationId === selected.id ? callState.unanswered.id : undefined, onDismissCallMessage: calling?.dismissUnanswered }) : _jsxs(MessagingProvider, { ...screen, children: [_jsx(MessagingInbox, { viewMemory: inboxMemory, client: inbox, userId: props.userId, contacts: contacts, companyName: props.companyName, appearance: props.appearance, active: props.active !== false, headerActions: _jsx(MessagingAccountMenu, { onPublicRooms: inboxState.publicRoomsAvailable ? () => setPublicRooms(true) : undefined, onMaps: mapsAvailable ? () => setShowMaps(true) : undefined, onNewGroup: inboxState.groupCreationAvailable ? () => setNewGroup(true) : undefined }), onSelect: open, onDeleted: id => props.client.forgetConversation(id) }), publicRooms && _jsx(MessagingPublicRooms, { inbox: inbox, accent: (props.appearance ?? inboxState.appearance ?? defaultMessagingSettings().appearance).colors.accent, createRequestId: () => props.client.createForwardId(), onClose: () => setPublicRooms(false), onJoined: (id, name) => { setPublicRooms(false); open(inbox.getSnapshot().conversations.find(c => c.id === id) ?? { id, kind: 'group', name, contactId: null, lastMessage: null, unreadCount: 0, canSend: true }); } }), newGroup && _jsx(MessagingGroupEditor, { client: props.client, inbox: inbox, userId: props.userId, contacts: contacts, groups: inboxState.groups, accent: (props.appearance ?? inboxState.appearance ?? defaultMessagingSettings().appearance).colors.accent, onClose: () => setNewGroup(false), onSaved: (id, name) => { setNewGroup(false); const row = inbox.getSnapshot().conversations.find(c => c.id === id) ?? { id, kind: 'group', name, contactId: null, lastMessage: null, unreadCount: 0, canSend: true }; open(row); } })] }) }), _jsx(ExpoMessengerCalls, { calling: calling, state: callState })] });
|
|
101
177
|
}
|
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,8 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { MessagingTypography } from './typography.js';
|
|
3
|
+
import { Text } from './typography.js';
|
|
4
|
+
import { RobonoLocaleProvider } from './localization.js';
|
|
5
|
+
import { useRobonoLocale } from './localization.js';
|
|
2
6
|
import * as Haptics from 'expo-haptics';
|
|
3
7
|
import { Vibration } from 'react-native';
|
|
4
8
|
import { MessagingPopupHeader, messagingPopupBackdrop } from './popup.js';
|
|
@@ -9,7 +13,7 @@ import { expoSaved } from './expo-saved.js';
|
|
|
9
13
|
export { createExpoMessagingClient } from './expo-outbox.js';
|
|
10
14
|
import { createExpoThumbnails } from './expo-thumbnails.js';
|
|
11
15
|
import { useCallback, useEffect, useContext, useMemo, useRef, useState, useSyncExternalStore } from 'react';
|
|
12
|
-
import { AppState, Image, Modal, Platform, Pressable, ScrollView,
|
|
16
|
+
import { AppState, Image, Modal, Platform, Pressable, ScrollView, View } from 'react-native';
|
|
13
17
|
import { SafeAreaProvider, SafeAreaView } from 'react-native-safe-area-context';
|
|
14
18
|
import MaterialIcons from "./visual/RobonoIcon";
|
|
15
19
|
import { useAudioPlayer } from 'expo-audio';
|
|
@@ -68,6 +72,7 @@ async function acquireVoice(scope, audio) {
|
|
|
68
72
|
return scope.alive && scope.epoch === epoch && !!scope.release;
|
|
69
73
|
}
|
|
70
74
|
function VoicePlayer({ file, audio, design, inline, draft, autoPlay = false }) {
|
|
75
|
+
const { text: uiText, locale: uiLocale, number: uiNumber, count: uiCount } = useRobonoLocale();
|
|
71
76
|
const [playing, setPlaying] = useState(false), [position, setPosition] = useState(0), [error, setError] = useState('');
|
|
72
77
|
const source = useMemo(() => file.stream ? { uri: file.uri, headers: file.stream.headers } : file.uri, [file]);
|
|
73
78
|
const player = useAudioPlayer(source, { updateInterval: 100, downloadFirst: false, preferredForwardBufferDuration: 5, keepAudioSessionActive: true });
|
|
@@ -110,7 +115,7 @@ function VoicePlayer({ file, audio, design, inline, draft, autoPlay = false }) {
|
|
|
110
115
|
}
|
|
111
116
|
catch { } await releaseVoice(scope); await scope.acquiring?.catch(() => { }); await releaseVoice(scope); };
|
|
112
117
|
const fail = (e) => { cancelled = true; autoPreparing = false; scope.busy = false; clearTimeout(startupTimeout); if (scope.alive)
|
|
113
|
-
setError(e instanceof Error ? e.message :
|
|
118
|
+
setError(e instanceof Error ? e.message : uiText("Playback failed.")); scope.epoch++; void releaseVoice(scope); };
|
|
114
119
|
// Native file loading and audio-session preparation are independent. Start
|
|
115
120
|
// both now; playback still requires both and an active app.
|
|
116
121
|
const begin = () => {
|
|
@@ -217,7 +222,7 @@ function VoicePlayer({ file, audio, design, inline, draft, autoPlay = false }) {
|
|
|
217
222
|
}
|
|
218
223
|
catch (e) {
|
|
219
224
|
if (scope.alive)
|
|
220
|
-
setError(e instanceof Error ? e.message :
|
|
225
|
+
setError(e instanceof Error ? e.message : uiText("Playback failed."));
|
|
221
226
|
await releaseVoice(scope);
|
|
222
227
|
}
|
|
223
228
|
finally {
|
|
@@ -230,9 +235,10 @@ function VoicePlayer({ file, audio, design, inline, draft, autoPlay = false }) {
|
|
|
230
235
|
if (draft)
|
|
231
236
|
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
237
|
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(
|
|
238
|
+
setError(uiText("Could not seek. Try again.")); }); } });
|
|
234
239
|
}
|
|
235
240
|
function VideoPlayer({ file, audio }) {
|
|
241
|
+
const { text: uiText, locale: uiLocale, number: uiNumber, count: uiCount } = useRobonoLocale();
|
|
236
242
|
const mediaReady = useContext(MediaReadiness), recover = useContext(StreamRecovery);
|
|
237
243
|
const source = useMemo(() => file.stream ? { uri: file.uri, headers: file.stream.headers, contentType: 'progressive', useCaching: false } : file.uri, [file]);
|
|
238
244
|
const player = useVideoPlayer(source, p => { p.loop = false; p.audioMixingMode = 'mixWithOthers'; if (file.stream)
|
|
@@ -271,7 +277,7 @@ function VideoPlayer({ file, audio }) {
|
|
|
271
277
|
desired.current = false;
|
|
272
278
|
player.pause();
|
|
273
279
|
setReady(false);
|
|
274
|
-
setError(
|
|
280
|
+
setError(uiText("Playback paused for a call. Close and reopen the video to continue."));
|
|
275
281
|
void release?.().catch(() => { });
|
|
276
282
|
release = undefined;
|
|
277
283
|
} }, 100);
|
|
@@ -287,14 +293,14 @@ function VideoPlayer({ file, audio }) {
|
|
|
287
293
|
});
|
|
288
294
|
const listener = player.addListener('statusChange', s => { if (s.status === 'error') {
|
|
289
295
|
if (!recoverStream())
|
|
290
|
-
setError(
|
|
296
|
+
setError(uiText("This video could not be played on this phone."));
|
|
291
297
|
}
|
|
292
298
|
else
|
|
293
299
|
begin(); });
|
|
294
300
|
if (player.status === 'error')
|
|
295
301
|
recoverStream();
|
|
296
302
|
const timeout = setTimeout(() => { if (alive && desired.current && player.currentTime === 0 && player.status !== 'readyToPlay' && !recoverStream())
|
|
297
|
-
setError(
|
|
303
|
+
setError(uiText("This video could not be loaded. Try again.")); }, 15000);
|
|
298
304
|
const app = AppState.addEventListener('change', s => { if (s !== 'active') {
|
|
299
305
|
desired.current = false;
|
|
300
306
|
player.pause();
|
|
@@ -307,25 +313,27 @@ function VideoPlayer({ file, audio }) {
|
|
|
307
313
|
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
314
|
}
|
|
309
315
|
function FileViewer({ file, kind, audio, design }) {
|
|
316
|
+
const { text: uiText, locale: uiLocale, number: uiNumber, count: uiCount } = useRobonoLocale();
|
|
310
317
|
const [error, setError] = useState('');
|
|
311
318
|
if (kind === 'voice')
|
|
312
319
|
return _jsx(VoicePlayer, { file: file, audio: audio, design: design });
|
|
313
320
|
if (kind === 'video')
|
|
314
321
|
return _jsx(VideoPlayer, { file: file, audio: audio, design: design });
|
|
315
322
|
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: () => {
|
|
323
|
+
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 })] });
|
|
324
|
+
return _jsxs(View, { children: [_jsx(Control, { label: uiText("File options"), icon: "open-in-new", color: design.colors.accent, onPress: () => {
|
|
318
325
|
void (async () => { try {
|
|
319
326
|
if (!await Sharing.isAvailableAsync())
|
|
320
327
|
throw Error('No file-opening menu is available.');
|
|
321
328
|
await Sharing.shareAsync(file.uri, { mimeType: file.mimeType, dialogTitle: file.fileName });
|
|
322
329
|
}
|
|
323
330
|
catch (e) {
|
|
324
|
-
setError(e instanceof Error ? e.message :
|
|
331
|
+
setError(e instanceof Error ? e.message : uiText("The file could not be opened."));
|
|
325
332
|
} })();
|
|
326
333
|
} }), !!error && _jsx(Text, { accessibilityRole: "alert", style: { color: design.colors.foreground }, children: error })] });
|
|
327
334
|
}
|
|
328
335
|
export function MessagingMediaOverlay({ controller, audio, appearance, inlineVoice = false, inlineRecorder = false, viewerActions, onDismiss }) {
|
|
336
|
+
const { text: uiText, locale: uiLocale, number: uiNumber, count: uiCount } = useRobonoLocale();
|
|
329
337
|
const state = useSyncExternalStore(controller.subscribe, controller.getSnapshot, controller.getSnapshot);
|
|
330
338
|
const c = appearance.colors;
|
|
331
339
|
const mediaReady = useContext(MediaReadiness);
|
|
@@ -338,24 +346,27 @@ export function MessagingMediaOverlay({ controller, audio, appearance, inlineVoi
|
|
|
338
346
|
return null;
|
|
339
347
|
if ((typeof inlineVoice === 'function' ? inlineVoice() : inlineVoice) && state.kind === 'voice' && ['opening', 'viewing', 'failed'].includes(state.phase) && !review)
|
|
340
348
|
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 :
|
|
349
|
+
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)
|
|
350
|
+
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
351
|
if (!await Sharing.isAvailableAsync())
|
|
344
352
|
throw Error('File opening is unavailable.');
|
|
345
353
|
await Sharing.shareAsync(state.file.uri, { mimeType: state.file.mimeType, dialogTitle: state.file.fileName });
|
|
346
354
|
mediaReady('file-opened', state.file.uri);
|
|
347
355
|
}
|
|
348
356
|
catch (e) {
|
|
349
|
-
setFileError(e instanceof Error ? e.message :
|
|
357
|
+
setFileError(e instanceof Error ? e.message : uiText("Could not open the file."));
|
|
350
358
|
} })(); }, 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
359
|
// A native modal has its own window; measure its safe area inside that window.
|
|
352
360
|
return _jsx(Modal, { transparent: true, visible: state.phase !== 'idle' && state.phase !== 'choosing', onDismiss: onDismiss, animationType: "fade", onRequestClose: () => { if (expanded)
|
|
353
361
|
setExpanded(false);
|
|
354
362
|
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' ?
|
|
363
|
+
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 ? uiText('Checking attachment… You can cancel.') : uiText('Uploading… {progress}%', { progress: Math.round(state.progress * 100) })) : state.phase === 'opening' ? (state.progress > 0 ? uiText('Opening… {progress}%', { progress: Math.round(state.progress * 100) }) : uiText('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
364
|
}
|
|
357
365
|
/** Optional ready-made recording, preview, upload and download tools for both mobile platforms. */
|
|
358
|
-
export function ExpoMessagingScreen(
|
|
366
|
+
export function ExpoMessagingScreen(props) {
|
|
367
|
+
return _jsx(RobonoLocaleProvider, { value: props.interfaceLocale, children: _jsx(ExpoMessagingScreenContent, { ...props }) });
|
|
368
|
+
}
|
|
369
|
+
function ExpoMessagingScreenContent({ storage, audioSession, renderConversation, preloadRecentMedia = true, ...props }) {
|
|
359
370
|
useEffect(() => { void props.client.reportUsage({ platform: Platform.OS === 'ios' ? 'ios' : Platform.OS === 'android' ? 'android' : 'other', components: ['media-tools'] }).catch(() => { }); }, [props.client]);
|
|
360
371
|
const [requestedMessageAction, setRequestedMessageAction] = useState();
|
|
361
372
|
const afterViewer = useRef(undefined);
|
|
@@ -545,20 +556,20 @@ export function ExpoMessagingScreen({ storage, audioSession, renderConversation,
|
|
|
545
556
|
current.current.onMediaReady?.({ messageId: event.messageId, kind: event.kind, stage, elapsedMs: Date.now() - event.started });
|
|
546
557
|
}
|
|
547
558
|
catch { } };
|
|
548
|
-
return _jsx(StreamRecovery.Provider, { value: (uri, position, playing) => { void controller.fallbackStream(uri, position, playing); }, children: _jsxs(MediaReadiness.Provider, { value: mediaReady, children: [renderConversation ? renderConversation(screenProps) : _jsx(MessagingScreen, { ...screenProps }), _jsx(MessagingMediaOverlay, { onDismiss: viewerDismissed, viewerActions: () => {
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
559
|
+
return _jsx(MessagingTypography, { appearance: props.appearance ?? state.settings?.appearance ?? defaultMessagingSettings().appearance, children: _jsx(StreamRecovery.Provider, { value: (uri, position, playing) => { void controller.fallbackStream(uri, position, playing); }, children: _jsxs(MediaReadiness.Provider, { value: mediaReady, children: [renderConversation ? renderConversation(screenProps) : _jsx(MessagingScreen, { ...screenProps }), _jsx(MessagingMediaOverlay, { onDismiss: viewerDismissed, viewerActions: () => {
|
|
560
|
+
const m = message.current;
|
|
561
|
+
if (!m || !state.messages.some(row => row.id === m.id && !row.deleted))
|
|
562
|
+
return {};
|
|
563
|
+
const action = (kind) => {
|
|
564
|
+
afterViewer.current = { kind, messageId: m.id, sequence: Date.now() };
|
|
565
|
+
void controller.cancel().then(() => {
|
|
566
|
+
// Some installed native runtimes omit onDismiss after a media modal closes.
|
|
567
|
+
// Keep the normal event fast path; recover only after the native fade ends.
|
|
568
|
+
// Navigation/unmount clears the queued action, so it cannot open in another chat.
|
|
569
|
+
if (afterViewer.current)
|
|
570
|
+
dismissFallback.current = setTimeout(viewerDismissed, Platform.OS === 'ios' ? 500 : 250);
|
|
571
|
+
});
|
|
572
|
+
};
|
|
573
|
+
return { onForward: props.client.canForward(m) ? () => action('forward') : undefined, onDelete: m.senderId === props.userId && !m.readAt && state.messageActionsAvailable ? () => action('delete') : undefined };
|
|
574
|
+
}, inlineVoice: () => !!message.current, inlineRecorder: () => !!message.current && !!current.current.client.getSnapshot().settings?.features.voice, controller: controller, audio: audioSession, appearance: props.appearance ?? state.settings?.appearance ?? defaultMessagingSettings().appearance })] }) }) });
|
|
564
575
|
}
|
package/dist/expo-social.js
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
import {
|
|
2
|
+
import { Text } from './typography.js';
|
|
3
|
+
import { useRobonoLocale } from './localization.js';
|
|
4
|
+
import { Linking, UIManager } from 'react-native';
|
|
3
5
|
// Expo builds include this native module when the documented peer is installed.
|
|
4
6
|
const available = !!UIManager.getViewManagerConfig('RNCWebView');
|
|
5
7
|
const WebView = available ? require('react-native-webview').WebView : null;
|
|
6
8
|
export function ExpoSocialPlayer({ html, origin, onError }) {
|
|
9
|
+
const { text: uiText, locale: uiLocale, number: uiNumber, count: uiCount } = useRobonoLocale();
|
|
7
10
|
if (!WebView)
|
|
8
|
-
return _jsx(Text, { children: "Update the app to play this preview." });
|
|
11
|
+
return _jsx(Text, { children: uiText("Update the app to play this preview.") });
|
|
9
12
|
const open = (url) => { try {
|
|
10
13
|
const u = new URL(url);
|
|
11
14
|
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,9 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { Text } from './typography.js';
|
|
3
|
+
import { useRobonoLocale } from './localization.js';
|
|
2
4
|
import { forwardRef, useEffect, useImperativeHandle, useRef, useState } from 'react';
|
|
3
|
-
import { ActivityIndicator, Image, Linking, Platform, Pressable, ScrollView,
|
|
4
|
-
import { currentTrackers, trackerFresh,
|
|
5
|
+
import { ActivityIndicator, Image, Linking, Platform, Pressable, ScrollView, View } from 'react-native';
|
|
6
|
+
import { currentTrackers, trackerFresh, weatherTemperature } from '@robono/messaging';
|
|
5
7
|
import { MessagingPopup, messagingPopupColors } from './popup.js';
|
|
6
8
|
import { MessageIcon } from './icons.js';
|
|
7
9
|
import WeatherArtwork from './visual/WeatherArtwork.js';
|
|
@@ -26,6 +28,7 @@ else
|
|
|
26
28
|
await Linking.openURL(`https://www.google.com/maps/search/?api=1&query=${query}`); }
|
|
27
29
|
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
30
|
export const MessageExtra = forwardRef(function MessageExtra({ addressBubbleHeight = 44, extra, messageId, userId, extras, ink, accent, background, foreground, footer, renderAddress, Map, onCall, onLongPress }, ref) {
|
|
31
|
+
const { text: uiText, locale: uiLocale, number: uiNumber, count: uiCount } = useRobonoLocale();
|
|
29
32
|
const [mapHeight, setMapHeight] = useState(230), [pendingShare, setPendingShare] = useState(false);
|
|
30
33
|
const resolution = useRef(null), shareIntent = useRef(false);
|
|
31
34
|
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 +42,7 @@ export const MessageExtra = forwardRef(function MessageExtra({ addressBubbleHeig
|
|
|
39
42
|
catch (e) {
|
|
40
43
|
shareIntent.current = false;
|
|
41
44
|
setPendingShare(false);
|
|
42
|
-
setError(e instanceof Error ? e.message :
|
|
45
|
+
setError(e instanceof Error ? e.message : uiText("Please try again."));
|
|
43
46
|
}
|
|
44
47
|
finally {
|
|
45
48
|
setBusy(false);
|
|
@@ -60,10 +63,11 @@ export const MessageExtra = forwardRef(function MessageExtra({ addressBubbleHeig
|
|
|
60
63
|
}
|
|
61
64
|
} await extras.startTracking(messageId); setPendingShare(false); };
|
|
62
65
|
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
|
-
|
|
66
|
+
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)) })] });
|
|
67
|
+
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…');
|
|
68
|
+
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(' · ') }) });
|
|
69
|
+
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
70
|
await extras.startTracking(messageId);
|
|
67
71
|
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 ?
|
|
72
|
+
} }), 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
73
|
});
|
package/dist/forward.js
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Text } from './typography.js';
|
|
3
|
+
import { useRobonoLocale } from './localization.js';
|
|
2
4
|
import { messagingPopupColors } from './popup.js';
|
|
3
5
|
import { useEffect, useRef, useState } from 'react';
|
|
4
|
-
import { ActivityIndicator, Pressable, ScrollView,
|
|
6
|
+
import { ActivityIndicator, Pressable, ScrollView, View } from 'react-native';
|
|
5
7
|
import { MessagingPopup } from './popup.js';
|
|
6
8
|
/** Messenger's compact destination picker, retaining per-message retry identities. */
|
|
7
9
|
export function MessagingForward({ messages, client, appearance, participants = {}, userId, onClose }) {
|
|
10
|
+
const { text: uiText, locale: uiLocale, number: uiNumber, count: uiCount } = useRobonoLocale();
|
|
8
11
|
const [page, setPage] = useState(null), [loading, setLoading] = useState(false), [busy, setBusy] = useState(false), [error, setError] = useState('');
|
|
9
12
|
const live = useRef(true), inFlight = useRef(false), attempts = useRef(new Map()), done = useRef(new Set());
|
|
10
13
|
const c = { ...appearance.colors, ...messagingPopupColors };
|
|
@@ -17,7 +20,7 @@ export function MessagingForward({ messages, client, appearance, participants =
|
|
|
17
20
|
}
|
|
18
21
|
catch (e) {
|
|
19
22
|
if (live.current)
|
|
20
|
-
setError(e instanceof Error ? e.message :
|
|
23
|
+
setError(e instanceof Error ? e.message : uiText("Chats could not be loaded."));
|
|
21
24
|
}
|
|
22
25
|
finally {
|
|
23
26
|
inFlight.current = false;
|
|
@@ -49,7 +52,7 @@ export function MessagingForward({ messages, client, appearance, participants =
|
|
|
49
52
|
}
|
|
50
53
|
catch (e) {
|
|
51
54
|
if (live.current)
|
|
52
|
-
setError(e instanceof Error ? e.message :
|
|
55
|
+
setError(e instanceof Error ? e.message : uiText("The message could not be forwarded. Try again."));
|
|
53
56
|
}
|
|
54
57
|
finally {
|
|
55
58
|
inFlight.current = false;
|
|
@@ -57,6 +60,6 @@ export function MessagingForward({ messages, client, appearance, participants =
|
|
|
57
60
|
setBusy(false);
|
|
58
61
|
}
|
|
59
62
|
}
|
|
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 '
|
|
63
|
+
return _jsxs(MessagingPopup, { contentWidth: 640, title: uiText("Forward to"), foreground: c.foreground, closeLabel: uiText("Close forwarding"), closeDisabled: busy, background: c.background, onClose: () => { if (!busy)
|
|
64
|
+
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
65
|
}
|