@robono/messaging-react-native 0.9.0 → 0.11.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 +61 -0
- package/SBOM.spdx.json +498 -210
- package/dist/account.d.ts +3 -0
- package/dist/account.js +15 -4
- package/dist/attachment-menu.js +2 -1
- package/dist/call-message.js +2 -1
- package/dist/calling.js +4 -2
- package/dist/conversation-details.d.ts +1 -0
- package/dist/conversation-details.js +3 -2
- 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.d.ts +8 -0
- package/dist/expo-connected-calling.js +6 -3
- package/dist/expo-messenger.d.ts +20 -1
- package/dist/expo-messenger.js +37 -9
- package/dist/expo-screen.js +20 -18
- package/dist/expo-social.js +2 -1
- package/dist/extras.js +2 -1
- package/dist/forward.js +2 -1
- package/dist/group-editor.js +2 -1
- package/dist/header-calls.js +2 -1
- package/dist/icons.js +2 -1
- package/dist/inbox-search.d.ts +3 -1
- package/dist/inbox-search.js +30 -9
- 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 +24 -17
- package/dist/index.d.ts +10 -3
- package/dist/index.js +40 -29
- package/dist/maps-connection.js +2 -1
- package/dist/media-ui.js +2 -1
- package/dist/message-menu.js +2 -1
- package/dist/native-outbox.d.ts +1 -1
- package/dist/polls.js +4 -3
- package/dist/popup.js +5 -3
- package/dist/public-rooms.d.ts +10 -0
- package/dist/public-rooms.js +78 -0
- package/dist/quick-send.js +2 -1
- package/dist/room-moderation.d.ts +22 -0
- package/dist/room-moderation.js +87 -0
- package/dist/safety.js +2 -1
- package/dist/saved.js +2 -1
- package/dist/scheduled.js +2 -1
- package/dist/search.js +2 -1
- package/dist/social.js +2 -1
- package/dist/translation-settings.js +2 -1
- package/dist/typography.d.ts +12 -0
- package/dist/typography.js +31 -0
- package/dist/voice-composer.js +2 -1
- package/package.json +6 -6
package/dist/account.d.ts
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
|
+
import { type CallDeviceCheck } from '@robono/calling-react-native';
|
|
1
2
|
import React from 'react';
|
|
2
3
|
/** Profile and account actions belong to the customer's sign-in system. */
|
|
3
4
|
export interface MessagingAccount {
|
|
5
|
+
deviceCheck?: CallDeviceCheck;
|
|
6
|
+
renderCallPreview?: (stream: MediaStream) => React.ReactNode;
|
|
4
7
|
name: string;
|
|
5
8
|
avatarUrl?: string;
|
|
6
9
|
phone?: string;
|
package/dist/account.js
CHANGED
|
@@ -1,11 +1,16 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { CallDeviceCheckPanel, callDeviceCheckText } from '@robono/calling-react-native';
|
|
3
|
+
import { Text } from './typography.js';
|
|
2
4
|
import { useRobonoLocale } from './localization.js';
|
|
3
5
|
import { useEffect, useRef, useState } from 'react';
|
|
4
|
-
import { Image, Platform, Pressable, ScrollView,
|
|
6
|
+
import { AppState, Image, Platform, Pressable, ScrollView, View } from 'react-native';
|
|
5
7
|
import { MessagingPopup, messagingPopupColors as c } from './popup.js';
|
|
6
8
|
import { MessageIcon } from './icons.js';
|
|
7
9
|
export function MessagingAccountScreen({ account, companyName, onClose }) {
|
|
8
10
|
const { text: uiText, locale: uiLocale, number: uiNumber, count: uiCount } = useRobonoLocale();
|
|
11
|
+
const [checking, setChecking] = useState(false);
|
|
12
|
+
useEffect(() => { const sub = AppState.addEventListener('change', value => { if (value !== 'active')
|
|
13
|
+
void account?.deviceCheck?.close(); }); return () => { sub.remove(); void account?.deviceCheck?.close(); }; }, [account?.deviceCheck]);
|
|
9
14
|
const [busy, setBusy] = useState(false), [error, setError] = useState(''), [visible, setVisible] = useState(true), pending = useRef(null);
|
|
10
15
|
const dismissed = () => { const action = pending.current; pending.current = null; if (action)
|
|
11
16
|
requestAnimationFrame(() => void run(action)); };
|
|
@@ -24,6 +29,12 @@ export function MessagingAccountScreen({ account, companyName, onClose }) {
|
|
|
24
29
|
finally {
|
|
25
30
|
setBusy(false);
|
|
26
31
|
} };
|
|
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
|
-
|
|
32
|
+
return _jsx(MessagingPopup, { visible: visible, onDismiss: dismissed, title: uiText(checking ? "Check call devices" : "Account"), headerBack: checking, closeLabel: uiText("Close account"), closeDisabled: busy, background: c.background, contentWidth: 640, onClose: () => { if (!busy) {
|
|
33
|
+
if (checking) {
|
|
34
|
+
void account?.deviceCheck?.close();
|
|
35
|
+
setChecking(false);
|
|
36
|
+
}
|
|
37
|
+
else
|
|
38
|
+
onClose();
|
|
39
|
+
} }, children: _jsx(ScrollView, { contentContainerStyle: { gap: 16, paddingBottom: 12 }, children: checking && account?.deviceCheck ? _jsx(CallDeviceCheckPanel, { check: account.deviceCheck, renderVideo: account.renderCallPreview, labels: Object.fromEntries(Object.entries(callDeviceCheckText).map(([key, value]) => [key, uiText(value)])) }) : _jsxs(_Fragment, { 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?.deviceCheck && _jsx(Pressable, { accessibilityRole: "button", accessibilityLabel: uiText("Check call devices"), disabled: busy, onPress: () => setChecking(true), style: { backgroundColor: '#111111', borderRadius: 12, minHeight: 52, paddingHorizontal: 16, justifyContent: 'center' }, children: _jsx(Text, { style: { color: c.foreground, fontSize: 16 }, children: uiText("Check call devices") }) }), 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 })] }) }) });
|
|
29
40
|
}
|
package/dist/attachment-menu.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Text } from './typography.js';
|
|
2
3
|
import { useRobonoLocale } from './localization.js';
|
|
3
4
|
import { useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
|
|
4
|
-
import { Animated, BackHandler, Platform, Pressable, ScrollView,
|
|
5
|
+
import { Animated, BackHandler, Platform, Pressable, ScrollView, View, useWindowDimensions } from 'react-native';
|
|
5
6
|
import { MessagingPopupLayer, useMessagingPopupBounds } from './popup.js';
|
|
6
7
|
import { MessageIcon } from './icons.js';
|
|
7
8
|
/** Messenger's 206 × 46 attachment chips, staggered above its plus button. */
|
package/dist/call-message.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Text } from './typography.js';
|
|
2
3
|
import { useRobonoLocale } from './localization.js';
|
|
3
4
|
import { useEffect, useRef, useState } from 'react';
|
|
4
|
-
import { Pressable,
|
|
5
|
+
import { Pressable, View } from 'react-native';
|
|
5
6
|
import { useMessagingUI } from './index.js';
|
|
6
7
|
/** A new, reviewed message after no answer. Never records or sends a call automatically. */
|
|
7
8
|
export function UnansweredCallMessage({ callId, onDismiss }) {
|
package/dist/calling.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Text } from './typography.js';
|
|
2
3
|
import { useRobonoLocale } from './localization.js';
|
|
3
4
|
import { createContext, useContext, useSyncExternalStore } from 'react';
|
|
4
|
-
import { Pressable, View
|
|
5
|
+
import { Pressable, View } from 'react-native';
|
|
5
6
|
import { RobonoIcon } from './visual/RobonoIcon.js';
|
|
6
7
|
import { MessagingScreen, MessagingProvider, MessagingHeader, MessagingComposer } from './index.js';
|
|
7
8
|
import { UnansweredCallMessage } from './call-message.js';
|
|
@@ -16,5 +17,6 @@ export function MessagingCallingScreen({ onCall, onVideoCall, videoCallLabel = '
|
|
|
16
17
|
const design = props.appearance ?? state.settings?.appearance;
|
|
17
18
|
const screenProps = { ...props, callActions: { ...props.callActions, ...(onCall ? { voice: onCall, voiceLabel: callLabel } : {}), ...(onVideoCall ? { video: onVideoCall, videoLabel: videoCallLabel } : {}) } };
|
|
18
19
|
const callingOnly = (onCall || onVideoCall) && design && state.settings && !state.settings.enabled;
|
|
19
|
-
|
|
20
|
+
const callingContent = design ? _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 })] })] })] }) : null;
|
|
21
|
+
return _jsx(FollowUp.Provider, { value: unansweredCallId && onDismissCallMessage ? { callId: unansweredCallId, dismiss: onDismissCallMessage } : null, children: callingOnly ? _jsx(MessagingProvider, { ...screenProps, children: props.renderLayout ? props.renderLayout(callingContent) : callingContent }) : _jsx(MessagingScreen, { ...screenProps, components: { Header: MessagingCallHeader, Composer: CallingMessageComposer, ...components } }) });
|
|
20
22
|
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { Text } from './typography.js';
|
|
2
3
|
import { useRobonoLocale } from './localization.js';
|
|
3
4
|
import { useEffect, useRef, useState } from 'react';
|
|
4
|
-
import { ActivityIndicator, FlatList, Image, Platform, Pressable,
|
|
5
|
+
import { ActivityIndicator, FlatList, Image, Platform, Pressable, View } from 'react-native';
|
|
5
6
|
import { MessagingPopup, messagingPopupColors as c } from './popup.js';
|
|
6
7
|
import { MessageIcon, fileSize, duration } from './icons.js';
|
|
7
8
|
function MediaThumbnail({ message, preview }) {
|
|
@@ -82,5 +83,5 @@ export function ConversationDetails(props) {
|
|
|
82
83
|
setLeaving(false);
|
|
83
84
|
else
|
|
84
85
|
setVisible(false);
|
|
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") }) })] })] }) });
|
|
86
|
+
} }, 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 && props.onModerate && ['owner', 'moderator'].includes(state.conversation?.roomRole ?? '') && _jsx(Pressable, { accessibilityRole: "button", onPress: () => { pending.current = props.onModerate; setVisible(false); }, style: { minHeight: 48, justifyContent: 'center', alignItems: 'center', borderTopWidth: 1, borderTopColor: '#ffffff20' }, children: _jsx(Text, { style: { color: accent, fontSize: 16 }, children: uiText('Group moderation') }) }), 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") }) })] })] }) });
|
|
86
87
|
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import type { MessageRegionProps, MessagingCustomization } from './customization.js';
|
|
3
|
+
/** Data and customer render errors fall back inside the original message, never the whole chat. */
|
|
4
|
+
export declare function CustomMessage({ components, enabled, ...props }: MessageRegionProps & {
|
|
5
|
+
components?: MessagingCustomization;
|
|
6
|
+
enabled: boolean;
|
|
7
|
+
}): React.JSX.Element;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Component, useRef, useState } from 'react';
|
|
3
|
+
import { Pressable, View } from 'react-native';
|
|
4
|
+
import { validateCustomMessage, validateMessageCard } from '@robono/messaging';
|
|
5
|
+
import { Text } from './typography.js';
|
|
6
|
+
import { useRobonoLocale } from './localization.js';
|
|
7
|
+
class CardBoundary extends Component {
|
|
8
|
+
state = { failed: false };
|
|
9
|
+
static getDerivedStateFromError() { return { failed: true }; }
|
|
10
|
+
render() { return this.state.failed ? this.props.fallback : this.props.children; }
|
|
11
|
+
}
|
|
12
|
+
/** Data and customer render errors fall back inside the original message, never the whole chat. */
|
|
13
|
+
export function CustomMessage({ components, enabled, ...props }) {
|
|
14
|
+
const { text: uiText } = useRobonoLocale();
|
|
15
|
+
const [busy, setBusy] = useState(null), [failed, setFailed] = useState(false), inFlight = useRef(false);
|
|
16
|
+
let custom;
|
|
17
|
+
try {
|
|
18
|
+
custom = validateCustomMessage(props.message.custom);
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return _jsx(_Fragment, { children: props.children });
|
|
22
|
+
}
|
|
23
|
+
const { colors } = props.appearance, ink = props.mine ? colors.outgoingText : colors.incomingText;
|
|
24
|
+
const actionsEnabled = enabled && !props.pendingStatus && !props.message.deleted && !!components?.onCustomMessageAction;
|
|
25
|
+
const onAction = async (actionId) => {
|
|
26
|
+
if (!actionsEnabled || inFlight.current)
|
|
27
|
+
return;
|
|
28
|
+
if (!/^[a-zA-Z][a-zA-Z0-9_.:-]{0,79}$/.test(actionId))
|
|
29
|
+
throw Error('Invalid card action.');
|
|
30
|
+
inFlight.current = true;
|
|
31
|
+
setBusy(actionId);
|
|
32
|
+
setFailed(false);
|
|
33
|
+
try {
|
|
34
|
+
await components.onCustomMessageAction({ message: props.message, actionId });
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
setFailed(true);
|
|
38
|
+
}
|
|
39
|
+
finally {
|
|
40
|
+
inFlight.current = false;
|
|
41
|
+
setBusy(null);
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
const registered = Object.hasOwn(components?.customMessages ?? {}, custom.type) ? components?.customMessages?.[custom.type] : undefined;
|
|
45
|
+
let content = props.children;
|
|
46
|
+
if (registered?.versions.includes(custom.version))
|
|
47
|
+
content = _jsx(registered.Component, { ...props, custom: custom, actionsEnabled: actionsEnabled && !busy, onAction: onAction });
|
|
48
|
+
else if (custom.type === 'robono.card' && custom.version === 1) {
|
|
49
|
+
const card = validateMessageCard(custom.data);
|
|
50
|
+
content = _jsxs(View, { testID: "message-business-card", style: { gap: 6, minWidth: 180, maxWidth: 320 }, children: [_jsx(Text, { selectable: true, style: { fontSize: 16, lineHeight: 22, fontWeight: '600', color: ink }, children: card.title }), !!card.body && _jsx(Text, { selectable: true, style: { fontSize: 15, lineHeight: 21, color: ink }, children: card.body }), card.fields?.map((field, index) => _jsxs(View, { style: { flexDirection: 'row', flexWrap: 'wrap', columnGap: 12, rowGap: 2, justifyContent: 'space-between' }, children: [_jsx(Text, { style: { fontSize: 13, lineHeight: 19, color: ink, opacity: .75 }, children: field.label }), _jsx(Text, { selectable: true, style: { fontSize: 14, lineHeight: 20, color: ink, flexShrink: 1 }, children: field.value })] }, index)), !!components?.onCustomMessageAction && card.actions?.map(action => _jsx(Pressable, { accessibilityRole: "button", accessibilityLabel: action.label, accessibilityState: { disabled: !actionsEnabled || !!busy, busy: busy === action.id }, disabled: !actionsEnabled || !!busy, onPress: () => void onAction(action.id), style: { minHeight: 44, paddingHorizontal: 12, justifyContent: 'center', alignItems: 'center', borderWidth: 1, borderColor: colors.accent, borderRadius: 8, opacity: actionsEnabled && !busy ? 1 : .55 }, children: _jsx(Text, { style: { fontSize: 14, fontWeight: '600', color: colors.accent }, children: action.label }) }, action.id))] });
|
|
51
|
+
}
|
|
52
|
+
return _jsxs(CardBoundary, { fallback: props.children, children: [content, failed && _jsx(Text, { accessibilityRole: "alert", style: { color: ink, fontSize: 13, lineHeight: 19, marginTop: 6 }, children: uiText('This action could not be completed. Try again.') })] }, props.message.id + ':' + custom.type + ':' + custom.version);
|
|
53
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { ComponentType, ReactNode } from 'react';
|
|
2
|
+
import type { MessagingAppearance, MessagingMessage, CustomMessageContent } from '@robono/messaging';
|
|
3
|
+
export interface MessageRegionProps {
|
|
4
|
+
message: MessagingMessage;
|
|
5
|
+
mine: boolean;
|
|
6
|
+
appearance: MessagingAppearance;
|
|
7
|
+
pendingStatus?: 'sending' | 'queued' | 'failed';
|
|
8
|
+
/** The supplied region. Render children to retain its standard behavior. */
|
|
9
|
+
children?: ReactNode;
|
|
10
|
+
}
|
|
11
|
+
export interface HeaderRegionProps {
|
|
12
|
+
conversationId: string;
|
|
13
|
+
contact: {
|
|
14
|
+
name: string;
|
|
15
|
+
avatarUrl?: string;
|
|
16
|
+
language?: string;
|
|
17
|
+
phoneNumber?: string;
|
|
18
|
+
};
|
|
19
|
+
appearance: MessagingAppearance;
|
|
20
|
+
}
|
|
21
|
+
export interface CustomMessageProps extends MessageRegionProps {
|
|
22
|
+
custom: CustomMessageContent;
|
|
23
|
+
/** False for queued/deleted messages and when no customer callback is supplied. */
|
|
24
|
+
actionsEnabled: boolean;
|
|
25
|
+
/** Invokes the same supplied callback as ready-made cards. Never interprets the ID. */
|
|
26
|
+
onAction: (actionId: string) => Promise<void>;
|
|
27
|
+
}
|
|
28
|
+
export interface CustomMessageAction {
|
|
29
|
+
message: MessagingMessage;
|
|
30
|
+
actionId: string;
|
|
31
|
+
}
|
|
32
|
+
/** Small visual additions/replacements, independent of the media engine.
|
|
33
|
+
* Define components outside render so drafts and players are not remounted.
|
|
34
|
+
* These options never grant service access or change server permissions. */
|
|
35
|
+
export interface MessagingCustomization {
|
|
36
|
+
/** Register only the data versions your component understands. Unknown types use text. */
|
|
37
|
+
customMessages?: Record<string, {
|
|
38
|
+
versions: readonly number[];
|
|
39
|
+
Component: ComponentType<CustomMessageProps>;
|
|
40
|
+
}>;
|
|
41
|
+
/** Connect to your authorized application action. Check permissions and current state on your server. */
|
|
42
|
+
onCustomMessageAction?: (event: CustomMessageAction) => void | Promise<void>;
|
|
43
|
+
/** Wrap/replace only the message body, preserving its outer status and gestures. */
|
|
44
|
+
MessageBody?: ComponentType<MessageRegionProps>;
|
|
45
|
+
/** Optional content inside the bubble, after the body and supplied language actions. */
|
|
46
|
+
MessageFooter?: ComponentType<MessageRegionProps>;
|
|
47
|
+
/** Add a customer action beside the contact identity; built-in actions remain. */
|
|
48
|
+
HeaderActions?: ComponentType<HeaderRegionProps>;
|
|
49
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -18,6 +18,14 @@ export declare function createExpoMessengerCalling(options: {
|
|
|
18
18
|
name: string;
|
|
19
19
|
avatarUrl?: string;
|
|
20
20
|
}>): void;
|
|
21
|
+
deviceCheck: {
|
|
22
|
+
getSnapshot: () => import("@robono/calling-react-native").CallDeviceCheckState;
|
|
23
|
+
subscribe(fn: () => void): () => void;
|
|
24
|
+
start(kind?: "voice" | "video"): Promise<void>;
|
|
25
|
+
speaker(): Promise<void>;
|
|
26
|
+
close: () => Promise<void>;
|
|
27
|
+
dispose(): Promise<void>;
|
|
28
|
+
} | undefined;
|
|
21
29
|
start: () => Promise<void>;
|
|
22
30
|
getSnapshot: () => MessengerCallingState;
|
|
23
31
|
subscribe(fn: () => void): () => void;
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { callDiagnosticsText, callRecordingText } from '@robono/calling-react-native';
|
|
3
|
+
import { Text } from './typography.js';
|
|
2
4
|
import { callingText } from '@robono/calling-react-native/appearance';
|
|
3
5
|
import { useRobonoLocale } from './localization.js';
|
|
4
6
|
import { useEffect, useSyncExternalStore } from 'react';
|
|
5
|
-
import { Pressable,
|
|
7
|
+
import { Pressable, View } from 'react-native';
|
|
6
8
|
import { setAudioModeAsync } from 'expo-audio';
|
|
7
9
|
import { createMessengerCalling } from '@robono/calling-react-native/messenger';
|
|
8
|
-
import { createNativeCallAdapters } from '@robono/calling-react-native/native';
|
|
10
|
+
import { createNativeCallAdapters, createNativeCallDeviceCheck } from '@robono/calling-react-native/native';
|
|
9
11
|
import { createNativeCallRuntime } from '@robono/calling-react-native/runtime';
|
|
10
12
|
import { createNativeSystemCalling } from '@robono/calling-react-native/system-calling';
|
|
11
13
|
import { NativeCallingOverlay, NativeGroupCallingOverlay } from '@robono/calling-react-native/native-video';
|
|
@@ -16,6 +18,7 @@ export function createExpoMessengerCalling(options) {
|
|
|
16
18
|
let people = {};
|
|
17
19
|
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.') });
|
|
18
20
|
const calling = createMessengerCalling({
|
|
21
|
+
deviceCheck: active => createNativeCallDeviceCheck({ active, restoreAudio: () => setAudioModeAsync({ allowsRecording: false, playsInSilentMode: true, shouldPlayInBackground: false, shouldRouteThroughEarpiece: false, interruptionMode: 'mixWithOthers' }) }),
|
|
19
22
|
request: createMessagingTransport({ ...options, url: url.href }),
|
|
20
23
|
resolveContact: id => {
|
|
21
24
|
const row = options.inbox.getSnapshot().conversations.find(c => c.id === id);
|
|
@@ -52,5 +55,5 @@ export function useExpoMessengerCalling(calling) {
|
|
|
52
55
|
}
|
|
53
56
|
export function ExpoMessengerCalls({ calling, state }) {
|
|
54
57
|
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)])) })] });
|
|
58
|
+
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, recordingLabels: Object.fromEntries(Object.entries(callRecordingText).map(([key, value]) => [key, uiText(value)])), diagnostics: { labels: Object.fromEntries(Object.entries(callDiagnosticsText).map(([key, value]) => [key, uiText(value)])) }, 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, recordingLabels: Object.fromEntries(Object.entries(callRecordingText).map(([key, value]) => [key, uiText(value)])), diagnostics: { labels: Object.fromEntries(Object.entries(callDiagnosticsText).map(([key, value]) => [key, uiText(value)])) }, appearance: state.appearance, labels: Object.fromEntries(Object.entries({ ...callingText, ...state.appearance?.labels }).map(([key, value]) => [key, uiText(value)])) })] });
|
|
56
59
|
}
|
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 { type HostedRobonoConnectionOptions, MessagingInboxClient, type ConversationSummary } from '@robono/messaging';
|
|
4
|
+
import { MessagingAvailabilityClient, 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. */
|
|
@@ -10,6 +10,7 @@ export declare function createExpoMessenger(options: {
|
|
|
10
10
|
userId: string;
|
|
11
11
|
getAccessToken: () => Promise<string>;
|
|
12
12
|
fetch?: typeof fetch;
|
|
13
|
+
availability?: false;
|
|
13
14
|
notifications?: false;
|
|
14
15
|
extras?: false;
|
|
15
16
|
maps?: false;
|
|
@@ -45,6 +46,14 @@ export declare function createExpoMessenger(options: {
|
|
|
45
46
|
name: string;
|
|
46
47
|
avatarUrl?: string;
|
|
47
48
|
}>): void;
|
|
49
|
+
deviceCheck: {
|
|
50
|
+
getSnapshot: () => import("@robono/calling-react-native").CallDeviceCheckState;
|
|
51
|
+
subscribe(fn: () => void): () => void;
|
|
52
|
+
start(kind?: "voice" | "video"): Promise<void>;
|
|
53
|
+
speaker(): Promise<void>;
|
|
54
|
+
close: () => Promise<void>;
|
|
55
|
+
dispose(): Promise<void>;
|
|
56
|
+
} | undefined;
|
|
48
57
|
start: () => Promise<void>;
|
|
49
58
|
getSnapshot: () => import("@robono/calling-react-native/messenger").MessengerCallingState;
|
|
50
59
|
subscribe(fn: () => void): () => void;
|
|
@@ -54,6 +63,7 @@ export declare function createExpoMessenger(options: {
|
|
|
54
63
|
prime(): void;
|
|
55
64
|
call(conversationId: string, kind: "voice" | "video", group?: boolean): Promise<void>;
|
|
56
65
|
} | undefined;
|
|
66
|
+
availability: MessagingAvailabilityClient | undefined;
|
|
57
67
|
maps: LocationWorkspace | undefined;
|
|
58
68
|
};
|
|
59
69
|
export declare function createHostedExpoMessenger(options: Omit<Parameters<typeof createExpoMessenger>[0], 'endpoint' | 'getAccessToken' | 'namespace'> & HostedRobonoConnectionOptions & {
|
|
@@ -81,6 +91,14 @@ export declare function createHostedExpoMessenger(options: Omit<Parameters<typeo
|
|
|
81
91
|
name: string;
|
|
82
92
|
avatarUrl?: string;
|
|
83
93
|
}>): void;
|
|
94
|
+
deviceCheck: {
|
|
95
|
+
getSnapshot: () => import("@robono/calling-react-native").CallDeviceCheckState;
|
|
96
|
+
subscribe(fn: () => void): () => void;
|
|
97
|
+
start(kind?: "voice" | "video"): Promise<void>;
|
|
98
|
+
speaker(): Promise<void>;
|
|
99
|
+
close: () => Promise<void>;
|
|
100
|
+
dispose(): Promise<void>;
|
|
101
|
+
} | undefined;
|
|
84
102
|
start: () => Promise<void>;
|
|
85
103
|
getSnapshot: () => import("@robono/calling-react-native/messenger").MessengerCallingState;
|
|
86
104
|
subscribe(fn: () => void): () => void;
|
|
@@ -90,6 +108,7 @@ export declare function createHostedExpoMessenger(options: Omit<Parameters<typeo
|
|
|
90
108
|
prime(): void;
|
|
91
109
|
call(conversationId: string, kind: "voice" | "video", group?: boolean): Promise<void>;
|
|
92
110
|
} | undefined;
|
|
111
|
+
availability: MessagingAvailabilityClient | undefined;
|
|
93
112
|
maps: LocationWorkspace | undefined;
|
|
94
113
|
}>;
|
|
95
114
|
export interface ExpoMessengerProps extends Omit<ExpoMessagingScreenProps, 'contact' | 'renderConversation' | 'onBack'> {
|
package/dist/expo-messenger.js
CHANGED
|
@@ -1,15 +1,18 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { renderNativeCallVideo } from '@robono/calling-react-native/native-video';
|
|
3
|
+
import { createInboxViewMemory } from './inbox-view.js';
|
|
4
|
+
import { MessagingPublicRooms } from './public-rooms.js';
|
|
2
5
|
import { RobonoLocaleProvider, useRobonoLocale } from './localization.js';
|
|
3
6
|
import { useMessengerMaps, MessengerMapsNotice } from './maps-connection.js';
|
|
4
7
|
import { MessagingCallingScreen } from './calling.js';
|
|
5
8
|
import { createExpoMessengerCalling, useExpoMessengerCalling, ExpoMessengerCalls } from './expo-connected-calling.js';
|
|
6
9
|
import { isNativeCallAudioActive } from '@robono/calling-react-native/native';
|
|
7
|
-
import { View } from 'react-native';
|
|
10
|
+
import { View, AppState } from 'react-native';
|
|
8
11
|
import { createExpoMaps, RobonoMaps } from '@robono/location-react-native/expo';
|
|
9
12
|
import { createExpoMessagingExtras } from './expo-extras.js';
|
|
10
13
|
import { connectExpoMessagingNotifications } from './expo-notifications.js';
|
|
11
|
-
import { useEffect, useState, useSyncExternalStore } from 'react';
|
|
12
|
-
import { connectHostedRobono, createMessagingTransport, defaultMessagingSettings, MessagingInboxClient } from '@robono/messaging';
|
|
14
|
+
import { useMemo, useLayoutEffect, useEffect, useState, useSyncExternalStore } from 'react';
|
|
15
|
+
import { MessagingAvailabilityClient, connectHostedRobono, createMessagingTransport, defaultMessagingSettings, MessagingInboxClient } from '@robono/messaging';
|
|
13
16
|
import { ExpoMessagingScreen } from './expo-screen.js';
|
|
14
17
|
import { createExpoMessagingClient, expoOutbox } from './expo-outbox.js';
|
|
15
18
|
import { createExpoMessagingAudioSession, createExpoMessagingStorage } from './expo-media.js';
|
|
@@ -26,6 +29,13 @@ export function createExpoMessenger(options) {
|
|
|
26
29
|
const client = createExpoMessagingClient({ ...options, url: endpoint.href });
|
|
27
30
|
const mapConnection = options.maps === false ? undefined : createExpoMaps(options);
|
|
28
31
|
const connectedExtras = options.extras === false ? undefined : createExpoMessagingExtras(options);
|
|
32
|
+
const availability = options.availability === false ? undefined : new MessagingAvailabilityClient({ request: createMessagingTransport({ ...options, url: endpoint.href }) });
|
|
33
|
+
const activityChanged = (state) => { if (state === 'active')
|
|
34
|
+
availability?.start();
|
|
35
|
+
else
|
|
36
|
+
void availability?.stop(); };
|
|
37
|
+
const activitySubscription = availability ? AppState.addEventListener('change', activityChanged) : undefined;
|
|
38
|
+
activityChanged(AppState.currentState);
|
|
29
39
|
const inbox = new MessagingInboxClient({ request: createMessagingTransport({ ...options, url: endpoint.href }) });
|
|
30
40
|
const storage = createExpoMessagingStorage({ url: media.href, getAccessToken: options.getAccessToken, ...(options.fetch ? { fetch: options.fetch } : {}) });
|
|
31
41
|
const outgoing = expoOutbox(client);
|
|
@@ -85,12 +95,12 @@ export function createExpoMessenger(options) {
|
|
|
85
95
|
throw Error('Notifications are not connected yet. Check your Apple/Google setup in Robono.'); if (!await connection.enable())
|
|
86
96
|
throw Error('Allow notifications in your phone’s app settings.'); },
|
|
87
97
|
};
|
|
88
|
-
return { client, inbox, storage, audioSession, notifications, calling, maps: mapConnection?.workspace, ...(connectedExtras ? { extras: connectedExtras.extras, components: { TrackingMap: connectedExtras.TrackingMap } } : {}),
|
|
98
|
+
return { client, inbox, storage, audioSession, notifications, calling, availability, maps: mapConnection?.workspace, ...(connectedExtras ? { extras: connectedExtras.extras, components: { TrackingMap: connectedExtras.TrackingMap } } : {}),
|
|
89
99
|
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')
|
|
100
|
+
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')
|
|
91
101
|
throw failed.reason; },
|
|
92
102
|
dispose() { if (closed)
|
|
93
|
-
return; closed = true; void calling?.dispose().catch(() => { }); mapConnection?.dispose(); void connectedExtras?.dispose().catch(() => { }); connection?.dispose(); inbox.dispose(); client.dispose(); },
|
|
103
|
+
return; closed = true; activitySubscription?.remove(); availability?.dispose(); void calling?.dispose().catch(() => { }); mapConnection?.dispose(); void connectedExtras?.dispose().catch(() => { }); connection?.dispose(); inbox.dispose(); client.dispose(); },
|
|
94
104
|
};
|
|
95
105
|
}
|
|
96
106
|
export async function createHostedExpoMessenger(options) {
|
|
@@ -126,13 +136,30 @@ export function ExpoMessenger(props) {
|
|
|
126
136
|
return _jsx(RobonoLocaleProvider, { value: props.interfaceLocale, children: _jsx(ExpoMessengerContent, { ...props }) });
|
|
127
137
|
}
|
|
128
138
|
function ExpoMessengerContent({ inbox, contacts = {}, initialConversation, selectedConversation, onSelectConversation, notifications, maps, calling, ...props }) {
|
|
139
|
+
const inboxMemory = useMemo(createInboxViewMemory, [inbox, props.userId]);
|
|
129
140
|
const { configuration: mapsLocale } = useRobonoLocale();
|
|
130
141
|
const callState = useExpoMessengerCalling(calling);
|
|
131
142
|
useEffect(() => { calling?.updateContacts(contacts); }, [calling, contacts]);
|
|
132
143
|
const chatState = useSyncExternalStore(props.client.subscribe, props.client.getSnapshot, props.client.getSnapshot);
|
|
133
144
|
const groupAllowed = chatState.conversation?.kind !== 'group' || !chatState.conversation.supergroup && (chatState.conversation.memberCount ?? 0) >= 2 && (chatState.conversation.memberCount ?? 0) <= 8;
|
|
134
|
-
const [internal, setInternal] = useState(initialConversation ?? null), [newGroup, setNewGroup] = useState(false);
|
|
145
|
+
const [internal, setInternal] = useState(initialConversation ?? null), [newGroup, setNewGroup] = useState(false), [publicRooms, setPublicRooms] = useState(false);
|
|
135
146
|
const selected = selectedConversation === undefined ? internal : selectedConversation;
|
|
147
|
+
// Declarative navigation loads the selected conversation itself. Internal open()
|
|
148
|
+
// changes the client synchronously, so this never opens it a second time.
|
|
149
|
+
useLayoutEffect(() => { if (!selected) {
|
|
150
|
+
props.client.stop();
|
|
151
|
+
return;
|
|
152
|
+
} if (props.client.getSnapshot().conversationId !== selected.id)
|
|
153
|
+
void props.client.open(selected.id).catch(() => { }); }, [selected?.id, props.client]);
|
|
154
|
+
useEffect(() => {
|
|
155
|
+
const sync = (state) => { if (selected && props.active !== false && (state === null || state === 'active'))
|
|
156
|
+
props.client.start();
|
|
157
|
+
else
|
|
158
|
+
props.client.stop(); };
|
|
159
|
+
sync(AppState.currentState);
|
|
160
|
+
const subscription = AppState.addEventListener('change', sync);
|
|
161
|
+
return () => { subscription.remove(); props.client.stop(); };
|
|
162
|
+
}, [selected?.id, props.client, props.active]);
|
|
136
163
|
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;
|
|
137
164
|
const setSelected = (row) => { setInternal(row); onSelectConversation?.(row); };
|
|
138
165
|
const [showMaps, setShowMaps] = useState(false), mapConnection = useMessengerMaps(maps), mapsAvailable = mapConnection.available;
|
|
@@ -145,7 +172,8 @@ function ExpoMessengerContent({ inbox, contacts = {}, initialConversation, selec
|
|
|
145
172
|
};
|
|
146
173
|
useEffect(() => { void notifications?.start(async (id, message) => { await inbox.refresh(); const row = inbox.getSnapshot().conversations.find(c => c.id === id); if (row)
|
|
147
174
|
open(row, message); }).catch(() => { }); }, [notifications, inbox]);
|
|
148
|
-
const
|
|
175
|
+
const notificationAccount = notifications ? { name: props.account?.name ?? 'Your account', ...props.account, onEnableNotifications: () => notifications.enable() } : props.account;
|
|
176
|
+
const account = calling?.deviceCheck ? { name: 'Your account', ...notificationAccount, deviceCheck: calling.deviceCheck, renderCallPreview: (stream) => renderNativeCallVideo({ stream, local: true, style: { width: '100%', height: '100%' } }) } : notificationAccount;
|
|
149
177
|
const contact = selected?.kind === 'group' ? { name: selected.name ?? 'Group' } : contacts[selected?.contactId ?? ''] ?? { name: selected?.contactId ?? 'Chat' };
|
|
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 })] });
|
|
178
|
+
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 })] });
|
|
151
179
|
}
|
package/dist/expo-screen.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { MessagingTypography } from './typography.js';
|
|
3
|
+
import { Text } from './typography.js';
|
|
2
4
|
import { RobonoLocaleProvider } from './localization.js';
|
|
3
5
|
import { useRobonoLocale } from './localization.js';
|
|
4
6
|
import * as Haptics from 'expo-haptics';
|
|
@@ -11,7 +13,7 @@ import { expoSaved } from './expo-saved.js';
|
|
|
11
13
|
export { createExpoMessagingClient } from './expo-outbox.js';
|
|
12
14
|
import { createExpoThumbnails } from './expo-thumbnails.js';
|
|
13
15
|
import { useCallback, useEffect, useContext, useMemo, useRef, useState, useSyncExternalStore } from 'react';
|
|
14
|
-
import { AppState, Image, Modal, Platform, Pressable, ScrollView,
|
|
16
|
+
import { AppState, Image, Modal, Platform, Pressable, ScrollView, View } from 'react-native';
|
|
15
17
|
import { SafeAreaProvider, SafeAreaView } from 'react-native-safe-area-context';
|
|
16
18
|
import MaterialIcons from "./visual/RobonoIcon";
|
|
17
19
|
import { useAudioPlayer } from 'expo-audio';
|
|
@@ -358,7 +360,7 @@ export function MessagingMediaOverlay({ controller, audio, appearance, inlineVoi
|
|
|
358
360
|
return _jsx(Modal, { transparent: true, visible: state.phase !== 'idle' && state.phase !== 'choosing', onDismiss: onDismiss, animationType: "fade", onRequestClose: () => { if (expanded)
|
|
359
361
|
setExpanded(false);
|
|
360
362
|
else
|
|
361
|
-
void controller.cancel(); }, children: _jsx(SafeAreaProvider, { style: { flex: 1, backgroundColor: state.kind === 'image' || state.kind === 'video' ? 'rgba(0,0,0,0.94)' : messagingPopupBackdrop }, children: _jsx(SafeAreaView, { edges: expanded ? [] : ['top', 'bottom', 'left', 'right'], style: { flex: 1 }, children: _jsx(View, { style: { flex: 1, margin: state.kind === 'image' || state.kind === 'video' ? 0 : 8, borderRadius: state.kind === 'image' || state.kind === 'video' ? 0 : 18, overflow: 'hidden', backgroundColor: state.kind === 'image' || state.kind === 'video' ? 'transparent' : '#000000' }, children: viewer ?? (_jsx(View, { style: { flex: 1, backgroundColor: c.background }, children: _jsxs(ScrollView, { contentContainerStyle: { padding: 8, paddingTop: 0, gap: 12, flexGrow: 1 }, children: [_jsx(MessagingPopupHeader, { title: state.kind === 'voice' ? uiText("Voice message") : state.kind === 'image' ? uiText("Photo") : state.kind === 'video' ? uiText("Video") : uiText("File"), foreground: c.foreground, closeLabel: uiText("Close attachment"), onClose: () => void controller.cancel() }), state.phase === 'recording' ? _jsxs(View, { style: { alignItems: 'center', gap: 20, paddingVertical: 32 }, children: [_jsx(MaterialIcons, { name: "mic", size: 48, color: c.accent }), _jsx(Text, { style: { color: c.foreground, fontSize: 32, fontVariant: ['tabular-nums'] }, children: time(state.elapsedMs) }), _jsx(Text, { style: { color: c.secondary }, children: uiText('{time} remaining', { time: time(Math.max(0, state.maxDurationMs - state.elapsedMs)) }) }), _jsx(Control, { label: uiText("Stop recording"), icon: "stop", color: c.accent, onPress: () => void controller.finishRecording() })] }) : null, ['choosing', 'preparing', 'uploading', 'opening'].includes(state.phase) && _jsx(Text, { accessibilityRole: "progressbar", accessibilityValue: { min: 0, max: 100, now: Math.round(state.progress * 100) }, style: { color: c.foreground, fontSize: 18 }, children: state.phase === 'uploading' ? (state.processing ? 'Checking attachment… You can cancel.' :
|
|
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() })] }) })) }) }) }) });
|
|
362
364
|
}
|
|
363
365
|
/** Optional ready-made recording, preview, upload and download tools for both mobile platforms. */
|
|
364
366
|
export function ExpoMessagingScreen(props) {
|
|
@@ -554,20 +556,20 @@ function ExpoMessagingScreenContent({ storage, audioSession, renderConversation,
|
|
|
554
556
|
current.current.onMediaReady?.({ messageId: event.messageId, kind: event.kind, stage, elapsedMs: Date.now() - event.started });
|
|
555
557
|
}
|
|
556
558
|
catch { } };
|
|
557
|
-
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: () => {
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
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 })] }) }) });
|
|
573
575
|
}
|
package/dist/expo-social.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { Text } from './typography.js';
|
|
2
3
|
import { useRobonoLocale } from './localization.js';
|
|
3
|
-
import { Linking,
|
|
4
|
+
import { Linking, UIManager } from 'react-native';
|
|
4
5
|
// Expo builds include this native module when the documented peer is installed.
|
|
5
6
|
const available = !!UIManager.getViewManagerConfig('RNCWebView');
|
|
6
7
|
const WebView = available ? require('react-native-webview').WebView : null;
|
package/dist/extras.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { Text } from './typography.js';
|
|
2
3
|
import { useRobonoLocale } from './localization.js';
|
|
3
4
|
import { forwardRef, useEffect, useImperativeHandle, useRef, useState } from 'react';
|
|
4
|
-
import { ActivityIndicator, Image, Linking, Platform, Pressable, ScrollView,
|
|
5
|
+
import { ActivityIndicator, Image, Linking, Platform, Pressable, ScrollView, View } from 'react-native';
|
|
5
6
|
import { currentTrackers, trackerFresh, weatherTemperature } from '@robono/messaging';
|
|
6
7
|
import { MessagingPopup, messagingPopupColors } from './popup.js';
|
|
7
8
|
import { MessageIcon } from './icons.js';
|