@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
package/dist/account.js
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useRobonoLocale } from './localization.js';
|
|
2
3
|
import { useEffect, useRef, useState } from 'react';
|
|
3
4
|
import { Image, Platform, Pressable, ScrollView, Text, View } from 'react-native';
|
|
4
5
|
import { MessagingPopup, messagingPopupColors as c } from './popup.js';
|
|
5
6
|
import { MessageIcon } from './icons.js';
|
|
6
7
|
export function MessagingAccountScreen({ account, companyName, onClose }) {
|
|
8
|
+
const { text: uiText, locale: uiLocale, number: uiNumber, count: uiCount } = useRobonoLocale();
|
|
7
9
|
const [busy, setBusy] = useState(false), [error, setError] = useState(''), [visible, setVisible] = useState(true), pending = useRef(null);
|
|
8
10
|
const dismissed = () => { const action = pending.current; pending.current = null; if (action)
|
|
9
11
|
requestAnimationFrame(() => void run(action)); };
|
|
@@ -17,11 +19,11 @@ export function MessagingAccountScreen({ account, companyName, onClose }) {
|
|
|
17
19
|
}
|
|
18
20
|
catch (e) {
|
|
19
21
|
setVisible(true);
|
|
20
|
-
setError(e instanceof Error ? e.message :
|
|
22
|
+
setError(e instanceof Error ? e.message : uiText("The change could not be completed. Please try again."));
|
|
21
23
|
}
|
|
22
24
|
finally {
|
|
23
25
|
setBusy(false);
|
|
24
26
|
} };
|
|
25
|
-
return _jsx(MessagingPopup, { visible: visible, onDismiss: dismissed, title: "Account", closeLabel: "Close account", closeDisabled: busy, background: c.background, contentWidth: 640, onClose: () => { if (!busy)
|
|
26
|
-
onClose(); }, children: _jsxs(ScrollView, { contentContainerStyle: { gap: 16, paddingBottom: 12 }, children: [_jsxs(View, { style: { backgroundColor: '#111111', borderRadius: 16, padding: 16, gap: 14 }, children: [_jsxs(View, { style: { flexDirection: 'row', alignItems: 'center', gap: 14 }, children: [account?.avatarUrl ? _jsx(Image, { source: { uri: account.avatarUrl }, style: { width: 56, height: 56, borderRadius: 28 } }) : _jsx(View, { style: { width: 56, height: 56, borderRadius: 28, backgroundColor: '#252525', alignItems: 'center', justifyContent: 'center' }, children: _jsx(MessageIcon, { name: "person-circle-outline", color: c.foreground, size: 36 }) }), _jsxs(View, { style: { flex: 1, gap: 4 }, children: [_jsx(Text, { style: { color: c.foreground, fontSize: 20, fontWeight: '600' }, children: account?.name ??
|
|
27
|
+
return _jsx(MessagingPopup, { visible: visible, onDismiss: dismissed, title: uiText("Account"), closeLabel: uiText("Close account"), closeDisabled: busy, background: c.background, contentWidth: 640, onClose: () => { if (!busy)
|
|
28
|
+
onClose(); }, children: _jsxs(ScrollView, { contentContainerStyle: { gap: 16, paddingBottom: 12 }, children: [_jsxs(View, { style: { backgroundColor: '#111111', borderRadius: 16, padding: 16, gap: 14 }, children: [_jsxs(View, { style: { flexDirection: 'row', alignItems: 'center', gap: 14 }, children: [account?.avatarUrl ? _jsx(Image, { source: { uri: account.avatarUrl }, style: { width: 56, height: 56, borderRadius: 28 } }) : _jsx(View, { style: { width: 56, height: 56, borderRadius: 28, backgroundColor: '#252525', alignItems: 'center', justifyContent: 'center' }, children: _jsx(MessageIcon, { name: "person-circle-outline", color: c.foreground, size: 36 }) }), _jsxs(View, { style: { flex: 1, gap: 4 }, children: [_jsx(Text, { style: { color: c.foreground, fontSize: 20, fontWeight: '600' }, children: account?.name ?? uiText("Your account") }), _jsx(Text, { style: { color: c.secondary, fontSize: 14 }, children: companyName })] })] }), account?.phone && _jsx(Text, { style: { color: c.foreground, fontSize: 16 }, children: account.phone }), account?.email && _jsx(Text, { style: { color: c.foreground, fontSize: 16 }, children: account.email }), account?.onEditProfile && _jsxs(Pressable, { accessibilityRole: "button", accessibilityLabel: uiText("Edit profile"), disabled: busy, onPress: () => begin(account.onEditProfile), style: { minHeight: 44, flexDirection: 'row', alignItems: 'center', gap: 10 }, children: [_jsx(MessageIcon, { name: "edit", color: c.foreground, size: 20 }), _jsx(Text, { style: { color: c.foreground, fontSize: 16 }, children: uiText("Edit profile") })] })] }), account?.onEnableNotifications && _jsx(Pressable, { accessibilityRole: "button", accessibilityLabel: uiText("Enable notifications"), disabled: busy, onPress: () => void run(account.onEnableNotifications), style: { backgroundColor: '#111111', borderRadius: 12, minHeight: 52, paddingHorizontal: 16, justifyContent: 'center' }, children: _jsx(Text, { style: { color: c.foreground, fontSize: 16 }, children: uiText("Enable notifications") }) }), account?.onSignOut && _jsx(Pressable, { accessibilityRole: "button", accessibilityLabel: uiText("Sign out"), disabled: busy, onPress: () => begin(account.onSignOut), style: { backgroundColor: '#111111', borderRadius: 12, minHeight: 52, paddingHorizontal: 16, justifyContent: 'center' }, children: _jsx(Text, { style: { color: c.foreground, fontSize: 16 }, children: uiText("Sign out") }) }), !!error && _jsx(Text, { accessibilityRole: "alert", style: { color: c.foreground, fontSize: 15 }, children: error })] }) });
|
|
27
29
|
}
|
package/dist/attachment-menu.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, useLayoutEffect, useMemo, useRef, useState } from 'react';
|
|
3
4
|
import { Animated, BackHandler, Platform, Pressable, ScrollView, Text, View, useWindowDimensions } from 'react-native';
|
|
4
5
|
import { MessagingPopupLayer, useMessagingPopupBounds } from './popup.js';
|
|
5
6
|
import { MessageIcon } from './icons.js';
|
|
6
7
|
/** Messenger's 206 × 46 attachment chips, staggered above its plus button. */
|
|
7
8
|
export function AttachmentMenu({ anchor, actions, accent, onClose }) {
|
|
9
|
+
const { text: uiText, locale: uiLocale, number: uiNumber, count: uiCount } = useRobonoLocale();
|
|
8
10
|
const bounds = useMessagingPopupBounds(), window = useWindowDimensions(), fade = useRef(new Animated.Value(0)).current;
|
|
9
11
|
const [position, setPosition] = useState(null);
|
|
10
12
|
const menu = useRef(null), close = useRef(onClose);
|
|
@@ -59,7 +61,7 @@ export function AttachmentMenu({ anchor, actions, accent, onClose }) {
|
|
|
59
61
|
const listHeight = Math.min(Math.max(46, position.height - 28), actions.length * 54 - 8);
|
|
60
62
|
const listTop = Math.max(14, Math.min(position.height - listHeight - 14, position.y - listHeight - 30));
|
|
61
63
|
const Container = compact ? ScrollView : View;
|
|
62
|
-
return _jsxs(View, { style: { position: 'absolute', inset: 0, zIndex: 1000, elevation: 1000 }, children: [_jsx(Animated.View, { pointerEvents: "none", style: { position: 'absolute', inset: 0, backgroundColor: '#000', opacity: fade.interpolate({ inputRange: [0, 1], outputRange: [0, .38] }) } }), _jsx(Pressable, { accessibilityLabel: "Close attachment menu", onPress: onClose, style: { position: 'absolute', inset: 0 } }), _jsx(View, { ref: menu, accessibilityViewIsModal: true, style: { position: 'absolute', inset: 0 }, pointerEvents: "box-none", children: _jsx(Container, { style: compact ? { position: 'absolute', top: listTop, left: 10, right: 10, maxHeight: listHeight } : { position: 'absolute', inset: 0 }, ...(compact ? { contentContainerStyle: { gap: 8, paddingHorizontal: 4 }, keyboardShouldPersistTaps: 'always' } : { pointerEvents: 'box-none' }), children: (compact ? [...actions].reverse() : actions).map((action, index) => {
|
|
64
|
+
return _jsxs(View, { style: { position: 'absolute', inset: 0, zIndex: 1000, elevation: 1000 }, children: [_jsx(Animated.View, { pointerEvents: "none", style: { position: 'absolute', inset: 0, backgroundColor: '#000', opacity: fade.interpolate({ inputRange: [0, 1], outputRange: [0, .38] }) } }), _jsx(Pressable, { accessibilityLabel: uiText("Close attachment menu"), onPress: onClose, style: { position: 'absolute', inset: 0 } }), _jsx(View, { ref: menu, accessibilityViewIsModal: true, style: { position: 'absolute', inset: 0 }, pointerEvents: "box-none", children: _jsx(Container, { style: compact ? { position: 'absolute', top: listTop, left: 10, right: 10, maxHeight: listHeight } : { position: 'absolute', inset: 0 }, ...(compact ? { contentContainerStyle: { gap: 8, paddingHorizontal: 4 }, keyboardShouldPersistTaps: 'always' } : { pointerEvents: 'box-none' }), children: (compact ? [...actions].reverse() : actions).map((action, index) => {
|
|
63
65
|
const chipWidth = Math.min(206, position.width - 20), step = Math.min(58, Math.max(46, (position.y - 80) / Math.max(1, actions.length - 1)));
|
|
64
66
|
return _jsx(Pressable, { accessibilityRole: "button", accessibilityLabel: action.label, onPress: () => { onClose(); if (Platform.OS === 'web')
|
|
65
67
|
action.run();
|
package/dist/call-message.js
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useRobonoLocale } from './localization.js';
|
|
2
3
|
import { useEffect, useRef, useState } from 'react';
|
|
3
4
|
import { Pressable, Text, View } from 'react-native';
|
|
4
5
|
import { useMessagingUI } from './index.js';
|
|
5
6
|
/** A new, reviewed message after no answer. Never records or sends a call automatically. */
|
|
6
7
|
export function UnansweredCallMessage({ callId, onDismiss }) {
|
|
8
|
+
const { text: uiText, locale: uiLocale, number: uiNumber, count: uiCount } = useRobonoLocale();
|
|
7
9
|
const { client, design, state, pickAttachment, releaseAttachment, components } = useMessagingUI();
|
|
8
10
|
const [option, setOption] = useState(null);
|
|
9
11
|
const [draft, setDraft] = useState(null);
|
|
@@ -27,7 +29,7 @@ export function UnansweredCallMessage({ callId, onDismiss }) {
|
|
|
27
29
|
void client.unansweredCall(callId).then(o => { if (id === generation.current)
|
|
28
30
|
setOption(o); })
|
|
29
31
|
.catch(() => { if (id === generation.current)
|
|
30
|
-
setError(
|
|
32
|
+
setError(uiText("Could not check message availability.")); });
|
|
31
33
|
return () => { generation.current++; const media = prepared.current; prepared.current = null; if (media)
|
|
32
34
|
void release.current?.(media); };
|
|
33
35
|
}, [client, callId, state.conversationId, attempt]);
|
|
@@ -66,7 +68,7 @@ export function UnansweredCallMessage({ callId, onDismiss }) {
|
|
|
66
68
|
}
|
|
67
69
|
catch {
|
|
68
70
|
if (id === generation.current)
|
|
69
|
-
setError(
|
|
71
|
+
setError(uiText("Could not prepare your message. Try again."));
|
|
70
72
|
}
|
|
71
73
|
finally {
|
|
72
74
|
inFlight.current = false;
|
|
@@ -99,7 +101,7 @@ export function UnansweredCallMessage({ callId, onDismiss }) {
|
|
|
99
101
|
}
|
|
100
102
|
catch {
|
|
101
103
|
if (id === generation.current)
|
|
102
|
-
setError(
|
|
104
|
+
setError(uiText("The message could not be sent. Check the conversation for a retry."));
|
|
103
105
|
}
|
|
104
106
|
finally {
|
|
105
107
|
inFlight.current = false;
|
|
@@ -111,5 +113,5 @@ export function UnansweredCallMessage({ callId, onDismiss }) {
|
|
|
111
113
|
return null;
|
|
112
114
|
const c = design.colors, VoiceAttachment = components?.VoiceAttachment;
|
|
113
115
|
const action = (label, onPress) => _jsx(Pressable, { accessibilityRole: "button", disabled: busy, onPress: onPress, style: { paddingVertical: 12, paddingHorizontal: 4, minHeight: 44 }, children: _jsx(Text, { style: { color: c.incomingText, fontWeight: '600' }, children: label }) });
|
|
114
|
-
return _jsxs(View, { style: { backgroundColor: c.incoming, padding: 16, borderTopWidth: 1, borderTopColor: c.secondary + '30', gap: 6 }, children: [_jsx(Text, { style: { color: c.incomingText, fontSize: 16, fontWeight: '600' }, children: "No answer" }), option && _jsx(Text, { style: { color: c.incomingText, fontSize: 14 }, children: draft ?
|
|
116
|
+
return _jsxs(View, { style: { backgroundColor: c.incoming, padding: 16, borderTopWidth: 1, borderTopColor: c.secondary + '30', gap: 6 }, children: [_jsx(Text, { style: { color: c.incomingText, fontSize: 16, fontWeight: '600' }, children: uiText("No answer") }), option && _jsx(Text, { style: { color: c.incomingText, fontSize: 14 }, children: draft ? uiText("Review your message, then send it.") : uiText("Leave a {value1} message in this chat.", { value1: option.kind === 'voice' ? 'voice' : 'video' }) }), draft && _jsx(View, { children: draft.kind === 'voice' && VoiceAttachment ? _jsx(VoiceAttachment, { media: draft.media, appearance: design, disabled: busy, onRemove: clearDraft }) : _jsx(Text, { style: { color: c.incomingText }, children: draft.kind === 'voice' ? uiText('Voice message ready') : uiText('Video message ready') }) }), _jsxs(View, { style: { flexDirection: 'row', gap: 16, flexWrap: 'wrap' }, children: [option && (draft ? action('Send message', () => void send()) : action(`Record ${option.kind === 'voice' ? 'voice' : 'video'} message`, () => void record())), !option && error && action('Try again', () => setAttempt(value => value + 1)), action('Not now', () => { clearDraft(); onDismiss(); })] }), !!error && _jsx(Text, { accessibilityRole: "alert", style: { color: c.incomingText }, children: error })] });
|
|
115
117
|
}
|
package/dist/calling.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useRobonoLocale } from './localization.js';
|
|
2
3
|
import { createContext, useContext, useSyncExternalStore } from 'react';
|
|
3
4
|
import { Pressable, View, Text } from 'react-native';
|
|
4
5
|
import { RobonoIcon } from './visual/RobonoIcon.js';
|
|
@@ -10,9 +11,10 @@ function CallingMessageComposer() { const follow = useContext(FollowUp); return
|
|
|
10
11
|
/** Optional call entry in the standard message header. The host controls eligibility and admission. */
|
|
11
12
|
export function MessagingCallHeader() { return _jsx(MessagingHeader, {}); }
|
|
12
13
|
export function MessagingCallingScreen({ onCall, onVideoCall, videoCallLabel = 'Video call', callLabel = 'Robono call', components, unansweredCallId, onDismissCallMessage, ...props }) {
|
|
14
|
+
const { text: uiText, locale: uiLocale, number: uiNumber, count: uiCount } = useRobonoLocale();
|
|
13
15
|
const state = useSyncExternalStore(props.client.subscribe, props.client.getSnapshot, props.client.getSnapshot);
|
|
14
16
|
const design = props.appearance ?? state.settings?.appearance;
|
|
15
17
|
const screenProps = { ...props, callActions: { ...props.callActions, ...(onCall ? { voice: onCall, voiceLabel: callLabel } : {}), ...(onVideoCall ? { video: onVideoCall, videoLabel: videoCallLabel } : {}) } };
|
|
16
18
|
const callingOnly = (onCall || onVideoCall) && design && state.settings && !state.settings.enabled;
|
|
17
|
-
return _jsx(FollowUp.Provider, { value: unansweredCallId && onDismissCallMessage ? { callId: unansweredCallId, dismiss: onDismissCallMessage } : null, children: callingOnly ? _jsx(MessagingProvider, { ...screenProps, children: _jsxs(View, { style: [{ flex: 1, backgroundColor: design.colors.background }, props.style], children: [_jsx(MessagingCallHeader, {}), _jsxs(View, { style: { flex: 1, alignItems: 'center', justifyContent: 'center', gap: 24, padding: 24 }, children: [_jsx(Text, { style: { fontSize: 28, fontWeight: '600', color: design.colors.foreground }, children: props.contact.name }), onCall && _jsxs(Pressable, { accessibilityRole: "button", accessibilityLabel:
|
|
19
|
+
return _jsx(FollowUp.Provider, { value: unansweredCallId && onDismissCallMessage ? { callId: unansweredCallId, dismiss: onDismissCallMessage } : null, children: callingOnly ? _jsx(MessagingProvider, { ...screenProps, children: _jsxs(View, { style: [{ flex: 1, backgroundColor: design.colors.background }, props.style], children: [_jsx(MessagingCallHeader, {}), _jsxs(View, { style: { flex: 1, alignItems: 'center', justifyContent: 'center', gap: 24, padding: 24 }, children: [_jsx(Text, { style: { fontSize: 28, fontWeight: '600', color: design.colors.foreground }, children: props.contact.name }), onCall && _jsxs(Pressable, { accessibilityRole: "button", accessibilityLabel: uiText("Start {value1}", { value1: callLabel.toLowerCase() }), onPress: onCall, style: { minHeight: 56, minWidth: 160, borderRadius: 28, backgroundColor: design.colors.incoming, alignItems: 'center', justifyContent: 'center', padding: 16, flexDirection: 'row', gap: 12 }, children: [_jsx(RobonoIcon, { name: "phone", size: 24, color: design.colors.incomingText }), _jsx(Text, { style: { fontSize: 18, color: design.colors.incomingText }, children: callLabel })] }), onVideoCall && _jsxs(Pressable, { accessibilityRole: "button", accessibilityLabel: uiText("Start {value1}", { value1: videoCallLabel.toLowerCase() }), onPress: onVideoCall, style: { minHeight: 56, padding: 16, borderRadius: 28, backgroundColor: design.colors.incoming, flexDirection: 'row', alignItems: 'center', gap: 12 }, children: [_jsx(RobonoIcon, { name: "video", size: 24, color: design.colors.incomingText }), _jsx(Text, { style: { color: design.colors.incomingText, fontSize: 18 }, children: videoCallLabel })] })] })] }) }) : _jsx(MessagingScreen, { ...screenProps, components: { Header: MessagingCallHeader, Composer: CallingMessageComposer, ...components } }) });
|
|
18
20
|
}
|
|
@@ -1,17 +1,20 @@
|
|
|
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 { ActivityIndicator, FlatList, Image, Platform, Pressable, Text, View } from 'react-native';
|
|
4
5
|
import { MessagingPopup, messagingPopupColors as c } from './popup.js';
|
|
5
6
|
import { MessageIcon, fileSize, duration } from './icons.js';
|
|
6
7
|
function MediaThumbnail({ message, preview }) {
|
|
8
|
+
const { text: uiText, locale: uiLocale, number: uiNumber, count: uiCount } = useRobonoLocale();
|
|
7
9
|
const [uri, setUri] = useState();
|
|
8
10
|
useEffect(() => { let live = true; setUri(undefined); if (preview && ['image', 'video'].includes(message.kind))
|
|
9
11
|
void preview(message).then(value => { if (live)
|
|
10
12
|
setUri(value.uri); }).catch(() => { }); return () => { live = false; }; }, [message.id, message.media?.assetId, preview]);
|
|
11
|
-
return _jsxs(View, { style: { width: 56, height: 56, borderRadius: 8, overflow: 'hidden', alignItems: 'center', justifyContent: 'center', backgroundColor: c.incoming }, children: [uri ? _jsx(Image, { source: { uri }, accessibilityLabel: message.media?.fileName ??
|
|
13
|
+
return _jsxs(View, { style: { width: 56, height: 56, borderRadius: 8, overflow: 'hidden', alignItems: 'center', justifyContent: 'center', backgroundColor: c.incoming }, children: [uri ? _jsx(Image, { source: { uri }, accessibilityLabel: message.media?.fileName ?? uiText("Attachment"), onError: () => setUri(undefined), style: { width: '100%', height: '100%' } }) : _jsx(MessageIcon, { name: message.kind === 'voice' ? 'mic' : message.kind === 'document' ? 'document' : message.kind === 'video' ? 'video' : 'image', color: c.foreground, size: 24 }), uri && message.kind === 'video' && _jsx(View, { style: { position: 'absolute', backgroundColor: '#0008', padding: 3, borderRadius: 20 }, children: _jsx(MessageIcon, { name: "play", color: "#fff", size: 18 }) })] });
|
|
12
14
|
}
|
|
13
15
|
/** Contact/group identity and retained shared media, using Messenger's bounded popup. */
|
|
14
16
|
export function ConversationDetails(props) {
|
|
17
|
+
const { text: uiText, locale: uiLocale, number: uiNumber, count: uiCount } = useRobonoLocale();
|
|
15
18
|
const { client, state, contact, participants = {}, userId, accent, onClose } = props, group = state.conversation?.kind === 'group';
|
|
16
19
|
const [tab, setTab] = useState('media'), [messages, setMessages] = useState([]), [members, setMembers] = useState([]);
|
|
17
20
|
const [next, setNext] = useState(null), [busy, setBusy] = useState(false), [error, setError] = useState(''), [visible, setVisible] = useState(true);
|
|
@@ -33,7 +36,7 @@ export function ConversationDetails(props) {
|
|
|
33
36
|
setVisible(false);
|
|
34
37
|
}
|
|
35
38
|
catch (e) {
|
|
36
|
-
setError(e instanceof Error ? e.message :
|
|
39
|
+
setError(e instanceof Error ? e.message : uiText("The group could not be left."));
|
|
37
40
|
}
|
|
38
41
|
finally {
|
|
39
42
|
setBusy(false);
|
|
@@ -64,7 +67,7 @@ export function ConversationDetails(props) {
|
|
|
64
67
|
}
|
|
65
68
|
catch (e) {
|
|
66
69
|
if (version === generation.current && !control.signal.aborted)
|
|
67
|
-
setError(e instanceof Error ? e.message :
|
|
70
|
+
setError(e instanceof Error ? e.message : uiText("These details are unavailable."));
|
|
68
71
|
}
|
|
69
72
|
finally {
|
|
70
73
|
if (version === generation.current)
|
|
@@ -73,11 +76,11 @@ export function ConversationDetails(props) {
|
|
|
73
76
|
}
|
|
74
77
|
useEffect(() => { setMessages([]); setMembers([]); setNext(null); if (tab === 'members' || state.sharedMediaAvailable)
|
|
75
78
|
void load(); return () => { ++generation.current; request.current?.abort(); }; }, [client, state.conversationId, tab]);
|
|
76
|
-
const footer = _jsxs(View, { children: [busy && _jsx(ActivityIndicator, { accessibilityLabel: "Loading conversation details", color: accent, style: { padding: 16 } }), !!error && _jsxs(_Fragment, { children: [_jsx(Text, { accessibilityRole: "alert", style: { color: c.foreground, padding: 12 }, children: error }), _jsx(Pressable, { accessibilityRole: "button", onPress: () => void load(!!next), style: { minHeight: 44, justifyContent: 'center' }, children: _jsx(Text, { style: { color: accent }, children: "Try again" }) })] }), next && !busy && !error && _jsx(Pressable, { accessibilityRole: "button", onPress: () => void load(true), style: { minHeight: 48, alignItems: 'center', justifyContent: 'center' }, children: _jsx(Text, { style: { color: accent }, children: "Load more" }) })] });
|
|
77
|
-
return _jsx(MessagingPopup, { visible: visible, onDismiss: dismiss, title: leaving ?
|
|
79
|
+
const footer = _jsxs(View, { children: [busy && _jsx(ActivityIndicator, { accessibilityLabel: uiText("Loading conversation details"), color: accent, style: { padding: 16 } }), !!error && _jsxs(_Fragment, { children: [_jsx(Text, { accessibilityRole: "alert", style: { color: c.foreground, padding: 12 }, children: error }), _jsx(Pressable, { accessibilityRole: "button", onPress: () => void load(!!next), style: { minHeight: 44, justifyContent: 'center' }, children: _jsx(Text, { style: { color: accent }, children: uiText("Try again") }) })] }), next && !busy && !error && _jsx(Pressable, { accessibilityRole: "button", onPress: () => void load(true), style: { minHeight: 48, alignItems: 'center', justifyContent: 'center' }, children: _jsx(Text, { style: { color: accent }, children: uiText("Load more") }) })] });
|
|
80
|
+
return _jsx(MessagingPopup, { visible: visible, onDismiss: dismiss, title: leaving ? uiText("Leave group?") : group ? uiText("Group details") : uiText("Contact details"), compact: leaving, closeDisabled: busy, background: c.background, onClose: () => { if (!busy) {
|
|
78
81
|
if (leaving)
|
|
79
82
|
setLeaving(false);
|
|
80
83
|
else
|
|
81
84
|
setVisible(false);
|
|
82
|
-
} }, children: leaving ? _jsxs(View, { style: { gap: 16, paddingTop: 12 }, children: [_jsxs(Text, { style: { color: c.secondary, fontSize: 16, lineHeight: 23 }, children: ["You will stop receiving this group
|
|
85
|
+
} }, children: leaving ? _jsxs(View, { style: { gap: 16, paddingTop: 12 }, children: [_jsxs(Text, { style: { color: c.secondary, fontSize: 16, lineHeight: 23 }, children: [uiText("You will stop receiving this group’s messages."), state.conversation?.ownerId === userId ? ' ' + uiText("Ownership will pass to another member.") : ''] }), !!error && _jsx(Text, { accessibilityRole: "alert", style: { color: c.foreground }, children: error }), _jsx(Pressable, { accessibilityRole: "button", disabled: busy, onPress: () => void leave(), style: { minHeight: 48, borderRadius: 12, backgroundColor: '#b72d34', alignItems: 'center', justifyContent: 'center' }, children: _jsx(Text, { style: { color: '#fff', fontSize: 16, fontWeight: '600' }, children: busy ? uiText("Leaving…") : uiText("Leave group") }) })] }) : _jsxs(_Fragment, { children: [_jsxs(View, { style: { alignItems: 'center', gap: 8, paddingVertical: 20 }, children: [!group && contact.avatarUrl ? _jsx(Image, { source: { uri: contact.avatarUrl }, style: { width: 64, height: 64, borderRadius: 32 } }) : _jsx(View, { style: { width: 64, height: 64, borderRadius: 32, backgroundColor: c.incoming, alignItems: 'center', justifyContent: 'center' }, children: _jsx(Text, { style: { fontSize: 26, color: accent }, children: title.slice(0, 1).toUpperCase() }) }), _jsx(Text, { style: { color: c.foreground, fontSize: 21, fontWeight: '600' }, children: title }), group && _jsx(Text, { style: { color: c.secondary, fontSize: 15 }, children: uiText('{count} members', { count: state.conversation?.memberCount ?? state.conversation?.memberIds?.length ?? 0 }) })] }), _jsx(View, { style: { flexDirection: 'row', gap: 8, paddingBottom: 12 }, children: ['media', ...(group ? ['members'] : [])].map(value => _jsx(Pressable, { accessibilityRole: "button", accessibilityLabel: value === 'media' ? uiText("Shared media") : uiText("Members"), accessibilityState: { selected: tab === value }, onPress: () => setTab(value), style: { flex: 1, minHeight: 44, alignItems: 'center', justifyContent: 'center', borderBottomWidth: 2, borderBottomColor: tab === value ? accent : 'transparent' }, children: _jsx(Text, { style: { color: tab === value ? accent : c.secondary, fontSize: 16, fontWeight: '600' }, children: value === 'media' ? uiText("Shared media") : uiText("Members") }) }, value)) }), tab === 'media' ? _jsx(FlatList, { data: messages, keyExtractor: m => m.id, style: { flex: 1 }, ListFooterComponent: footer, ListEmptyComponent: !busy && !error ? _jsx(Text, { style: { color: c.secondary, padding: 16, textAlign: 'center' }, children: state.sharedMediaAvailable ? uiText("No shared photos, videos, files or voice messages.") : uiText("Shared media is unavailable for this integration.") }) : null, renderItem: ({ item }) => _jsxs(Pressable, { accessibilityRole: "button", accessibilityLabel: uiText('Open {name}', { name: item.media?.fileName ?? uiText('Attachment') }), disabled: !props.openAttachment, onPress: () => { pending.current = () => { void props.openAttachment?.(item).catch(() => { }); }; setVisible(false); }, style: { minHeight: 76, flexDirection: 'row', alignItems: 'center', gap: 12, paddingVertical: 8, borderBottomWidth: 1, borderBottomColor: '#ffffff16' }, children: [_jsx(MediaThumbnail, { message: item, preview: props.attachmentPreview }), _jsxs(View, { style: { flex: 1, minWidth: 0, gap: 4 }, children: [_jsx(Text, { numberOfLines: 2, style: { color: c.foreground, fontSize: 16 }, children: item.media?.fileName ?? ({ image: 'Photo', video: 'Video', voice: 'Voice message', document: 'File' }[item.kind]) }), _jsxs(Text, { style: { color: c.secondary, fontSize: 13 }, children: [item.senderId === userId ? uiText("You") : participants[item.senderId]?.name ?? (!group ? contact.name : 'Member'), " \u00B7 ", item.media?.durationMs ? duration(item.media.durationMs) : fileSize(item.media?.byteSize), " \u00B7 ", new Date(item.createdAt).toLocaleDateString(uiLocale)] })] }), _jsx(MessageIcon, { name: "chevron-right", color: c.secondary, size: 20 })] }) }) : _jsx(FlatList, { data: members, keyExtractor: m => m.userId, style: { flex: 1 }, ListFooterComponent: footer, renderItem: ({ item }) => _jsxs(View, { style: { minHeight: 56, flexDirection: 'row', alignItems: 'center', gap: 12, borderBottomWidth: 1, borderBottomColor: '#ffffff16' }, children: [participants[item.userId]?.avatarUrl ? _jsx(Image, { source: { uri: participants[item.userId].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 }, children: (participants[item.userId]?.name ?? 'Member').slice(0, 1) }) }), _jsx(Text, { style: { flex: 1, color: c.foreground, fontSize: 16 }, children: item.userId === userId ? uiText("You") : participants[item.userId]?.name ?? uiText("Member") }), state.conversation?.ownerId === item.userId && _jsx(Text, { style: { color: accent, fontSize: 13 }, children: uiText("Owner") }), !item.canSend && _jsx(Text, { style: { color: c.secondary, fontSize: 13 }, children: uiText("Read only") })] }) }), group && state.conversation?.managementAvailable && _jsxs(View, { style: { flexDirection: 'row', gap: 12, borderTopWidth: 1, borderTopColor: '#ffffff20' }, children: [state.conversation.ownerId === userId && props.onEditGroup && _jsx(Pressable, { accessibilityRole: "button", onPress: () => { pending.current = props.onEditGroup; setVisible(false); }, style: { flex: 1, minHeight: 48, justifyContent: 'center', alignItems: 'center' }, children: _jsx(Text, { style: { color: accent, fontSize: 16 }, children: uiText("Edit group") }) }), _jsx(Pressable, { accessibilityRole: "button", onPress: () => { setError(''); setLeaving(true); }, style: { flex: 1, minHeight: 48, justifyContent: 'center', alignItems: 'center' }, children: _jsx(Text, { style: { color: '#ff8282', fontSize: 16 }, children: uiText("Leave group") }) })] })] }) });
|
|
83
86
|
}
|
|
@@ -11,6 +11,9 @@ export declare function createExpoMessengerCalling(options: {
|
|
|
11
11
|
}>;
|
|
12
12
|
inbox: MessagingInboxClient;
|
|
13
13
|
}): {
|
|
14
|
+
systemNotifications: import("@robono/calling-react-native/system-calling").SystemCallNotifications;
|
|
15
|
+
notification(data: unknown, opened?: boolean): Promise<boolean>;
|
|
16
|
+
dispose(): Promise<void>;
|
|
14
17
|
updateContacts(next: Record<string, {
|
|
15
18
|
name: string;
|
|
16
19
|
avatarUrl?: string;
|
|
@@ -19,11 +22,10 @@ export declare function createExpoMessengerCalling(options: {
|
|
|
19
22
|
getSnapshot: () => MessengerCallingState;
|
|
20
23
|
subscribe(fn: () => void): () => void;
|
|
21
24
|
isActive: () => boolean;
|
|
25
|
+
reportError(message: string): void;
|
|
22
26
|
dismissUnanswered(): void;
|
|
23
|
-
notification(data: unknown, opened?: boolean): Promise<boolean>;
|
|
24
27
|
prime(): void;
|
|
25
28
|
call(conversationId: string, kind: "voice" | "video", group?: boolean): Promise<void>;
|
|
26
|
-
dispose(): Promise<void>;
|
|
27
29
|
};
|
|
28
30
|
export declare function useExpoMessengerCalling(calling?: MessengerCalling): MessengerCallingState;
|
|
29
31
|
export declare function ExpoMessengerCalls({ calling, state }: {
|
|
@@ -1,16 +1,20 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { callingText } from '@robono/calling-react-native/appearance';
|
|
3
|
+
import { useRobonoLocale } from './localization.js';
|
|
2
4
|
import { useEffect, useSyncExternalStore } from 'react';
|
|
3
5
|
import { Pressable, Text, View } from 'react-native';
|
|
4
6
|
import { setAudioModeAsync } from 'expo-audio';
|
|
5
7
|
import { createMessengerCalling } from '@robono/calling-react-native/messenger';
|
|
6
8
|
import { createNativeCallAdapters } from '@robono/calling-react-native/native';
|
|
7
9
|
import { createNativeCallRuntime } from '@robono/calling-react-native/runtime';
|
|
10
|
+
import { createNativeSystemCalling } from '@robono/calling-react-native/system-calling';
|
|
8
11
|
import { NativeCallingOverlay, NativeGroupCallingOverlay } from '@robono/calling-react-native/native-video';
|
|
9
12
|
import { createMessagingTransport } from '@robono/messaging';
|
|
10
13
|
export function createExpoMessengerCalling(options) {
|
|
11
14
|
const url = new URL(options.endpoint);
|
|
12
15
|
url.searchParams.set('service', 'calling');
|
|
13
16
|
let people = {};
|
|
17
|
+
const system = createNativeSystemCalling({ start: () => calling.start(), onError: () => calling.reportError('The incoming call could not connect. Open the app and check microphone access and your connection.') });
|
|
14
18
|
const calling = createMessengerCalling({
|
|
15
19
|
request: createMessagingTransport({ ...options, url: url.href }),
|
|
16
20
|
resolveContact: id => {
|
|
@@ -22,12 +26,22 @@ export function createExpoMessengerCalling(options) {
|
|
|
22
26
|
},
|
|
23
27
|
resolveMember: (_app, id) => { const person = (options.people?.() ?? people)[id]; return person ? { name: person.name, avatarUri: person.avatarUrl } : null; },
|
|
24
28
|
adapters: appearance => {
|
|
29
|
+
system.updateAppearance(appearance);
|
|
30
|
+
let controller;
|
|
25
31
|
const runtime = createNativeCallRuntime({ notificationTitle: 'Call in progress', notificationText: 'Tap to return to your call', channelName: 'Calls', endLabel: 'End call', onError: () => { } });
|
|
26
|
-
const adapter = createNativeCallAdapters({ appearance, runtime, ringtone: 'robono', onError: () => { }, restoreAudio: () => setAudioModeAsync({ allowsRecording: false, playsInSilentMode: true, shouldPlayInBackground: false, shouldRouteThroughEarpiece: false, interruptionMode: 'mixWithOthers' }) });
|
|
27
|
-
return { ...adapter, attach
|
|
32
|
+
const adapter = createNativeCallAdapters({ appearance, runtime, systemCalling: system.audio(() => controller), ringtone: 'robono', onError: () => { }, restoreAudio: () => setAudioModeAsync({ allowsRecording: false, playsInSilentMode: true, shouldPlayInBackground: false, shouldRouteThroughEarpiece: false, interruptionMode: 'mixWithOthers' }) });
|
|
33
|
+
return { ...adapter, attach(value) { controller = value; const a = runtime.attach(value), b = system.attach(value); return () => { b(); a(); controller = undefined; }; }, dispose: runtime.dispose };
|
|
28
34
|
},
|
|
29
35
|
});
|
|
30
|
-
return { ...calling,
|
|
36
|
+
return { ...calling, systemNotifications: system.notifications,
|
|
37
|
+
async notification(data, opened = false) { await system.receive(data); return calling.notification(data, opened); },
|
|
38
|
+
async dispose() { await system.dispose(); await calling.dispose(); },
|
|
39
|
+
updateContacts(next) { people = next; const names = {}; for (const row of options.inbox.getSnapshot().conversations) {
|
|
40
|
+
const person = next[row.contactId ?? ''];
|
|
41
|
+
const name = row.kind === 'group' ? row.name : person?.name;
|
|
42
|
+
if (name)
|
|
43
|
+
names[row.id] = name;
|
|
44
|
+
} system.cacheContacts(names); } };
|
|
31
45
|
}
|
|
32
46
|
const absent = { direct: null, group: null, loading: false, error: '', features: { voice: false, video: false } };
|
|
33
47
|
const noSubscribe = () => () => { };
|
|
@@ -37,5 +51,6 @@ export function useExpoMessengerCalling(calling) {
|
|
|
37
51
|
return state;
|
|
38
52
|
}
|
|
39
53
|
export function ExpoMessengerCalls({ calling, state }) {
|
|
40
|
-
|
|
54
|
+
const { text: uiText, locale: uiLocale, number: uiNumber, count: uiCount } = useRobonoLocale();
|
|
55
|
+
return _jsxs(_Fragment, { children: [state.error && _jsxs(View, { style: { backgroundColor: '#171717', paddingHorizontal: 16, paddingVertical: 8, flexDirection: 'row', gap: 12, alignItems: 'center' }, children: [_jsx(Text, { accessibilityRole: "alert", style: { color: '#FAFAFA', flex: 1 }, children: state.error }), _jsx(Pressable, { accessibilityRole: "button", onPress: () => void calling?.start().catch(() => { }), style: { padding: 8 }, children: _jsx(Text, { style: { color: '#FF8A2C' }, children: uiText("Retry calls") }) })] }), state.direct && _jsx(NativeCallingOverlay, { controller: state.direct, appearance: state.appearance, labels: Object.fromEntries(Object.entries({ ...callingText, ...state.appearance?.labels }).map(([key, value]) => [key, uiText(value)])) }), state.group && _jsx(NativeGroupCallingOverlay, { controller: state.group, appearance: state.appearance, labels: Object.fromEntries(Object.entries({ ...callingText, ...state.appearance?.labels }).map(([key, value]) => [key, uiText(value)])) })] });
|
|
41
56
|
}
|
package/dist/expo-extras.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { RobonoLocationLocaleProvider } from '@robono/location-react-native';
|
|
3
|
+
import { useRobonoLocale } from './localization.js';
|
|
2
4
|
import { prepareBackgroundTracking, captureBackgroundTracking, endBackgroundAccount } from './expo-tracking-task.js';
|
|
3
5
|
import * as Location from 'expo-location';
|
|
4
6
|
import Constants from 'expo-constants';
|
|
@@ -25,7 +27,8 @@ export function createExpoMessagingExtras(options) {
|
|
|
25
27
|
stop(); }).catch(() => { stop(); onError(); }); },
|
|
26
28
|
});
|
|
27
29
|
function ConnectedTrackingMap(props) {
|
|
28
|
-
|
|
30
|
+
const { configuration } = useRobonoLocale();
|
|
31
|
+
return _jsx(RobonoLocationLocaleProvider, { value: configuration, children: _jsx(ManagedMap, { client: extras, children: onLoad => _jsx(TrackingMap, { ...props, onLoad: onLoad }) }) });
|
|
29
32
|
}
|
|
30
33
|
return { extras, TrackingMap: ConnectedTrackingMap, async signOut() { extras.dispose(); const results = await Promise.allSettled([request.cancelUnfinished(), endBackgroundAccount(url.href, options.userId)]); const failed = results.find(r => r.status === 'rejected'); if (failed?.status === 'rejected')
|
|
31
34
|
throw failed.reason; }, async dispose() { extras.dispose(); await endBackgroundAccount(url.href, options.userId); } };
|
package/dist/expo-media.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { MessagingError } from '@robono/messaging';
|
|
1
2
|
import { awaitMediaScreening } from './media-screening';
|
|
2
3
|
import { AudioModule, RecordingPresets, createAudioPlayer, setAudioModeAsync } from 'expo-audio';
|
|
3
4
|
import * as Picker from 'expo-image-picker';
|
|
@@ -252,7 +253,7 @@ export function createExpoMessagingStorage(options) {
|
|
|
252
253
|
async function request(operation, input, signal) {
|
|
253
254
|
const token = await options.getAccessToken();
|
|
254
255
|
if (!token)
|
|
255
|
-
throw
|
|
256
|
+
throw new MessagingError('Sign in to continue.', 'authentication_required', 401);
|
|
256
257
|
const bounded = new AbortController();
|
|
257
258
|
const cancel = () => bounded.abort();
|
|
258
259
|
signal?.addEventListener('abort', cancel, { once: true });
|
|
@@ -263,11 +264,19 @@ export function createExpoMessagingStorage(options) {
|
|
|
263
264
|
const response = await (options.fetch ?? fetch)(endpoint, { method: 'POST', redirect: 'error', signal: bounded.signal, headers: { 'content-type': 'application/json', authorization: `Bearer ${token}` }, body: JSON.stringify({ ...input, operation }) });
|
|
264
265
|
const result = await response.json();
|
|
265
266
|
if (!response.ok || result?.error) {
|
|
266
|
-
|
|
267
|
-
throw error;
|
|
267
|
+
throw new MessagingError(result?.error?.message ?? 'The file could not be transferred. Try again.', result?.error?.code ?? 'media_unavailable', response.status);
|
|
268
268
|
}
|
|
269
269
|
return result;
|
|
270
270
|
}
|
|
271
|
+
catch (error) {
|
|
272
|
+
if (signal?.aborted)
|
|
273
|
+
throw error;
|
|
274
|
+
if (bounded.signal.aborted)
|
|
275
|
+
throw new MessagingError('Connection timed out.', 'request_timeout');
|
|
276
|
+
if (error instanceof TypeError)
|
|
277
|
+
throw new MessagingError('Connect to send your attachment.', 'network_unavailable');
|
|
278
|
+
throw error;
|
|
279
|
+
}
|
|
271
280
|
finally {
|
|
272
281
|
clearTimeout(timeout);
|
|
273
282
|
signal?.removeEventListener('abort', cancel);
|
|
@@ -319,11 +328,24 @@ export function createExpoMessagingStorage(options) {
|
|
|
319
328
|
if (plan.media)
|
|
320
329
|
return plan.media;
|
|
321
330
|
const task = FS.createUploadTask(https(plan.url), uri, { httpMethod: 'PUT', uploadType: FS.FileSystemUploadType.BINARY_CONTENT, headers: plan.headers ?? {}, sessionType: FS.FileSystemSessionType.FOREGROUND }, p => context.onProgress(p.totalBytesExpectedToSend > 0 ? p.totalBytesSent / p.totalBytesExpectedToSend : 0));
|
|
322
|
-
const result = await cancellable(task, context, () =>
|
|
331
|
+
const result = await cancellable(task, context, async () => {
|
|
332
|
+
try {
|
|
333
|
+
return await task.uploadAsync();
|
|
334
|
+
}
|
|
335
|
+
catch (error) {
|
|
336
|
+
check(context.signal);
|
|
337
|
+
// Native upload failures are not JavaScript TypeErrors. Preserve local
|
|
338
|
+
// file/permission errors, but allow the durable queue to retry network loss.
|
|
339
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
340
|
+
if (/(?:network (?:request failed|is unreachable|connection)|not connected to the internet|internet connection appears to be offline|unknownhost|unable to resolve host|failed to connect|connection (?:reset|refused|timed out)|sockettimeout|timed? ?out|NSURLErrorDomain.*(?:-1001|-1005|-1009)|ENETUNREACH|ECONNRESET)/i.test(detail))
|
|
341
|
+
throw new MessagingError('Connect to send your attachment.', 'network_unavailable');
|
|
342
|
+
throw error;
|
|
343
|
+
}
|
|
344
|
+
});
|
|
323
345
|
// An immutable PUT may already have succeeded before its response was lost.
|
|
324
346
|
// A 412 is not success by itself: complete still verifies ownership and bytes.
|
|
325
347
|
if (!result || result.status !== 412 && (result.status < 200 || result.status >= 300))
|
|
326
|
-
throw
|
|
348
|
+
throw new MessagingError('The attachment upload was interrupted.', 'upload_interrupted', result?.status ?? 503);
|
|
327
349
|
// Verification is repeatable for the same upload. A transient inspector
|
|
328
350
|
// failure must not upload the bytes again or create a second attachment.
|
|
329
351
|
for (let attempt = 0;; attempt++) {
|
package/dist/expo-messenger.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createExpoMessengerCalling } from './expo-connected-calling.js';
|
|
2
2
|
import type { LocationWorkspace } from '@robono/location';
|
|
3
3
|
import React from 'react';
|
|
4
|
-
import { MessagingInboxClient, type ConversationSummary } from '@robono/messaging';
|
|
4
|
+
import { type HostedRobonoConnectionOptions, MessagingInboxClient, type ConversationSummary } from '@robono/messaging';
|
|
5
5
|
import { type ExpoMessagingScreenProps } from './expo-screen.js';
|
|
6
6
|
import { type MessagingAudioSession } from './expo-media.js';
|
|
7
7
|
/** One account, one authenticated route, supplied storage and media-session adapters. */
|
|
@@ -38,6 +38,9 @@ export declare function createExpoMessenger(options: {
|
|
|
38
38
|
enable(): Promise<void>;
|
|
39
39
|
} | undefined;
|
|
40
40
|
calling: {
|
|
41
|
+
systemNotifications: import("@robono/calling-react-native/system-calling").SystemCallNotifications;
|
|
42
|
+
notification(data: unknown, opened?: boolean): Promise<boolean>;
|
|
43
|
+
dispose(): Promise<void>;
|
|
41
44
|
updateContacts(next: Record<string, {
|
|
42
45
|
name: string;
|
|
43
46
|
avatarUrl?: string;
|
|
@@ -46,14 +49,49 @@ export declare function createExpoMessenger(options: {
|
|
|
46
49
|
getSnapshot: () => import("@robono/calling-react-native/messenger").MessengerCallingState;
|
|
47
50
|
subscribe(fn: () => void): () => void;
|
|
48
51
|
isActive: () => boolean;
|
|
52
|
+
reportError(message: string): void;
|
|
49
53
|
dismissUnanswered(): void;
|
|
50
|
-
notification(data: unknown, opened?: boolean): Promise<boolean>;
|
|
51
54
|
prime(): void;
|
|
52
55
|
call(conversationId: string, kind: "voice" | "video", group?: boolean): Promise<void>;
|
|
53
|
-
dispose(): Promise<void>;
|
|
54
56
|
} | undefined;
|
|
55
57
|
maps: LocationWorkspace | undefined;
|
|
56
58
|
};
|
|
59
|
+
export declare function createHostedExpoMessenger(options: Omit<Parameters<typeof createExpoMessenger>[0], 'endpoint' | 'getAccessToken' | 'namespace'> & HostedRobonoConnectionOptions & {
|
|
60
|
+
signal?: AbortSignal;
|
|
61
|
+
}): Promise<{
|
|
62
|
+
signOut(): Promise<void>;
|
|
63
|
+
dispose(): void;
|
|
64
|
+
extras?: import("@robono/messaging").MessagingExtrasClient | undefined;
|
|
65
|
+
components?: {
|
|
66
|
+
TrackingMap: (props: import("./extras.js").TrackingMapProps) => React.JSX.Element;
|
|
67
|
+
} | undefined;
|
|
68
|
+
client: import("@robono/messaging").MessagingClient;
|
|
69
|
+
inbox: MessagingInboxClient;
|
|
70
|
+
storage: import("./media-controller.js").MessagingMediaStorage;
|
|
71
|
+
audioSession: MessagingAudioSession;
|
|
72
|
+
notifications: {
|
|
73
|
+
start(open: (conversationId: string, messageId: string) => Promise<void>): Promise<void>;
|
|
74
|
+
enable(): Promise<void>;
|
|
75
|
+
} | undefined;
|
|
76
|
+
calling: {
|
|
77
|
+
systemNotifications: import("@robono/calling-react-native/system-calling").SystemCallNotifications;
|
|
78
|
+
notification(data: unknown, opened?: boolean): Promise<boolean>;
|
|
79
|
+
dispose(): Promise<void>;
|
|
80
|
+
updateContacts(next: Record<string, {
|
|
81
|
+
name: string;
|
|
82
|
+
avatarUrl?: string;
|
|
83
|
+
}>): void;
|
|
84
|
+
start: () => Promise<void>;
|
|
85
|
+
getSnapshot: () => import("@robono/calling-react-native/messenger").MessengerCallingState;
|
|
86
|
+
subscribe(fn: () => void): () => void;
|
|
87
|
+
isActive: () => boolean;
|
|
88
|
+
reportError(message: string): void;
|
|
89
|
+
dismissUnanswered(): void;
|
|
90
|
+
prime(): void;
|
|
91
|
+
call(conversationId: string, kind: "voice" | "video", group?: boolean): Promise<void>;
|
|
92
|
+
} | undefined;
|
|
93
|
+
maps: LocationWorkspace | undefined;
|
|
94
|
+
}>;
|
|
57
95
|
export interface ExpoMessengerProps extends Omit<ExpoMessagingScreenProps, 'contact' | 'renderConversation' | 'onBack'> {
|
|
58
96
|
calling?: ReturnType<typeof createExpoMessengerCalling>;
|
|
59
97
|
maps?: LocationWorkspace;
|
|
@@ -72,4 +110,4 @@ export interface ExpoMessengerProps extends Omit<ExpoMessagingScreenProps, 'cont
|
|
|
72
110
|
onSelectConversation?: (conversation: ConversationSummary | null) => void;
|
|
73
111
|
}
|
|
74
112
|
/** Supplied inbox, group creation, conversation, settings and media screens. */
|
|
75
|
-
export declare function ExpoMessenger(
|
|
113
|
+
export declare function ExpoMessenger(props: ExpoMessengerProps): React.JSX.Element;
|
package/dist/expo-messenger.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { RobonoLocaleProvider, useRobonoLocale } from './localization.js';
|
|
2
3
|
import { useMessengerMaps, MessengerMapsNotice } from './maps-connection.js';
|
|
3
4
|
import { MessagingCallingScreen } from './calling.js';
|
|
4
5
|
import { createExpoMessengerCalling, useExpoMessengerCalling, ExpoMessengerCalls } from './expo-connected-calling.js';
|
|
@@ -8,9 +9,9 @@ import { createExpoMaps, RobonoMaps } from '@robono/location-react-native/expo';
|
|
|
8
9
|
import { createExpoMessagingExtras } from './expo-extras.js';
|
|
9
10
|
import { connectExpoMessagingNotifications } from './expo-notifications.js';
|
|
10
11
|
import { useEffect, useState, useSyncExternalStore } from 'react';
|
|
11
|
-
import { createMessagingTransport, defaultMessagingSettings, MessagingInboxClient } from '@robono/messaging';
|
|
12
|
+
import { connectHostedRobono, createMessagingTransport, defaultMessagingSettings, MessagingInboxClient } from '@robono/messaging';
|
|
12
13
|
import { ExpoMessagingScreen } from './expo-screen.js';
|
|
13
|
-
import { createExpoMessagingClient } from './expo-outbox.js';
|
|
14
|
+
import { createExpoMessagingClient, expoOutbox } from './expo-outbox.js';
|
|
14
15
|
import { createExpoMessagingAudioSession, createExpoMessagingStorage } from './expo-media.js';
|
|
15
16
|
import { MessagingProvider, MessagingAccountMenu } from './index.js';
|
|
16
17
|
import { MessagingInbox } from './inbox.js';
|
|
@@ -27,6 +28,21 @@ export function createExpoMessenger(options) {
|
|
|
27
28
|
const connectedExtras = options.extras === false ? undefined : createExpoMessagingExtras(options);
|
|
28
29
|
const inbox = new MessagingInboxClient({ request: createMessagingTransport({ ...options, url: endpoint.href }) });
|
|
29
30
|
const storage = createExpoMessagingStorage({ url: media.href, getAccessToken: options.getAccessToken, ...(options.fetch ? { fetch: options.fetch } : {}) });
|
|
31
|
+
const outgoing = expoOutbox(client);
|
|
32
|
+
if (outgoing)
|
|
33
|
+
client.setRecoveryAdapter(worker => {
|
|
34
|
+
const abort = new AbortController(), conversationId = worker.getSnapshot().conversationId;
|
|
35
|
+
const release = worker.setAttachmentPreparer(async (content) => {
|
|
36
|
+
if (!content.media || content.kind === 'text' || content.kind === 'poll')
|
|
37
|
+
throw Error('The saved attachment is unavailable.');
|
|
38
|
+
const file = await outgoing.file(conversationId, content.media.assetId);
|
|
39
|
+
if (!file)
|
|
40
|
+
throw Error('The saved attachment is missing. Remove it and choose it again.');
|
|
41
|
+
const media = await storage.upload(file, { conversationId, uploadId: content.media.assetId, kind: content.kind, signal: abort.signal, onProgress: () => { } });
|
|
42
|
+
return { ...content, media };
|
|
43
|
+
});
|
|
44
|
+
return () => { abort.abort(); release(); };
|
|
45
|
+
});
|
|
30
46
|
const calling = options.calling === false ? undefined : createExpoMessengerCalling({ ...options, inbox });
|
|
31
47
|
const audioSession = options.audioSession ?? createExpoMessagingAudioSession({ isCallActive: () => isNativeCallAudioActive() || calling?.isActive() === true || options.isCallActive?.() === true });
|
|
32
48
|
let connection = null, pending = null, closed = false;
|
|
@@ -49,7 +65,7 @@ export function createExpoMessenger(options) {
|
|
|
49
65
|
catch {
|
|
50
66
|
return false;
|
|
51
67
|
} },
|
|
52
|
-
onCall: calling?.notification, open: e => open(e.conversationId, e.messageId), refresh: () => inbox.refresh().then(() => { }), onError: () => { },
|
|
68
|
+
onCall: calling?.notification, systemCalls: calling?.systemNotifications, open: e => open(e.conversationId, e.messageId), refresh: () => inbox.refresh().then(() => { }), onError: () => { },
|
|
53
69
|
});
|
|
54
70
|
if (closed) {
|
|
55
71
|
await n.signOut();
|
|
@@ -70,13 +86,47 @@ export function createExpoMessenger(options) {
|
|
|
70
86
|
throw Error('Allow notifications in your phone’s app settings.'); },
|
|
71
87
|
};
|
|
72
88
|
return { client, inbox, storage, audioSession, notifications, calling, maps: mapConnection?.workspace, ...(connectedExtras ? { extras: connectedExtras.extras, components: { TrackingMap: connectedExtras.TrackingMap } } : {}),
|
|
73
|
-
async signOut() {
|
|
89
|
+
async signOut() { if (closed)
|
|
90
|
+
return; closed = true; const extraCleanup = connectedExtras?.signOut(); inbox.dispose(); const results = await Promise.allSettled([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
91
|
throw failed.reason; },
|
|
75
|
-
dispose() {
|
|
92
|
+
dispose() { if (closed)
|
|
93
|
+
return; closed = true; void calling?.dispose().catch(() => { }); mapConnection?.dispose(); void connectedExtras?.dispose().catch(() => { }); connection?.dispose(); inbox.dispose(); client.dispose(); },
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
export async function createHostedExpoMessenger(options) {
|
|
97
|
+
const session = await connectHostedRobono(options);
|
|
98
|
+
if (options.signal?.aborted) {
|
|
99
|
+
await session.signOut();
|
|
100
|
+
throw Error('The messenger was closed.');
|
|
101
|
+
}
|
|
102
|
+
let connection;
|
|
103
|
+
try {
|
|
104
|
+
connection = createExpoMessenger({ ...options, endpoint: session.endpoint, getAccessToken: session.getAccessToken, namespace: session.namespace });
|
|
105
|
+
}
|
|
106
|
+
catch (error) {
|
|
107
|
+
await session.signOut().catch(() => { });
|
|
108
|
+
throw error;
|
|
109
|
+
}
|
|
110
|
+
let signingOut;
|
|
111
|
+
return { ...connection,
|
|
112
|
+
signOut() { return signingOut ??= (async () => { try {
|
|
113
|
+
await connection.signOut();
|
|
114
|
+
}
|
|
115
|
+
finally {
|
|
116
|
+
await session.signOut();
|
|
117
|
+
} })(); },
|
|
118
|
+
dispose() { if (!signingOut) {
|
|
119
|
+
connection.dispose();
|
|
120
|
+
session.dispose();
|
|
121
|
+
} },
|
|
76
122
|
};
|
|
77
123
|
}
|
|
78
124
|
/** Supplied inbox, group creation, conversation, settings and media screens. */
|
|
79
|
-
export function ExpoMessenger(
|
|
125
|
+
export function ExpoMessenger(props) {
|
|
126
|
+
return _jsx(RobonoLocaleProvider, { value: props.interfaceLocale, children: _jsx(ExpoMessengerContent, { ...props }) });
|
|
127
|
+
}
|
|
128
|
+
function ExpoMessengerContent({ inbox, contacts = {}, initialConversation, selectedConversation, onSelectConversation, notifications, maps, calling, ...props }) {
|
|
129
|
+
const { configuration: mapsLocale } = useRobonoLocale();
|
|
80
130
|
const callState = useExpoMessengerCalling(calling);
|
|
81
131
|
useEffect(() => { calling?.updateContacts(contacts); }, [calling, contacts]);
|
|
82
132
|
const chatState = useSyncExternalStore(props.client.subscribe, props.client.getSnapshot, props.client.getSnapshot);
|
|
@@ -97,5 +147,5 @@ export function ExpoMessenger({ inbox, contacts = {}, initialConversation, selec
|
|
|
97
147
|
open(row, message); }).catch(() => { }); }, [notifications, inbox]);
|
|
98
148
|
const account = notifications ? { name: props.account?.name ?? 'Your account', ...props.account, onEnableNotifications: () => notifications.enable() } : props.account;
|
|
99
149
|
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 })] });
|
|
150
|
+
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, { 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 })] });
|
|
101
151
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import * as Notifications from 'expo-notifications';
|
|
2
|
+
import type { SystemCallNotifications } from '@robono/calling-react-native/system-calling';
|
|
2
3
|
import { type MessagingNotification } from '@robono/messaging';
|
|
3
4
|
export interface ExpoMessagingNotificationOptions {
|
|
4
5
|
/** The combined server route, with service=notifications. */
|
|
@@ -11,6 +12,7 @@ export interface ExpoMessagingNotificationOptions {
|
|
|
11
12
|
onError: (error: Error) => void;
|
|
12
13
|
fetch?: typeof fetch;
|
|
13
14
|
onCall?: (data: unknown, opened: boolean) => Promise<boolean>;
|
|
15
|
+
systemCalls?: SystemCallNotifications;
|
|
14
16
|
}
|
|
15
17
|
/** One restored account. Never prompts on mount, replaces a global handler or opens an unverified call. */
|
|
16
18
|
export declare function connectExpoMessagingNotifications(options: ExpoMessagingNotificationOptions): Promise<{
|