@robono/messaging-react-native 0.5.0 → 0.7.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 -5
- package/SBOM.spdx.json +411 -171
- package/dist/attachment-menu.d.ts +16 -0
- package/dist/attachment-menu.js +71 -0
- package/dist/expo-connected-calling.d.ts +32 -0
- package/dist/expo-connected-calling.js +41 -0
- package/dist/expo-extras.d.ts +18 -0
- package/dist/expo-extras.js +32 -0
- package/dist/expo-media.js +4 -4
- package/dist/expo-messenger.d.ts +34 -3
- package/dist/expo-messenger.js +27 -16
- package/dist/expo-notifications.d.ts +2 -1
- package/dist/expo-notifications.js +16 -5
- package/dist/expo-screen.js +10 -3
- package/dist/expo-tracking-task.d.ts +1 -0
- package/dist/expo-tracking-task.js +1 -0
- package/dist/extras-lifecycle.js +3 -3
- package/dist/extras.js +24 -4
- package/dist/header-calls.js +3 -2
- package/dist/inbox.js +28 -11
- package/dist/index.d.ts +6 -2
- package/dist/index.js +31 -36
- package/dist/maps-connection.d.ts +12 -0
- package/dist/maps-connection.js +42 -0
- package/dist/media-controller.d.ts +2 -2
- package/dist/media-controller.js +3 -3
- package/dist/message-menu.js +8 -4
- package/dist/popup.d.ts +25 -0
- package/dist/popup.js +71 -2
- package/dist/quick-send.js +3 -3
- package/dist/safety.js +3 -2
- package/dist/use-quote-gesture.d.ts +1 -1
- package/dist/use-quote-gesture.js +9 -3
- package/package.json +13 -4
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { View } from 'react-native';
|
|
3
|
+
import { MessageIcon } from './icons.js';
|
|
4
|
+
export interface AttachmentMenuAction {
|
|
5
|
+
key: string;
|
|
6
|
+
label: string;
|
|
7
|
+
icon: React.ComponentProps<typeof MessageIcon>['name'];
|
|
8
|
+
run: () => void;
|
|
9
|
+
}
|
|
10
|
+
/** Messenger's 206 × 46 attachment chips, staggered above its plus button. */
|
|
11
|
+
export declare function AttachmentMenu({ anchor, actions, accent, onClose }: {
|
|
12
|
+
anchor: React.RefObject<View | null>;
|
|
13
|
+
actions: AttachmentMenuAction[];
|
|
14
|
+
accent: string;
|
|
15
|
+
onClose: () => void;
|
|
16
|
+
}): React.JSX.Element;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
|
|
3
|
+
import { Animated, BackHandler, Platform, Pressable, ScrollView, Text, View, useWindowDimensions } from 'react-native';
|
|
4
|
+
import { MessagingPopupLayer, useMessagingPopupBounds } from './popup.js';
|
|
5
|
+
import { MessageIcon } from './icons.js';
|
|
6
|
+
/** Messenger's 206 × 46 attachment chips, staggered above its plus button. */
|
|
7
|
+
export function AttachmentMenu({ anchor, actions, accent, onClose }) {
|
|
8
|
+
const bounds = useMessagingPopupBounds(), window = useWindowDimensions(), fade = useRef(new Animated.Value(0)).current;
|
|
9
|
+
const [position, setPosition] = useState(null);
|
|
10
|
+
const menu = useRef(null), close = useRef(onClose);
|
|
11
|
+
close.current = onClose;
|
|
12
|
+
useLayoutEffect(() => {
|
|
13
|
+
let live = true;
|
|
14
|
+
const frame = requestAnimationFrame(() => {
|
|
15
|
+
const root = bounds?.root.current, button = anchor.current;
|
|
16
|
+
if (!root || !button)
|
|
17
|
+
return;
|
|
18
|
+
root.measureInWindow((rx, ry, width, height) => button.measureInWindow((x, y, w, h) => { if (live)
|
|
19
|
+
setPosition({ x: x - rx + w / 2, y: y - ry + h / 2, width, height }); }));
|
|
20
|
+
});
|
|
21
|
+
return () => { live = false; cancelAnimationFrame(frame); };
|
|
22
|
+
}, [bounds, anchor, window.width, window.height]);
|
|
23
|
+
useEffect(() => { Animated.timing(fade, { toValue: 1, duration: 120, useNativeDriver: true, isInteraction: false }).start(); return () => fade.stopAnimation(); }, [fade]);
|
|
24
|
+
useEffect(() => {
|
|
25
|
+
if (Platform.OS !== 'web') {
|
|
26
|
+
const off = BackHandler.addEventListener('hardwareBackPress', () => { close.current(); return true; });
|
|
27
|
+
return () => off.remove();
|
|
28
|
+
}
|
|
29
|
+
const previous = document.activeElement;
|
|
30
|
+
const frame = requestAnimationFrame(() => { menu.current?.querySelector('[role="button"]')?.focus(); });
|
|
31
|
+
const key = (event) => {
|
|
32
|
+
const element = menu.current;
|
|
33
|
+
if (event.key === 'Escape') {
|
|
34
|
+
event.preventDefault();
|
|
35
|
+
event.stopPropagation();
|
|
36
|
+
close.current();
|
|
37
|
+
}
|
|
38
|
+
if (event.key === 'Tab' && element) {
|
|
39
|
+
const items = [...element.querySelectorAll('[role="button"]')];
|
|
40
|
+
const first = items[0], last = items.at(-1);
|
|
41
|
+
if (event.shiftKey && (document.activeElement === first || !element.contains(document.activeElement))) {
|
|
42
|
+
event.preventDefault();
|
|
43
|
+
last?.focus();
|
|
44
|
+
}
|
|
45
|
+
else if (!event.shiftKey && (document.activeElement === last || !element.contains(document.activeElement))) {
|
|
46
|
+
event.preventDefault();
|
|
47
|
+
first?.focus();
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
document.addEventListener('keydown', key, true);
|
|
52
|
+
return () => { cancelAnimationFrame(frame); document.removeEventListener('keydown', key, true); if (previous?.isConnected)
|
|
53
|
+
previous.focus(); };
|
|
54
|
+
}, [!!position]);
|
|
55
|
+
const node = useMemo(() => {
|
|
56
|
+
if (!position)
|
|
57
|
+
return null;
|
|
58
|
+
const compact = position.y - 80 < Math.max(0, actions.length - 1) * 46;
|
|
59
|
+
const listHeight = Math.min(Math.max(46, position.height - 28), actions.length * 54 - 8);
|
|
60
|
+
const listTop = Math.max(14, Math.min(position.height - listHeight - 14, position.y - listHeight - 30));
|
|
61
|
+
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) => {
|
|
63
|
+
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
|
+
return _jsx(Pressable, { accessibilityRole: "button", accessibilityLabel: action.label, onPress: () => { onClose(); if (Platform.OS === 'web')
|
|
65
|
+
action.run();
|
|
66
|
+
else
|
|
67
|
+
requestAnimationFrame(() => requestAnimationFrame(action.run)); }, style: { position: compact ? 'relative' : 'absolute', width: chipWidth, height: 46, flexShrink: 0, left: compact ? 0 : Math.max(10, Math.min(position.width - chipWidth - 10, position.x + 14 + index * 18)), top: compact ? 0 : Math.max(14, position.y - 66 - index * step), shadowColor: '#000', shadowOpacity: .42, shadowRadius: 15, shadowOffset: { width: 0, height: 7 }, elevation: 10 }, children: ({ pressed }) => _jsxs(Animated.View, { pointerEvents: "none", style: { flex: 1, opacity: fade, borderRadius: 23, borderWidth: 1, borderColor: '#ffffff1c', backgroundColor: pressed ? '#211D1A' : '#171717', flexDirection: 'row', alignItems: 'center', gap: 10, paddingStart: 7, paddingEnd: 12, transform: [{ scale: pressed ? .97 : 1 }] }, children: [_jsx(View, { style: { width: 32, height: 32, borderRadius: 11, borderWidth: .5, borderColor: accent + '38', backgroundColor: accent + '1a', alignItems: 'center', justifyContent: 'center' }, children: _jsx(MessageIcon, { name: action.icon, size: 21, color: accent }) }), _jsx(Text, { numberOfLines: 1, style: { flex: 1, color: '#ffffffeb', fontFamily: Platform.select({ ios: 'Avenir Next', android: 'sans-serif-medium' }), fontSize: 14, fontWeight: '600', letterSpacing: .12 }, children: action.label }), _jsx(MessageIcon, { name: "chevron-forward", size: 14, color: "#ffffff52" })] }) }, action.key);
|
|
68
|
+
}) }) })] });
|
|
69
|
+
}, [position, actions, accent, onClose, fade]);
|
|
70
|
+
return _jsx(MessagingPopupLayer, { children: node });
|
|
71
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { type MessengerCalling, type MessengerCallingState } from '@robono/calling-react-native/messenger';
|
|
3
|
+
import { type MessagingInboxClient } from '@robono/messaging';
|
|
4
|
+
export declare function createExpoMessengerCalling(options: {
|
|
5
|
+
endpoint: string;
|
|
6
|
+
getAccessToken: () => Promise<string>;
|
|
7
|
+
fetch?: typeof fetch;
|
|
8
|
+
people?: () => Record<string, {
|
|
9
|
+
name: string;
|
|
10
|
+
avatarUrl?: string;
|
|
11
|
+
}>;
|
|
12
|
+
inbox: MessagingInboxClient;
|
|
13
|
+
}): {
|
|
14
|
+
updateContacts(next: Record<string, {
|
|
15
|
+
name: string;
|
|
16
|
+
avatarUrl?: string;
|
|
17
|
+
}>): void;
|
|
18
|
+
start: () => Promise<void>;
|
|
19
|
+
getSnapshot: () => MessengerCallingState;
|
|
20
|
+
subscribe(fn: () => void): () => void;
|
|
21
|
+
isActive: () => boolean;
|
|
22
|
+
dismissUnanswered(): void;
|
|
23
|
+
notification(data: unknown, opened?: boolean): Promise<boolean>;
|
|
24
|
+
prime(): void;
|
|
25
|
+
call(conversationId: string, kind: "voice" | "video", group?: boolean): Promise<void>;
|
|
26
|
+
dispose(): Promise<void>;
|
|
27
|
+
};
|
|
28
|
+
export declare function useExpoMessengerCalling(calling?: MessengerCalling): MessengerCallingState;
|
|
29
|
+
export declare function ExpoMessengerCalls({ calling, state }: {
|
|
30
|
+
calling?: MessengerCalling;
|
|
31
|
+
state: MessengerCallingState;
|
|
32
|
+
}): React.JSX.Element;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useSyncExternalStore } from 'react';
|
|
3
|
+
import { Pressable, Text, View } from 'react-native';
|
|
4
|
+
import { setAudioModeAsync } from 'expo-audio';
|
|
5
|
+
import { createMessengerCalling } from '@robono/calling-react-native/messenger';
|
|
6
|
+
import { createNativeCallAdapters } from '@robono/calling-react-native/native';
|
|
7
|
+
import { createNativeCallRuntime } from '@robono/calling-react-native/runtime';
|
|
8
|
+
import { NativeCallingOverlay, NativeGroupCallingOverlay } from '@robono/calling-react-native/native-video';
|
|
9
|
+
import { createMessagingTransport } from '@robono/messaging';
|
|
10
|
+
export function createExpoMessengerCalling(options) {
|
|
11
|
+
const url = new URL(options.endpoint);
|
|
12
|
+
url.searchParams.set('service', 'calling');
|
|
13
|
+
let people = {};
|
|
14
|
+
const calling = createMessengerCalling({
|
|
15
|
+
request: createMessagingTransport({ ...options, url: url.href }),
|
|
16
|
+
resolveContact: id => {
|
|
17
|
+
const row = options.inbox.getSnapshot().conversations.find(c => c.id === id);
|
|
18
|
+
if (row?.kind === 'group')
|
|
19
|
+
return { name: row.name ?? 'Group' };
|
|
20
|
+
const person = (options.people?.() ?? people)[row?.contactId ?? ''];
|
|
21
|
+
return person ? { name: person.name, avatarUri: person.avatarUrl } : null;
|
|
22
|
+
},
|
|
23
|
+
resolveMember: (_app, id) => { const person = (options.people?.() ?? people)[id]; return person ? { name: person.name, avatarUri: person.avatarUrl } : null; },
|
|
24
|
+
adapters: appearance => {
|
|
25
|
+
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: runtime.attach, dispose: runtime.dispose };
|
|
28
|
+
},
|
|
29
|
+
});
|
|
30
|
+
return { ...calling, updateContacts(next) { people = next; } };
|
|
31
|
+
}
|
|
32
|
+
const absent = { direct: null, group: null, loading: false, error: '', features: { voice: false, video: false } };
|
|
33
|
+
const noSubscribe = () => () => { };
|
|
34
|
+
export function useExpoMessengerCalling(calling) {
|
|
35
|
+
const state = useSyncExternalStore(calling?.subscribe ?? noSubscribe, calling?.getSnapshot ?? (() => absent), calling?.getSnapshot ?? (() => absent));
|
|
36
|
+
useEffect(() => { void calling?.start().catch(() => { }); }, [calling]);
|
|
37
|
+
return state;
|
|
38
|
+
}
|
|
39
|
+
export function ExpoMessengerCalls({ calling, state }) {
|
|
40
|
+
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: "Retry calls" }) })] }), state.direct && _jsx(NativeCallingOverlay, { controller: state.direct, appearance: state.appearance }), state.group && _jsx(NativeGroupCallingOverlay, { controller: state.group, appearance: state.appearance })] });
|
|
41
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { MessagingExtrasClient } from '@robono/messaging';
|
|
3
|
+
import type { TrackingMapProps } from './extras.js';
|
|
4
|
+
export declare function createExpoMessagingExtras(options: {
|
|
5
|
+
endpoint: string;
|
|
6
|
+
userId: string;
|
|
7
|
+
getAccessToken: () => Promise<string>;
|
|
8
|
+
fetch?: typeof fetch;
|
|
9
|
+
people?: () => Record<string, {
|
|
10
|
+
name: string;
|
|
11
|
+
avatarUrl?: string;
|
|
12
|
+
}>;
|
|
13
|
+
}): {
|
|
14
|
+
extras: MessagingExtrasClient;
|
|
15
|
+
TrackingMap: (props: TrackingMapProps) => React.JSX.Element;
|
|
16
|
+
signOut(): Promise<void>;
|
|
17
|
+
dispose(): Promise<void>;
|
|
18
|
+
};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { prepareBackgroundTracking, captureBackgroundTracking, endBackgroundAccount } from './expo-tracking-task.js';
|
|
3
|
+
import * as Location from 'expo-location';
|
|
4
|
+
import Constants from 'expo-constants';
|
|
5
|
+
import * as SecureStore from 'expo-secure-store';
|
|
6
|
+
import * as Crypto from 'expo-crypto';
|
|
7
|
+
import { TrackingMap } from '@robono/location-react-native';
|
|
8
|
+
import { getForegroundPosition, ManagedMap } from '@robono/location-react-native/expo';
|
|
9
|
+
import { MessagingExtrasClient, createMessagingTransport, createRecoverableExtrasTransport } from '@robono/messaging';
|
|
10
|
+
const convert = (value) => ({ latitude: value.coords.latitude, longitude: value.coords.longitude, accuracyMeters: value.coords.accuracy ?? 500, capturedAt: value.timestamp, ...(value.coords.speed !== null && value.coords.speed >= 0 ? { speedMps: value.coords.speed } : {}) });
|
|
11
|
+
export function createExpoMessagingExtras(options) {
|
|
12
|
+
const url = new URL(options.endpoint);
|
|
13
|
+
url.searchParams.set('service', 'extras');
|
|
14
|
+
const cleanup = endBackgroundAccount(url.href, options.userId);
|
|
15
|
+
const storageKey = Crypto.digestStringAsync(Crypto.CryptoDigestAlgorithm.SHA256, url.href + '\n' + options.userId).then(hash => 'robono.tracking.recovery.' + hash);
|
|
16
|
+
const request = createRecoverableExtrasTransport(createMessagingTransport({ ...options, url: url.href }), { load: async () => { await cleanup; return JSON.parse(await SecureStore.getItemAsync(await storageKey) ?? '[]'); }, save: async (records) => { const key = await storageKey; if (records.length)
|
|
17
|
+
await SecureStore.setItemAsync(key, JSON.stringify(records));
|
|
18
|
+
else
|
|
19
|
+
await SecureStore.deleteItemAsync(key); } });
|
|
20
|
+
const background = Constants.expoConfig?.extra?.robonoMaps?.background === true;
|
|
21
|
+
const extras = new MessagingExtrasClient({ request, ...(background ? { backgroundCapture: true, prepareTracking: prepareBackgroundTracking, capture: (context, signal) => captureBackgroundTracking({ url: url.href, userId: options.userId, getAccessToken: options.getAccessToken }, context, signal) } : {}), people: options.people, createRequestId: () => Crypto.randomUUID(),
|
|
22
|
+
locate: async (signal) => { const p = await getForegroundPosition(signal); return { ...p, capturedAt: Date.parse(p.capturedAt), speedMps: p.speedMps ?? undefined }; },
|
|
23
|
+
watch: (receive, onError, signal) => { let subscription; const stop = () => { subscription?.remove(); signal.removeEventListener('abort', stop); }; signal.addEventListener('abort', stop, { once: true }); void Location.watchPositionAsync({ accuracy: Location.Accuracy.High, timeInterval: 10000, distanceInterval: 5 }, p => { if (!signal.aborted)
|
|
24
|
+
receive(convert(p)); }, () => { stop(); onError(); }).then(s => { subscription = s; if (signal.aborted)
|
|
25
|
+
stop(); }).catch(() => { stop(); onError(); }); },
|
|
26
|
+
});
|
|
27
|
+
function ConnectedTrackingMap(props) {
|
|
28
|
+
return _jsx(ManagedMap, { client: extras, children: onLoad => _jsx(TrackingMap, { ...props, onLoad: onLoad }) });
|
|
29
|
+
}
|
|
30
|
+
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
|
+
throw failed.reason; }, async dispose() { extras.dispose(); await endBackgroundAccount(url.href, options.userId); } };
|
|
32
|
+
}
|
package/dist/expo-media.js
CHANGED
|
@@ -110,7 +110,7 @@ export function createExpoMessagingDevice(audio) {
|
|
|
110
110
|
let recordingDeadline = 0;
|
|
111
111
|
let starting = null;
|
|
112
112
|
let stopping = null;
|
|
113
|
-
async function chooseFiles(kind, settings, signal, source, maximum) {
|
|
113
|
+
async function chooseFiles(kind, settings, signal, source, maximum, mixedLibrary = false) {
|
|
114
114
|
if (source === 'camera' && (!['image', 'video'].includes(kind) || !settings.appearance.showCamera))
|
|
115
115
|
throw Error('Camera capture is unavailable.');
|
|
116
116
|
if (source === 'call-video' && (kind !== 'video' || !settings.enabled || !settings.features.video || settings.unansweredCalls?.video !== 'video'))
|
|
@@ -132,7 +132,7 @@ export function createExpoMessagingDevice(audio) {
|
|
|
132
132
|
if (!permission.granted)
|
|
133
133
|
throw Error('Camera permission is needed to take photos or record videos.');
|
|
134
134
|
}
|
|
135
|
-
const result = await (source ? Picker.launchCameraAsync : Picker.launchImageLibraryAsync)({ mediaTypes: source === 'call-video' ? ['videos'] : source ? [...((settings.features?.image ?? kind === 'image') ? ['images'] : []), ...(settings.features?.video ? ['videos'] : [])] : kind === 'image' ? ['images'] : ['videos'], ...(source ? { videoMaxDuration: settings.attachments?.video?.maxDurationSeconds ?? 60 } : {}), allowsMultipleSelection: !source && maximum > 1, selectionLimit: maximum, orderedSelection: true, allowsEditing: false, quality: 1, preferredAssetRepresentationMode: Picker.UIImagePickerPreferredAssetRepresentationMode.Current });
|
|
135
|
+
const result = await (source ? Picker.launchCameraAsync : Picker.launchImageLibraryAsync)({ mediaTypes: source === 'call-video' ? ['videos'] : source ? [...((settings.features?.image ?? kind === 'image') ? ['images'] : []), ...(settings.features?.video ? ['videos'] : [])] : mixedLibrary ? [...(settings.features.image ? ['images'] : []), ...(settings.features.video ? ['videos'] : [])] : kind === 'image' ? ['images'] : ['videos'], ...(source ? { videoMaxDuration: settings.attachments?.video?.maxDurationSeconds ?? 60 } : {}), allowsMultipleSelection: !source && maximum > 1, selectionLimit: maximum, orderedSelection: true, allowsEditing: false, quality: 1, preferredAssetRepresentationMode: Picker.UIImagePickerPreferredAssetRepresentationMode.Current });
|
|
136
136
|
assets = result.assets ?? [];
|
|
137
137
|
if (result.canceled)
|
|
138
138
|
return [];
|
|
@@ -142,7 +142,7 @@ export function createExpoMessagingDevice(audio) {
|
|
|
142
142
|
throw Error(`Choose up to ${maximum} attachments.`);
|
|
143
143
|
for (const asset of assets) {
|
|
144
144
|
check(signal);
|
|
145
|
-
const video =
|
|
145
|
+
const video = (mixedLibrary || !!source) ? asset.type === 'video' || !!asset.mimeType?.startsWith('video/') : kind === 'video';
|
|
146
146
|
const name = asset.name ?? asset.fileName ?? asset.uri.split('/').at(-1) ?? (video ? 'Video' : 'Photo');
|
|
147
147
|
if (video && (!asset.duration || !Number.isFinite(asset.duration)))
|
|
148
148
|
throw Error('The video length could not be read. Choose another video.');
|
|
@@ -161,7 +161,7 @@ export function createExpoMessagingDevice(audio) {
|
|
|
161
161
|
}
|
|
162
162
|
return {
|
|
163
163
|
async pick(kind, settings, signal, source) { return (await chooseFiles(kind, settings, signal, source, 1))[0] ?? null; },
|
|
164
|
-
pickMany: (kind, settings, signal, maximum) => chooseFiles(kind, settings, signal, undefined, maximum),
|
|
164
|
+
pickMany: (kind, settings, signal, maximum, mixedLibrary) => chooseFiles(kind, settings, signal, undefined, maximum, mixedLibrary),
|
|
165
165
|
async startRecording(maximum, signal) {
|
|
166
166
|
if (starting || recorder)
|
|
167
167
|
throw Error('A recording is already in progress.');
|
package/dist/expo-messenger.d.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { createExpoMessengerCalling } from './expo-connected-calling.js';
|
|
2
|
+
import type { LocationWorkspace } from '@robono/location';
|
|
1
3
|
import React from 'react';
|
|
2
4
|
import { MessagingInboxClient, type ConversationSummary } from '@robono/messaging';
|
|
3
5
|
import { type ExpoMessagingScreenProps } from './expo-screen.js';
|
|
@@ -9,11 +11,24 @@ export declare function createExpoMessenger(options: {
|
|
|
9
11
|
getAccessToken: () => Promise<string>;
|
|
10
12
|
fetch?: typeof fetch;
|
|
11
13
|
notifications?: false;
|
|
14
|
+
extras?: false;
|
|
15
|
+
maps?: false;
|
|
16
|
+
calling?: false;
|
|
17
|
+
people?: () => Record<string, {
|
|
18
|
+
name: string;
|
|
19
|
+
avatarUrl?: string;
|
|
20
|
+
}>;
|
|
12
21
|
persistence?: false;
|
|
13
22
|
namespace?: string;
|
|
14
23
|
audioSession?: MessagingAudioSession;
|
|
15
24
|
isCallActive?: () => boolean;
|
|
16
25
|
}): {
|
|
26
|
+
signOut(): Promise<void>;
|
|
27
|
+
dispose(): void;
|
|
28
|
+
extras?: import("@robono/messaging").MessagingExtrasClient | undefined;
|
|
29
|
+
components?: {
|
|
30
|
+
TrackingMap: (props: import("./extras.js").TrackingMapProps) => React.JSX.Element;
|
|
31
|
+
} | undefined;
|
|
17
32
|
client: import("@robono/messaging").MessagingClient;
|
|
18
33
|
inbox: MessagingInboxClient;
|
|
19
34
|
storage: import("./media-controller.js").MessagingMediaStorage;
|
|
@@ -22,10 +37,26 @@ export declare function createExpoMessenger(options: {
|
|
|
22
37
|
start(open: (conversationId: string, messageId: string) => Promise<void>): Promise<void>;
|
|
23
38
|
enable(): Promise<void>;
|
|
24
39
|
} | undefined;
|
|
25
|
-
|
|
26
|
-
|
|
40
|
+
calling: {
|
|
41
|
+
updateContacts(next: Record<string, {
|
|
42
|
+
name: string;
|
|
43
|
+
avatarUrl?: string;
|
|
44
|
+
}>): void;
|
|
45
|
+
start: () => Promise<void>;
|
|
46
|
+
getSnapshot: () => import("@robono/calling-react-native/messenger").MessengerCallingState;
|
|
47
|
+
subscribe(fn: () => void): () => void;
|
|
48
|
+
isActive: () => boolean;
|
|
49
|
+
dismissUnanswered(): void;
|
|
50
|
+
notification(data: unknown, opened?: boolean): Promise<boolean>;
|
|
51
|
+
prime(): void;
|
|
52
|
+
call(conversationId: string, kind: "voice" | "video", group?: boolean): Promise<void>;
|
|
53
|
+
dispose(): Promise<void>;
|
|
54
|
+
} | undefined;
|
|
55
|
+
maps: LocationWorkspace | undefined;
|
|
27
56
|
};
|
|
28
57
|
export interface ExpoMessengerProps extends Omit<ExpoMessagingScreenProps, 'contact' | 'renderConversation' | 'onBack'> {
|
|
58
|
+
calling?: ReturnType<typeof createExpoMessengerCalling>;
|
|
59
|
+
maps?: LocationWorkspace;
|
|
29
60
|
notifications?: {
|
|
30
61
|
start: (open: (conversationId: string, messageId: string) => Promise<void>) => Promise<void>;
|
|
31
62
|
enable: () => Promise<void>;
|
|
@@ -41,4 +72,4 @@ export interface ExpoMessengerProps extends Omit<ExpoMessagingScreenProps, 'cont
|
|
|
41
72
|
onSelectConversation?: (conversation: ConversationSummary | null) => void;
|
|
42
73
|
}
|
|
43
74
|
/** Supplied inbox, group creation, conversation, settings and media screens. */
|
|
44
|
-
export declare function ExpoMessenger({ inbox, contacts, initialConversation, selectedConversation, onSelectConversation, notifications, ...props }: ExpoMessengerProps): React.JSX.Element;
|
|
75
|
+
export declare function ExpoMessenger({ inbox, contacts, initialConversation, selectedConversation, onSelectConversation, notifications, maps, calling, ...props }: ExpoMessengerProps): React.JSX.Element;
|
package/dist/expo-messenger.js
CHANGED
|
@@ -1,11 +1,18 @@
|
|
|
1
|
-
import { jsx as _jsx,
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useMessengerMaps, MessengerMapsNotice } from './maps-connection.js';
|
|
3
|
+
import { MessagingCallingScreen } from './calling.js';
|
|
4
|
+
import { createExpoMessengerCalling, useExpoMessengerCalling, ExpoMessengerCalls } from './expo-connected-calling.js';
|
|
5
|
+
import { isNativeCallAudioActive } from '@robono/calling-react-native/native';
|
|
6
|
+
import { View } from 'react-native';
|
|
7
|
+
import { createExpoMaps, RobonoMaps } from '@robono/location-react-native/expo';
|
|
8
|
+
import { createExpoMessagingExtras } from './expo-extras.js';
|
|
2
9
|
import { connectExpoMessagingNotifications } from './expo-notifications.js';
|
|
3
10
|
import { useEffect, useState, useSyncExternalStore } from 'react';
|
|
4
11
|
import { createMessagingTransport, defaultMessagingSettings, MessagingInboxClient } from '@robono/messaging';
|
|
5
12
|
import { ExpoMessagingScreen } from './expo-screen.js';
|
|
6
13
|
import { createExpoMessagingClient } from './expo-outbox.js';
|
|
7
14
|
import { createExpoMessagingAudioSession, createExpoMessagingStorage } from './expo-media.js';
|
|
8
|
-
import { MessagingProvider,
|
|
15
|
+
import { MessagingProvider, MessagingAccountMenu } from './index.js';
|
|
9
16
|
import { MessagingInbox } from './inbox.js';
|
|
10
17
|
import { MessagingGroupEditor } from './group-editor.js';
|
|
11
18
|
/** One account, one authenticated route, supplied storage and media-session adapters. */
|
|
@@ -16,9 +23,12 @@ export function createExpoMessenger(options) {
|
|
|
16
23
|
const media = new URL(endpoint);
|
|
17
24
|
media.searchParams.set('service', 'media');
|
|
18
25
|
const client = createExpoMessagingClient({ ...options, url: endpoint.href });
|
|
26
|
+
const mapConnection = options.maps === false ? undefined : createExpoMaps(options);
|
|
27
|
+
const connectedExtras = options.extras === false ? undefined : createExpoMessagingExtras(options);
|
|
19
28
|
const inbox = new MessagingInboxClient({ request: createMessagingTransport({ ...options, url: endpoint.href }) });
|
|
20
29
|
const storage = createExpoMessagingStorage({ url: media.href, getAccessToken: options.getAccessToken, ...(options.fetch ? { fetch: options.fetch } : {}) });
|
|
21
|
-
const
|
|
30
|
+
const calling = options.calling === false ? undefined : createExpoMessengerCalling({ ...options, inbox });
|
|
31
|
+
const audioSession = options.audioSession ?? createExpoMessagingAudioSession({ isCallActive: () => isNativeCallAudioActive() || calling?.isActive() === true || options.isCallActive?.() === true });
|
|
22
32
|
let connection = null, pending = null, closed = false;
|
|
23
33
|
let openNotification;
|
|
24
34
|
const notifications = options.notifications === false ? undefined : {
|
|
@@ -39,7 +49,7 @@ export function createExpoMessenger(options) {
|
|
|
39
49
|
catch {
|
|
40
50
|
return false;
|
|
41
51
|
} },
|
|
42
|
-
open: e => open(e.conversationId, e.messageId), refresh: () => inbox.refresh().then(() => { }), onError: () => { },
|
|
52
|
+
onCall: calling?.notification, open: e => open(e.conversationId, e.messageId), refresh: () => inbox.refresh().then(() => { }), onError: () => { },
|
|
43
53
|
});
|
|
44
54
|
if (closed) {
|
|
45
55
|
await n.signOut();
|
|
@@ -59,25 +69,26 @@ export function createExpoMessenger(options) {
|
|
|
59
69
|
throw Error('Notifications are not connected yet. Check your Apple/Google setup in Robono.'); if (!await connection.enable())
|
|
60
70
|
throw Error('Allow notifications in your phone’s app settings.'); },
|
|
61
71
|
};
|
|
62
|
-
return { client, inbox, storage, audioSession, notifications,
|
|
63
|
-
async signOut() { closed = true;
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
}
|
|
67
|
-
finally {
|
|
68
|
-
inbox.dispose();
|
|
69
|
-
await client.signOut();
|
|
70
|
-
} },
|
|
71
|
-
dispose() { closed = true; connection?.dispose(); inbox.dispose(); client.dispose(); },
|
|
72
|
+
return { client, inbox, storage, audioSession, notifications, calling, maps: mapConnection?.workspace, ...(connectedExtras ? { extras: connectedExtras.extras, components: { TrackingMap: connectedExtras.TrackingMap } } : {}),
|
|
73
|
+
async signOut() { 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
|
+
throw failed.reason; },
|
|
75
|
+
dispose() { closed = true; void calling?.dispose().catch(() => { }); mapConnection?.dispose(); void connectedExtras?.dispose().catch(() => { }); connection?.dispose(); inbox.dispose(); client.dispose(); },
|
|
72
76
|
};
|
|
73
77
|
}
|
|
74
78
|
/** Supplied inbox, group creation, conversation, settings and media screens. */
|
|
75
|
-
export function ExpoMessenger({ inbox, contacts = {}, initialConversation, selectedConversation, onSelectConversation, notifications, ...props }) {
|
|
79
|
+
export function ExpoMessenger({ inbox, contacts = {}, initialConversation, selectedConversation, onSelectConversation, notifications, maps, calling, ...props }) {
|
|
80
|
+
const callState = useExpoMessengerCalling(calling);
|
|
81
|
+
useEffect(() => { calling?.updateContacts(contacts); }, [calling, contacts]);
|
|
82
|
+
const chatState = useSyncExternalStore(props.client.subscribe, props.client.getSnapshot, props.client.getSnapshot);
|
|
83
|
+
const groupAllowed = chatState.conversation?.kind !== 'group' || !chatState.conversation.supergroup && (chatState.conversation.memberCount ?? 0) >= 2 && (chatState.conversation.memberCount ?? 0) <= 8;
|
|
76
84
|
const [internal, setInternal] = useState(initialConversation ?? null), [newGroup, setNewGroup] = useState(false);
|
|
77
85
|
const selected = selectedConversation === undefined ? internal : selectedConversation;
|
|
86
|
+
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;
|
|
78
87
|
const setSelected = (row) => { setInternal(row); onSelectConversation?.(row); };
|
|
88
|
+
const [showMaps, setShowMaps] = useState(false), mapConnection = useMessengerMaps(maps), mapsAvailable = mapConnection.available;
|
|
79
89
|
const inboxState = useSyncExternalStore(inbox.subscribe, inbox.getSnapshot, inbox.getSnapshot);
|
|
80
90
|
const open = (row, messageId) => {
|
|
91
|
+
setShowMaps(false);
|
|
81
92
|
setSelected(row);
|
|
82
93
|
void props.client.open(row.id).then(async () => { if (messageId && props.client.getSnapshot().conversationId === row.id)
|
|
83
94
|
await props.client.focusMessage(messageId); }).catch(() => { });
|
|
@@ -86,5 +97,5 @@ export function ExpoMessenger({ inbox, contacts = {}, initialConversation, selec
|
|
|
86
97
|
open(row, message); }).catch(() => { }); }, [notifications, inbox]);
|
|
87
98
|
const account = notifications ? { name: props.account?.name ?? 'Your account', ...props.account, onEnableNotifications: () => notifications.enable() } : props.account;
|
|
88
99
|
const contact = selected?.kind === 'group' ? { name: selected.name ?? 'Group' } : contacts[selected?.contactId ?? ''] ?? { name: selected?.contactId ?? 'Chat' };
|
|
89
|
-
return _jsx(ExpoMessagingScreen, { ...props, account: account, contact: contact, participants: props.participants ?? contacts, extras: selected ? props.extras : undefined, callActions: selected ? { ...props.callActions, phoneNumber: contacts[selected.contactId ?? '']?.phoneNumber ?? props.callActions?.phoneNumber } : undefined, onBack: () => { props.client.stop(); setSelected(null); void inbox.refresh().catch(() => { }); }, renderConversation: screen => _jsx(
|
|
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 })] });
|
|
90
101
|
}
|
|
@@ -10,8 +10,9 @@ export interface ExpoMessagingNotificationOptions {
|
|
|
10
10
|
refresh: () => void | Promise<void>;
|
|
11
11
|
onError: (error: Error) => void;
|
|
12
12
|
fetch?: typeof fetch;
|
|
13
|
+
onCall?: (data: unknown, opened: boolean) => Promise<boolean>;
|
|
13
14
|
}
|
|
14
|
-
/** One restored account. Never prompts on mount, replaces a global handler or
|
|
15
|
+
/** One restored account. Never prompts on mount, replaces a global handler or opens an unverified call. */
|
|
15
16
|
export declare function connectExpoMessagingNotifications(options: ExpoMessagingNotificationOptions): Promise<{
|
|
16
17
|
ready: Promise<void>;
|
|
17
18
|
/** Call only after the user chooses Enable notifications in your interface. */
|
|
@@ -3,7 +3,7 @@ import * as Crypto from 'expo-crypto';
|
|
|
3
3
|
import * as SecureStore from 'expo-secure-store';
|
|
4
4
|
import { Platform } from 'react-native';
|
|
5
5
|
import { createMessagingNotificationRouter, parseMessagingNotification } from '@robono/messaging';
|
|
6
|
-
/** One restored account. Never prompts on mount, replaces a global handler or
|
|
6
|
+
/** One restored account. Never prompts on mount, replaces a global handler or opens an unverified call. */
|
|
7
7
|
export async function connectExpoMessagingNotifications(options) {
|
|
8
8
|
if (!['ios', 'android'].includes(Platform.OS))
|
|
9
9
|
throw Error('Mobile notification registration requires iPhone or Android.');
|
|
@@ -36,7 +36,7 @@ export async function connectExpoMessagingNotifications(options) {
|
|
|
36
36
|
const work = registerQueue.then(async () => {
|
|
37
37
|
if (disposed || closing)
|
|
38
38
|
return;
|
|
39
|
-
const data = await request({ operation: 'register-device', deviceId, provider: Platform.OS === 'ios' ? 'apns' : 'fcm', token: String(token.data) });
|
|
39
|
+
const data = await request({ operation: 'register-device', calling: !!options.onCall, deviceId, provider: Platform.OS === 'ios' ? 'apns' : 'fcm', token: String(token.data) });
|
|
40
40
|
if (data.accountScope !== status.accountScope)
|
|
41
41
|
throw Error('The signed-in account changed. Reconnect notifications for the new account.');
|
|
42
42
|
});
|
|
@@ -44,11 +44,19 @@ export async function connectExpoMessagingNotifications(options) {
|
|
|
44
44
|
return work;
|
|
45
45
|
};
|
|
46
46
|
const tokenListener = Notifications.addPushTokenListener(token => { void register(token).catch(options.onError); });
|
|
47
|
-
const
|
|
47
|
+
const handle = async (data, opened = false) => { if (!data || disposed || closing)
|
|
48
|
+
return false; if (data.accountScope === accountScope && ['calling.incoming', 'calling.dismissed', 'calling.group_incoming', 'calling.group_dismissed'].includes(String(data.event)) && options.onCall) {
|
|
49
|
+
if (String(data.event).endsWith('dismissed')) {
|
|
50
|
+
const shown = await Notifications.getPresentedNotificationsAsync();
|
|
51
|
+
await Promise.all(shown.filter(n => n.request.content.data?.accountScope === accountScope && n.request.content.data?.call_id === data.call_id).map(n => Notifications.dismissNotificationAsync(n.request.identifier)));
|
|
52
|
+
}
|
|
53
|
+
return options.onCall(data, opened);
|
|
54
|
+
} return router.handle(data, opened); };
|
|
55
|
+
const received = Notifications.addNotificationReceivedListener(notification => { void handle(notification.request.content.data).catch(options.onError); });
|
|
48
56
|
const response = async (value) => {
|
|
49
57
|
if (disposed || closing || !value || value.actionIdentifier !== Notifications.DEFAULT_ACTION_IDENTIFIER)
|
|
50
58
|
return;
|
|
51
|
-
if (await
|
|
59
|
+
if (await handle(value.notification.request.content.data, true)) {
|
|
52
60
|
const last = await Notifications.getLastNotificationResponseAsync();
|
|
53
61
|
if (!disposed && last?.notification.request.identifier === value.notification.request.identifier)
|
|
54
62
|
await Notifications.clearLastNotificationResponseAsync();
|
|
@@ -56,8 +64,11 @@ export async function connectExpoMessagingNotifications(options) {
|
|
|
56
64
|
};
|
|
57
65
|
const opened = Notifications.addNotificationResponseReceivedListener(value => { void response(value).catch(options.onError); });
|
|
58
66
|
const ready = (async () => {
|
|
59
|
-
if (Platform.OS === 'android')
|
|
67
|
+
if (Platform.OS === 'android') {
|
|
60
68
|
await Notifications.setNotificationChannelAsync('robono-messages', { name: 'Messages', importance: Notifications.AndroidImportance.HIGH });
|
|
69
|
+
if (options.onCall)
|
|
70
|
+
await Notifications.setNotificationChannelAsync('robono-calls', { name: 'Calls', importance: Notifications.AndroidImportance.HIGH });
|
|
71
|
+
}
|
|
61
72
|
if ((await Notifications.getPermissionsAsync()).granted)
|
|
62
73
|
await register(await Notifications.getDevicePushTokenAsync());
|
|
63
74
|
await response(await Notifications.getLastNotificationResponseAsync());
|
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 * as Haptics from 'expo-haptics';
|
|
3
|
+
import { Vibration } from 'react-native';
|
|
2
4
|
import { MessagingPopupHeader, messagingPopupBackdrop } from './popup.js';
|
|
3
5
|
import { ExpoMessagingPrivacyLayer } from './expo-privacy.js';
|
|
4
6
|
import { ExpoSocialPlayer } from './expo-social.js';
|
|
@@ -489,6 +491,10 @@ export function ExpoMessagingScreen({ storage, audioSession, renderConversation,
|
|
|
489
491
|
const attachmentPreview = useCallback(async (m) => { if (m.media && m.kind !== 'text' && m.kind !== 'poll')
|
|
490
492
|
await restore(m.media, m.kind); return thumbnails.preview(m); }, [restore, thumbnails]);
|
|
491
493
|
const screenProps = { ...props, requestedMessageAction,
|
|
494
|
+
onQuoteReady: props.onQuoteReady ?? (() => { if (Platform.OS === 'android')
|
|
495
|
+
Vibration.vibrate(8);
|
|
496
|
+
else if (Platform.OS === 'ios')
|
|
497
|
+
void Haptics.selectionAsync().catch(() => { }); }),
|
|
492
498
|
copyText: props.copyText ?? (async (text) => { await Clipboard.setStringAsync(text); }),
|
|
493
499
|
components: { PrivacyLayer: ExpoMessagingPrivacyLayer, SocialPlayer, TranslationVoice, VoiceMessage: InlineVoice, VoiceComposer: InlineComposer, VoiceAttachment, ...props.components },
|
|
494
500
|
attachmentPreview: props.attachmentPreview ?? attachmentPreview,
|
|
@@ -503,7 +509,7 @@ export function ExpoMessagingScreen({ storage, audioSession, renderConversation,
|
|
|
503
509
|
else {
|
|
504
510
|
message.current = null;
|
|
505
511
|
chosen.current = options.attachmentId;
|
|
506
|
-
media = !options.source && !options.attachmentId && options.intent !== 'preview' && (options.maxCount ?? 1) > 1 ? await controller.pickMany(kind, options.maxCount) : await controller.pick(kind, options.source, options.source === 'call-video' ? 'preview' : options.intent === 'preview' ? 'preview' : 'attach');
|
|
512
|
+
media = !options.source && !options.attachmentId && options.intent !== 'preview' && ((options.maxCount ?? 1) > 1 || options.mixedLibrary) ? await controller.pickMany(kind, options.maxCount ?? 1, options.mixedLibrary) : await controller.pick(kind, options.source, options.source === 'call-video' ? 'preview' : options.intent === 'preview' ? 'preview' : 'attach');
|
|
507
513
|
}
|
|
508
514
|
const selection = media ? (Array.isArray(media) ? media : [media]) : [];
|
|
509
515
|
try {
|
|
@@ -512,7 +518,7 @@ export function ExpoMessagingScreen({ storage, audioSession, renderConversation,
|
|
|
512
518
|
const file = recorder.getPrepared(item.assetId) ?? controller.getPrepared(item.assetId);
|
|
513
519
|
if (!file)
|
|
514
520
|
throw Error('The selected file is unavailable.');
|
|
515
|
-
await outbox.saveFile(conversationId, item, file, kind);
|
|
521
|
+
await outbox.saveFile(conversationId, item, file, (options.mixedLibrary || options.source === 'camera') ? (item.mimeType.startsWith('video/') ? 'video' : 'image') : kind);
|
|
516
522
|
}
|
|
517
523
|
}
|
|
518
524
|
catch (error) {
|
|
@@ -530,7 +536,8 @@ export function ExpoMessagingScreen({ storage, audioSession, renderConversation,
|
|
|
530
536
|
throw Error('This saved attachment is unavailable.'); const intent = ++openingIntent.current; await stopVoicePlayers(); const file = await copyMessagingCache(await saved.file(record)); if (intent !== openingIntent.current) {
|
|
531
537
|
await createExpoMessagingDevice(audioSession).remove(file);
|
|
532
538
|
return;
|
|
533
|
-
} opened.current = null; message.current = null; await controller.previewLocal(file, record.message.kind); },
|
|
539
|
+
} opened.current = null; message.current = null; await controller.previewLocal(file, record.message.kind); },
|
|
540
|
+
};
|
|
534
541
|
const mediaReady = (stage, uri) => { const event = opened.current; if (!event || controller.getSnapshot().phase !== 'viewing' || controller.getSnapshot().file?.uri !== uri)
|
|
535
542
|
return; if (stage !== 'video-frame' && message.current?.conversationId === current.current.client.getSnapshot().conversationId)
|
|
536
543
|
void current.current.client.consumeMessage(event.messageId).catch(() => { }); if (event.reported)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { prepareBackgroundTracking, captureBackgroundTracking, endBackgroundAccount } from '@robono/location-react-native/expo';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { prepareBackgroundTracking, captureBackgroundTracking, endBackgroundAccount } from '@robono/location-react-native/expo';
|
package/dist/extras-lifecycle.js
CHANGED
|
@@ -7,9 +7,9 @@ export function retainExtrasLifecycle(extras) {
|
|
|
7
7
|
if (!entry) {
|
|
8
8
|
const next = { users: 0, release: () => { } };
|
|
9
9
|
const subscription = AppState.addEventListener('change', state => {
|
|
10
|
-
if (state
|
|
11
|
-
extras.
|
|
12
|
-
else if (next.users)
|
|
10
|
+
if (state === 'background')
|
|
11
|
+
extras.background();
|
|
12
|
+
else if (state === 'active' && next.users)
|
|
13
13
|
void extras.refresh();
|
|
14
14
|
});
|
|
15
15
|
const cleanup = () => { if (!next.users && !extras.hasActiveTracking()) {
|