@robono/messaging-react-native 0.11.0 → 0.12.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 +38 -4
- package/SBOM.spdx.json +194 -98
- package/dist/account.d.ts +1 -0
- package/dist/account.js +1 -1
- package/dist/expo-media.js +18 -4
- package/dist/expo-messenger.d.ts +5 -0
- package/dist/expo-messenger.js +3 -3
- package/dist/expo-screen.d.ts +10 -2
- package/dist/expo-screen.js +15 -7
- package/dist/expo-thumbnails.d.ts +0 -1
- package/dist/expo-thumbnails.js +17 -26
- package/dist/expo.d.ts +1 -0
- package/dist/expo.js +1 -0
- package/dist/group-editor.d.ts +1 -0
- package/dist/group-editor.js +1 -1
- package/dist/inbox.d.ts +1 -0
- package/dist/inbox.js +2 -2
- package/dist/index.d.ts +10 -1
- package/dist/index.js +14 -12
- package/dist/media-controller.d.ts +9 -2
- package/dist/media-controller.js +15 -10
- package/dist/media-screening.js +9 -3
- package/dist/preview-cache.d.ts +24 -0
- package/dist/preview-cache.js +59 -0
- package/dist/recording-permission.d.ts +12 -0
- package/dist/recording-permission.js +34 -0
- package/package.json +4 -7
package/dist/account.d.ts
CHANGED
|
@@ -10,6 +10,7 @@ export interface MessagingAccount {
|
|
|
10
10
|
email?: string;
|
|
11
11
|
onEnableNotifications?: () => void | Promise<void>;
|
|
12
12
|
onEditProfile?: () => void | Promise<void>;
|
|
13
|
+
onDeleteAccount?: () => void | Promise<void>;
|
|
13
14
|
onSignOut?: () => void | Promise<void>;
|
|
14
15
|
}
|
|
15
16
|
export declare function MessagingAccountScreen({ account, companyName, onClose }: {
|
package/dist/account.js
CHANGED
|
@@ -36,5 +36,5 @@ export function MessagingAccountScreen({ account, companyName, onClose }) {
|
|
|
36
36
|
}
|
|
37
37
|
else
|
|
38
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 })] }) }) });
|
|
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?.onDeleteAccount && _jsx(Pressable, { accessibilityRole: "button", accessibilityLabel: uiText("Delete account"), disabled: busy, onPress: () => begin(account.onDeleteAccount), style: { backgroundColor: '#111111', borderRadius: 12, minHeight: 52, paddingHorizontal: 16, justifyContent: 'center' }, children: _jsx(Text, { style: { color: '#ffb5a3', fontSize: 16 }, children: uiText("Delete account") }) }), 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 })] }) }) });
|
|
40
40
|
}
|
package/dist/expo-media.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { MessagingError } from '@robono/messaging';
|
|
2
2
|
import { awaitMediaScreening } from './media-screening';
|
|
3
|
+
import { AppState } from 'react-native';
|
|
4
|
+
import { prepareRecordingPermission } from './recording-permission.js';
|
|
3
5
|
import { AudioModule, RecordingPresets, createAudioPlayer, setAudioModeAsync } from 'expo-audio';
|
|
4
6
|
import * as Picker from 'expo-image-picker';
|
|
5
7
|
import * as Documents from 'expo-document-picker';
|
|
@@ -107,6 +109,7 @@ async function recordingDuration(uri) {
|
|
|
107
109
|
const extensionMimes = { jpg: 'image/jpeg', jpeg: 'image/jpeg', png: 'image/png', gif: 'image/gif', webp: 'image/webp', heic: 'image/heic', heif: 'image/heif', avif: 'image/avif', mp4: 'video/mp4', mov: 'video/quicktime', webm: 'video/webm', '3gp': 'video/3gpp', pdf: 'application/pdf', txt: 'text/plain', csv: 'text/csv', zip: 'application/zip', doc: 'application/msword', docx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', xls: 'application/vnd.ms-excel', xlsx: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', ppt: 'application/vnd.ms-powerpoint', pptx: 'application/vnd.openxmlformats-officedocument.presentationml.presentation' };
|
|
108
110
|
function mime(value, name) { return value?.toLowerCase() || extensionMimes[name.split('.').at(-1)?.toLowerCase() ?? ''] || 'application/octet-stream'; }
|
|
109
111
|
export function createExpoMessagingDevice(audio) {
|
|
112
|
+
let permissionPending = false;
|
|
110
113
|
let recorder = null, release = null;
|
|
111
114
|
let recordingDeadline = 0;
|
|
112
115
|
let starting = null;
|
|
@@ -163,20 +166,31 @@ export function createExpoMessagingDevice(audio) {
|
|
|
163
166
|
return {
|
|
164
167
|
async pick(kind, settings, signal, source) { return (await chooseFiles(kind, settings, signal, source, 1))[0] ?? null; },
|
|
165
168
|
pickMany: (kind, settings, signal, maximum, mixedLibrary) => chooseFiles(kind, settings, signal, undefined, maximum, mixedLibrary),
|
|
169
|
+
isRequestingRecordingPermission: () => permissionPending,
|
|
166
170
|
async startRecording(maximum, signal) {
|
|
167
171
|
if (starting || recorder)
|
|
168
172
|
throw Error('A recording is already in progress.');
|
|
169
173
|
const work = (async () => {
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
+
permissionPending = true;
|
|
175
|
+
try {
|
|
176
|
+
await prepareRecordingPermission({
|
|
177
|
+
read: () => AudioModule.getRecordingPermissionsAsync(), request: () => AudioModule.requestRecordingPermissionsAsync(),
|
|
178
|
+
state: () => AppState.currentState, subscribe: changed => { const subscription = AppState.addEventListener('change', changed); return () => subscription.remove(); },
|
|
179
|
+
}, signal);
|
|
180
|
+
}
|
|
181
|
+
finally {
|
|
182
|
+
permissionPending = false;
|
|
183
|
+
}
|
|
174
184
|
release = await audio.acquire('recording');
|
|
175
185
|
try {
|
|
176
186
|
check(signal);
|
|
187
|
+
if (AppState.currentState !== 'active')
|
|
188
|
+
throw Error('Return to the conversation to record a voice message.');
|
|
177
189
|
recorder = new AudioModule.AudioRecorder({ ...RecordingPresets.HIGH_QUALITY, isMeteringEnabled: true });
|
|
178
190
|
await recorder.prepareToRecordAsync({ ...RecordingPresets.HIGH_QUALITY, isMeteringEnabled: true });
|
|
179
191
|
check(signal); // Leave room for AAC encoder/container padding; the backend checks final file duration.
|
|
192
|
+
if (AppState.currentState !== 'active')
|
|
193
|
+
throw Error('Return to the conversation to record a voice message.');
|
|
180
194
|
const captureMs = Math.max(100, maximum - 100);
|
|
181
195
|
recorder.record({ forDuration: captureMs / 1000 });
|
|
182
196
|
recordingDeadline = Date.now() + captureMs;
|
package/dist/expo-messenger.d.ts
CHANGED
|
@@ -118,11 +118,16 @@ export interface ExpoMessengerProps extends Omit<ExpoMessagingScreenProps, 'cont
|
|
|
118
118
|
start: (open: (conversationId: string, messageId: string) => Promise<void>) => Promise<void>;
|
|
119
119
|
enable: () => Promise<void>;
|
|
120
120
|
};
|
|
121
|
+
/** Let the host choose/create an authorized contact; no customer directory is guessed. */
|
|
122
|
+
onNewConversation?: () => void;
|
|
123
|
+
/** Custom inbox dialogs share the same bounded popup host. */
|
|
124
|
+
inboxOverlay?: React.ReactNode;
|
|
121
125
|
inbox: MessagingInboxClient;
|
|
122
126
|
contacts?: Record<string, {
|
|
123
127
|
name: string;
|
|
124
128
|
avatarUrl?: string;
|
|
125
129
|
phoneNumber?: string;
|
|
130
|
+
groupEligible?: boolean;
|
|
126
131
|
}>;
|
|
127
132
|
initialConversation?: ConversationSummary;
|
|
128
133
|
selectedConversation?: ConversationSummary | null;
|
package/dist/expo-messenger.js
CHANGED
|
@@ -160,7 +160,7 @@ function ExpoMessengerContent({ inbox, contacts = {}, initialConversation, selec
|
|
|
160
160
|
const subscription = AppState.addEventListener('change', sync);
|
|
161
161
|
return () => { subscription.remove(); props.client.stop(); };
|
|
162
162
|
}, [selected?.id, props.client, props.active]);
|
|
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;
|
|
163
|
+
const autoCalls = calling && selected && selected.contactId !== props.userId && 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;
|
|
164
164
|
const setSelected = (row) => { setInternal(row); onSelectConversation?.(row); };
|
|
165
165
|
const [showMaps, setShowMaps] = useState(false), mapConnection = useMessengerMaps(maps), mapsAvailable = mapConnection.available;
|
|
166
166
|
const inboxState = useSyncExternalStore(inbox.subscribe, inbox.getSnapshot, inbox.getSnapshot);
|
|
@@ -174,6 +174,6 @@ function ExpoMessengerContent({ inbox, contacts = {}, initialConversation, selec
|
|
|
174
174
|
open(row, message); }).catch(() => { }); }, [notifications, inbox]);
|
|
175
175
|
const notificationAccount = notifications ? { name: props.account?.name ?? 'Your account', ...props.account, onEnableNotifications: () => notifications.enable() } : props.account;
|
|
176
176
|
const account = calling?.deviceCheck ? { name: 'Your account', ...notificationAccount, deviceCheck: calling.deviceCheck, renderCallPreview: (stream) => renderNativeCallVideo({ stream, local: true, style: { width: '100%', height: '100%' } }) } : notificationAccount;
|
|
177
|
-
const contact = selected?.kind === 'group' ? { name: selected.name ?? 'Group' } : contacts[selected?.contactId ?? ''] ?? { name: selected?.contactId ?? 'Chat' };
|
|
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 })] });
|
|
177
|
+
const contact = selected?.contactId === props.userId ? { name: 'Notes to self' } : selected?.kind === 'group' ? { name: selected.name ?? 'Group' } : contacts[selected?.contactId ?? ''] ?? { name: selected?.contactId ?? 'Chat' };
|
|
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 && selected.contactId !== props.userId ? { ...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, { onNewConversation: props.onNewConversation, 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); } }), props.inboxOverlay] }) }), _jsx(ExpoMessengerCalls, { calling: calling, state: callState })] });
|
|
179
179
|
}
|
package/dist/expo-screen.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export { createExpoMessagingClient } from './expo-outbox.js';
|
|
2
2
|
import React from 'react';
|
|
3
|
-
import { type MessagingAppearance } from '@robono/messaging';
|
|
3
|
+
import { type MessagingAppearance, type MessagingMessage } from '@robono/messaging';
|
|
4
4
|
import { type MessagingScreenProps } from './index.js';
|
|
5
5
|
import { MessagingMediaController, type LocalMessagingMedia, type MessagingMediaStorage } from './media-controller.js';
|
|
6
6
|
import { type MediaReadyStage } from './media-readiness.js';
|
|
@@ -9,7 +9,13 @@ export { createExpoMessagingAudioSession, createExpoMessagingDevice, createExpoM
|
|
|
9
9
|
export type { MessagingAudioSession } from './expo-media.js';
|
|
10
10
|
export type { LocalMessagingMedia, MessagingMediaStorage, MediaTransferOptions } from './media-controller.js';
|
|
11
11
|
export { MessagingMediaController } from './media-controller.js';
|
|
12
|
-
export
|
|
12
|
+
export type MessagingAttachmentDownload = (input: {
|
|
13
|
+
message: MessagingMessage;
|
|
14
|
+
file: LocalMessagingMedia;
|
|
15
|
+
signal: AbortSignal;
|
|
16
|
+
}) => Promise<void>;
|
|
17
|
+
export declare function MessagingMediaOverlay({ controller, audio, appearance, inlineVoice, inlineRecorder, viewerActions, onDismiss, onDownloadAttachment }: {
|
|
18
|
+
onDownloadAttachment?: MessagingAttachmentDownload;
|
|
13
19
|
onDismiss?: () => void;
|
|
14
20
|
viewerActions?: () => {
|
|
15
21
|
onForward?: () => void;
|
|
@@ -24,6 +30,8 @@ export declare function MessagingMediaOverlay({ controller, audio, appearance, i
|
|
|
24
30
|
export interface ExpoMessagingScreenProps extends Omit<MessagingScreenProps, 'pickAttachment' | 'openAttachment'> {
|
|
25
31
|
storage: MessagingMediaStorage;
|
|
26
32
|
audioSession: MessagingAudioSession;
|
|
33
|
+
/** Save an authorized private copy to your own destination. Copy it before resolving; Robono removes its temporary copy afterward. Respect signal when the viewer/account closes. */
|
|
34
|
+
onDownloadAttachment?: MessagingAttachmentDownload;
|
|
27
35
|
/** Optional timing callback from native image/frame/playback readiness, measured from opening. */
|
|
28
36
|
onMediaReady?: (event: {
|
|
29
37
|
messageId: string;
|
package/dist/expo-screen.js
CHANGED
|
@@ -140,7 +140,7 @@ function VoicePlayer({ file, audio, design, inline, draft, autoPlay = false }) {
|
|
|
140
140
|
setLoaded(player.isLoaded);
|
|
141
141
|
restorePaused();
|
|
142
142
|
const subscription = player.addListener('playbackStatusUpdate', status => { if (!scope.alive)
|
|
143
|
-
return; setLoaded(status.isLoaded); if (status.error) {
|
|
143
|
+
return; setLoaded(status.isLoaded); if ('error' in status && status.error) {
|
|
144
144
|
if (!recoverStream())
|
|
145
145
|
fail(Error('The recording could not be played. Try again.'));
|
|
146
146
|
return;
|
|
@@ -332,7 +332,7 @@ function FileViewer({ file, kind, audio, design }) {
|
|
|
332
332
|
} })();
|
|
333
333
|
} }), !!error && _jsx(Text, { accessibilityRole: "alert", style: { color: design.colors.foreground }, children: error })] });
|
|
334
334
|
}
|
|
335
|
-
export function MessagingMediaOverlay({ controller, audio, appearance, inlineVoice = false, inlineRecorder = false, viewerActions, onDismiss }) {
|
|
335
|
+
export function MessagingMediaOverlay({ controller, audio, appearance, inlineVoice = false, inlineRecorder = false, viewerActions, onDismiss, onDownloadAttachment }) {
|
|
336
336
|
const { text: uiText, locale: uiLocale, number: uiNumber, count: uiCount } = useRobonoLocale();
|
|
337
337
|
const state = useSyncExternalStore(controller.subscribe, controller.getSnapshot, controller.getSnapshot);
|
|
338
338
|
const c = appearance.colors;
|
|
@@ -347,7 +347,14 @@ export function MessagingMediaOverlay({ controller, audio, appearance, inlineVoi
|
|
|
347
347
|
if ((typeof inlineVoice === 'function' ? inlineVoice() : inlineVoice) && state.kind === 'voice' && ['opening', 'viewing', 'failed'].includes(state.phase) && !review)
|
|
348
348
|
return null;
|
|
349
349
|
const viewer = state.file && (review || state.phase === 'viewing') ? _jsx(MediaViewer, { expanded: expanded, onExpandedChange: setExpanded, kind: state.kind, uri: state.file.uri, title: review ? uiText("Preview attachment") : state.kind === 'image' ? uiText("Photo") : state.kind === 'video' ? uiText("Video") : state.kind === 'voice' ? uiText("Voice message") : uiText("File"), subtitle: state.file.fileName, fileName: state.file.fileName, byteSize: state.file.byteSize, error: state.error ?? fileError, onClose: () => { setFileError(''); void controller.cancel(); }, reviewAction: review ? (state.phase === 'failed' ? 'Retry upload' : 'Add attachment') : undefined, onReview: review ? () => void controller.send() : undefined, ...(!review ? viewerActions?.() : {}), downloading: downloading, onDownload: !review && appearance.showDownloadButton === true && ['image', 'video', 'document'].includes(state.kind) ? () => { if (saving.current || !state.file)
|
|
350
|
-
return; saving.current = true; setDownloading(true); setFileError(''); void controller.exportFile(async (file
|
|
350
|
+
return; saving.current = true; setDownloading(true); setFileError(''); void controller.exportFile(async (file, context) => { if (onDownloadAttachment) {
|
|
351
|
+
if (!context.message)
|
|
352
|
+
throw Error('The attachment is unavailable.');
|
|
353
|
+
await onDownloadAttachment({ file, message: context.message, signal: context.signal });
|
|
354
|
+
}
|
|
355
|
+
else
|
|
356
|
+
await saveMessagingAttachment(file, state.kind); if (!context.signal.aborted)
|
|
357
|
+
mediaReady('file-opened', file.uri); }, { reauthorize: !!onDownloadAttachment }).catch(e => setFileError(e instanceof Error ? e.message : uiText("Could not save the attachment."))).finally(() => { saving.current = false; setDownloading(false); }); } : undefined, onShare: !review ? () => { void controller.exportFile(async (file) => { await Sharing.shareAsync(file.uri, { mimeType: file.mimeType, dialogTitle: file.fileName }); mediaReady('file-opened', state.file.uri); }).catch(() => setFileError(uiText("Could not share this attachment."))); } : undefined, onFileAction: () => { void (async () => { try {
|
|
351
358
|
if (!await Sharing.isAvailableAsync())
|
|
352
359
|
throw Error('File opening is unavailable.');
|
|
353
360
|
await Sharing.shareAsync(state.file.uri, { mimeType: state.file.mimeType, dialogTitle: state.file.fileName });
|
|
@@ -415,11 +422,12 @@ function ExpoMessagingScreenContent({ storage, audioSession, renderConversation,
|
|
|
415
422
|
return copy;
|
|
416
423
|
}, [controller, recorder, props.client, outbox, audioSession]);
|
|
417
424
|
const prepare = async (kind, media) => { await restore(media, kind); const owner = recorder.getPrepared(media.assetId) ? recorder : controller; if (!owner.getPrepared(media.assetId))
|
|
418
|
-
return media; const uploaded = await owner.uploadPrepared(media.assetId, kind);
|
|
425
|
+
return media; const uploaded = await owner.uploadPrepared(media.assetId, kind); return uploaded; };
|
|
419
426
|
useEffect(() => props.client.setAttachmentPreparer(async (content) => { if (!content.media || content.kind === 'text' || content.kind === 'poll' || !await restore(content.media, content.kind))
|
|
420
427
|
throw Error('The saved attachment is unavailable. Remove it and choose it again.'); return { ...content, media: await prepare(content.kind, content.media) }; }), [props.client, controller, recorder, outbox]);
|
|
421
428
|
const thumbnails = useMemo(() => createExpoThumbnails(recent.storage, id => controller.getPrepared(id) ?? recorder.getPrepared(id), createExpoMessagingDevice(audioSession).remove), [recent, controller, recorder, audioSession]);
|
|
422
|
-
|
|
429
|
+
const thumbnailAccess = JSON.stringify([state.settings?.enabled, state.settings?.features, state.conversation?.canSend, !!state.error]);
|
|
430
|
+
useEffect(() => () => thumbnails.clear(), [thumbnails, state.conversationId, thumbnailAccess, props.active]);
|
|
423
431
|
const cacheDisposal = useRef(new Map());
|
|
424
432
|
useEffect(() => {
|
|
425
433
|
clearTimeout(cacheDisposal.current.get(recent));
|
|
@@ -547,7 +555,7 @@ function ExpoMessagingScreenContent({ storage, audioSession, renderConversation,
|
|
|
547
555
|
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) {
|
|
548
556
|
await createExpoMessagingDevice(audioSession).remove(file);
|
|
549
557
|
return;
|
|
550
|
-
} opened.current = null; message.current = null; await controller.previewLocal(file, record.message.kind); },
|
|
558
|
+
} opened.current = null; message.current = null; await controller.previewLocal(file, record.message.kind, record.message); },
|
|
551
559
|
};
|
|
552
560
|
const mediaReady = (stage, uri) => { const event = opened.current; if (!event || controller.getSnapshot().phase !== 'viewing' || controller.getSnapshot().file?.uri !== uri)
|
|
553
561
|
return; if (stage !== 'video-frame' && message.current?.conversationId === current.current.client.getSnapshot().conversationId)
|
|
@@ -556,7 +564,7 @@ function ExpoMessagingScreenContent({ storage, audioSession, renderConversation,
|
|
|
556
564
|
current.current.onMediaReady?.({ messageId: event.messageId, kind: event.kind, stage, elapsedMs: Date.now() - event.started });
|
|
557
565
|
}
|
|
558
566
|
catch { } };
|
|
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: () => {
|
|
567
|
+
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, { onDownloadAttachment: props.onDownloadAttachment, onDismiss: viewerDismissed, viewerActions: () => {
|
|
560
568
|
const m = message.current;
|
|
561
569
|
if (!m || !state.messages.some(row => row.id === m.id && !row.deleted))
|
|
562
570
|
return {};
|
|
@@ -5,5 +5,4 @@ import type { LocalMessagingMedia, MessagingMediaStorage } from './media-control
|
|
|
5
5
|
export declare function createExpoThumbnails(storage: MessagingMediaStorage, local: (id: string) => LocalMessagingMedia | undefined, remove: (file: LocalMessagingMedia) => Promise<void>): {
|
|
6
6
|
preview: (message: MessagingMessage) => Promise<{}>;
|
|
7
7
|
clear: () => void;
|
|
8
|
-
alias: (from: string, to: string) => void;
|
|
9
8
|
};
|
package/dist/expo-thumbnails.js
CHANGED
|
@@ -1,14 +1,12 @@
|
|
|
1
|
+
import { MessagingPreviewCache } from './preview-cache.js';
|
|
1
2
|
import * as ImageManipulator from 'expo-image-manipulator';
|
|
2
3
|
import * as VideoThumbnails from 'expo-video-thumbnails';
|
|
3
4
|
import * as FS from 'expo-file-system/legacy';
|
|
4
5
|
/** Small local previews, with bounded downloads and memory. Large originals stay
|
|
5
6
|
* tap-to-open until the storage service supplies a dedicated thumbnail. */
|
|
6
7
|
export function createExpoThumbnails(storage, local, remove) {
|
|
7
|
-
|
|
8
|
-
const
|
|
9
|
-
let queue = Promise.resolve();
|
|
10
|
-
function clear() { generation++; cache.clear(); for (const c of abort)
|
|
11
|
-
c.abort(); abort.clear(); }
|
|
8
|
+
const cache = new MessagingPreviewCache();
|
|
9
|
+
const clear = () => cache.clear();
|
|
12
10
|
function preview(message) {
|
|
13
11
|
const media = message.media;
|
|
14
12
|
if (!media || message.deleted)
|
|
@@ -17,52 +15,45 @@ export function createExpoThumbnails(storage, local, remove) {
|
|
|
17
15
|
return Promise.resolve({ waveform: media.waveform });
|
|
18
16
|
if (!['image', 'video'].includes(message.kind))
|
|
19
17
|
return Promise.resolve({});
|
|
20
|
-
const id = media.assetId,
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
const g = generation;
|
|
24
|
-
const work = queue.then(async () => {
|
|
25
|
-
if (g !== generation)
|
|
18
|
+
const id = media.assetId, key = JSON.stringify([message.conversationId, id, message.revision, media]);
|
|
19
|
+
return cache.get(key, async (signal) => {
|
|
20
|
+
if (signal.aborted)
|
|
26
21
|
return {};
|
|
27
22
|
let file = local(id), downloaded = false, poster, thumbnail;
|
|
28
|
-
|
|
29
|
-
|
|
23
|
+
// An optimistic bubble has no server message ID yet. Only its local file can
|
|
24
|
+
// supply a preview until acknowledgement replaces the pending bubble.
|
|
25
|
+
if (!file && message.id.startsWith('pending:'))
|
|
26
|
+
return {};
|
|
30
27
|
try {
|
|
31
28
|
let dedicated = false;
|
|
32
29
|
if (!file && storage.thumbnail) {
|
|
33
|
-
file = await storage.thumbnail(message, { signal
|
|
30
|
+
file = await storage.thumbnail(message, { signal, onProgress: () => { } }) ?? undefined;
|
|
34
31
|
dedicated = !!file;
|
|
35
32
|
downloaded = !!file;
|
|
36
33
|
}
|
|
37
34
|
if (!file) {
|
|
38
35
|
if (media.byteSize > 2 * 1024 * 1024)
|
|
39
36
|
return {};
|
|
40
|
-
file = await storage.download(message, { signal:
|
|
37
|
+
file = await storage.download(message, { signal: signal, onProgress: () => { } });
|
|
41
38
|
downloaded = true;
|
|
42
39
|
}
|
|
43
|
-
if (
|
|
40
|
+
if (signal.aborted)
|
|
44
41
|
return {};
|
|
45
42
|
if (message.kind === 'video' && !dedicated)
|
|
46
43
|
poster = (await VideoThumbnails.getThumbnailAsync(file.uri, { time: 0, quality: .6 })).uri;
|
|
47
44
|
const result = await ImageManipulator.manipulateAsync(poster ?? file.uri, [{ resize: { width: 480 } }], { compress: .7, format: ImageManipulator.SaveFormat.JPEG, base64: true });
|
|
48
45
|
thumbnail = result.uri;
|
|
49
|
-
return
|
|
46
|
+
return !signal.aborted && result.base64 ? { uri: 'data:image/jpeg;base64,' + result.base64 } : {};
|
|
50
47
|
}
|
|
51
48
|
finally {
|
|
52
|
-
abort.delete(controller);
|
|
53
49
|
if (downloaded && file)
|
|
54
50
|
await remove(file).catch(() => { });
|
|
55
51
|
for (const uri of [poster, thumbnail])
|
|
56
52
|
if (uri && FS.cacheDirectory && uri.startsWith(FS.cacheDirectory))
|
|
57
53
|
await FS.deleteAsync(uri, { idempotent: true }).catch(() => { });
|
|
58
54
|
}
|
|
59
|
-
})
|
|
60
|
-
queue = work.then(() => { });
|
|
61
|
-
cache.set(id, work);
|
|
62
|
-
if (cache.size > 100)
|
|
63
|
-
cache.delete(cache.keys().next().value);
|
|
64
|
-
return work;
|
|
55
|
+
});
|
|
65
56
|
}
|
|
66
|
-
|
|
67
|
-
|
|
57
|
+
// Uploaded IDs get their own authorized preview; never alias access between assets.
|
|
58
|
+
return { preview, clear };
|
|
68
59
|
}
|
package/dist/expo.d.ts
CHANGED
package/dist/expo.js
CHANGED
package/dist/group-editor.d.ts
CHANGED
package/dist/group-editor.js
CHANGED
|
@@ -47,7 +47,7 @@ export function MessagingGroupEditor({ client, inbox, userId, contacts, conversa
|
|
|
47
47
|
}
|
|
48
48
|
useEffect(() => { if (editing)
|
|
49
49
|
void load(); return () => { ++generation.current; }; }, [client, conversationId]);
|
|
50
|
-
const people = useMemo(() => [...new Set([...Object.keys(contacts), ...original])].filter(id => id !== userId &&
|
|
50
|
+
const people = useMemo(() => [...new Set([...Object.keys(contacts), ...original])].filter(id => id !== userId && contacts[id]?.groupEligible !== false &&
|
|
51
51
|
(contacts[id]?.name ?? 'Member').toLocaleLowerCase().includes(query.trim().toLocaleLowerCase())).sort((a, b) => (contacts[a]?.name ?? a).localeCompare(contacts[b]?.name ?? b)), [contacts, original, query, userId]);
|
|
52
52
|
const chosen = (id) => !removed.has(id) && (original.has(id) || added.has(id));
|
|
53
53
|
const toggle = (id) => {
|
package/dist/inbox.d.ts
CHANGED
package/dist/inbox.js
CHANGED
|
@@ -24,7 +24,7 @@ export function MessagingInbox({ client, userId, contacts = {}, appearance, acti
|
|
|
24
24
|
const setQuery = (value) => { memory.query = value; (onSearchChange ?? setLocalQuery)(value); };
|
|
25
25
|
const scroll = useInboxScroll(memory, 'chats:' + query.trim(), state.conversations.length > 0 && !state.loading, !state.hasMore);
|
|
26
26
|
const [selected, setSelected] = useState(null), [confirm, setConfirm] = useState(false), [busy, setBusy] = useState(false), [error, setError] = useState('');
|
|
27
|
-
const title = (row) => row.kind === 'group' ? row.name ?? 'Group' : contacts[row.contactId ?? '']?.name ?? row.contactId ?? 'Chat';
|
|
27
|
+
const title = (row) => row.contactId === userId ? uiText('Notes to self') : row.kind === 'group' ? row.name ?? 'Group' : contacts[row.contactId ?? '']?.name ?? row.contactId ?? 'Chat';
|
|
28
28
|
const rows = useRef(new Map()), pending = useRef(null);
|
|
29
29
|
const [menuVisible, setMenuVisible] = useState(false), [confirmVisible, setConfirmVisible] = useState(false), [anchor, setAnchor] = useState({ x: 12, y: 88 });
|
|
30
30
|
const openOptions = (row) => { if (!state.inboxActionsAvailable)
|
|
@@ -62,7 +62,7 @@ export function MessagingInbox({ client, userId, contacts = {}, appearance, acti
|
|
|
62
62
|
const preview = (row) => { const m = row.lastMessage; if (!m)
|
|
63
63
|
return 'No messages yet'; const text = m.kind === 'text' ? m.text ?? '' : ({ voice: 'Voice message', image: 'Photo', video: 'Video', document: 'Document', poll: 'Poll' }[m.kind] ?? 'Message'); return (m.senderId === userId ? 'You: ' : '') + text; };
|
|
64
64
|
return _jsx(MessagingTypography, { appearance: design, children: _jsxs(View, { style: { flex: 1, minHeight: 0, backgroundColor: c.background }, children: [_jsx(MessagingPopupHeaderBoundary, { children: _jsxs(View, { testID: "messaging-inbox-header", style: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', minHeight: 64, paddingHorizontal: 16, paddingVertical: 8, gap: 8 }, children: [_jsx(Text, { accessibilityRole: "header", numberOfLines: 1, style: { flex: 1, color: c.foreground, fontSize: 20, fontWeight: '700' }, children: companyName ?? design.name }), headerActions] }) }), _jsxs(View, { style: { margin: 12, marginTop: 8, flexDirection: 'row', alignItems: 'center', gap: 8, borderRadius: 10, paddingHorizontal: 10, backgroundColor: c.foreground + '0b' }, children: [_jsx(MessageIcon, { name: "search", size: 20, color: c.secondary }), _jsx(TextInput, { accessibilityLabel: uiText("Search chats"), placeholder: state.globalSearchAvailable ? uiText("Search chats and messages") : uiText("Search chats"), placeholderTextColor: c.secondary, value: query, onChangeText: setQuery, maxLength: 200, autoCapitalize: "none", autoCorrect: false, style: { flex: 1, minHeight: 44, color: c.foreground, fontSize: 16 } }), !!query && _jsx(Pressable, { accessibilityRole: "button", accessibilityLabel: uiText("Clear chat search"), onPress: () => setQuery(''), style: { width: 44, height: 44, alignItems: 'center', justifyContent: 'center' }, children: _jsx(MessageIcon, { name: "close", size: 20, color: c.secondary }) })] }), state.error && _jsxs(View, { style: { padding: 12 }, children: [_jsx(Text, { accessibilityRole: "alert", style: { color: c.foreground }, children: state.error }), _jsx(Pressable, { accessibilityRole: "button", onPress: () => void client.refresh().catch(() => { }), children: _jsx(Text, { style: { color: c.accent, paddingVertical: 8 }, children: uiText("Try again") }) })] }), query.trim() && state.globalSearchAvailable ? _jsx(InboxMessageSearch, { viewMemory: memory, client: client, query: query.trim(), appearance: design, name: title, chats: state.conversations.filter(row => title(row).toLocaleLowerCase().includes(query.trim().toLocaleLowerCase())), onSelect: onSelect }, userId) : _jsx(FlatList, { ...scroll, testID: "messaging-inbox-list", data: state.conversations.filter(row => !query.trim() || [title(row), row.lastMessage?.text ?? '', searchText?.(row) ?? ''].join(' ').toLocaleLowerCase().includes(query.trim().toLocaleLowerCase())), keyExtractor: row => row.id, contentContainerStyle: { flexGrow: 1 }, ItemSeparatorComponent: () => _jsx(View, { style: { height: 1, backgroundColor: c.foreground + '12', marginLeft: 64 } }), ListEmptyComponent: _jsx(Text, { style: { padding: 20, textAlign: 'center', color: c.secondary }, children: state.loading ? uiText("Loading chats…") : state.error ? '' : query ? uiText("No matching chats.") : uiText("No chats yet") }), ListFooterComponent: state.hasMore ? _jsx(Pressable, { accessibilityRole: "button", disabled: state.loading, onPress: () => void client.loadMore().catch(() => { }), children: _jsx(Text, { style: { padding: 12, textAlign: 'center', color: c.accent }, children: state.loading ? uiText("Loading…") : uiText("More chats") }) }) : null, renderItem: ({ item }) => {
|
|
65
|
-
const person = item.contactId ? contacts[item.contactId] : undefined, title = item.kind === 'group' ? item.name ?? 'Group' : person?.name ?? item.contactId ?? 'Chat';
|
|
65
|
+
const person = item.contactId ? contacts[item.contactId] : undefined, title = item.contactId === userId ? uiText('Notes to self') : item.kind === 'group' ? item.name ?? 'Group' : person?.name ?? item.contactId ?? 'Chat';
|
|
66
66
|
const date = item.lastMessage ? new Date(item.lastMessage.createdAt) : null;
|
|
67
67
|
return _jsxs(Pressable, { ref: node => { if (node)
|
|
68
68
|
rows.current.set(item.id, node);
|
package/dist/index.d.ts
CHANGED
|
@@ -60,6 +60,14 @@ export interface AttachmentChoice {
|
|
|
60
60
|
durationMs?: number;
|
|
61
61
|
}
|
|
62
62
|
export interface MessagingScreenProps {
|
|
63
|
+
/** Disabled links render as plain text. Callbacks replace system navigation;
|
|
64
|
+
* providing openUrl also disables embedded social browsing. */
|
|
65
|
+
linkHandling?: {
|
|
66
|
+
webLinks?: boolean;
|
|
67
|
+
addresses?: boolean;
|
|
68
|
+
openUrl?: (url: string, message: MessagingMessage) => void | Promise<void>;
|
|
69
|
+
openAddress?: (address: string, message: MessagingMessage) => void | Promise<void>;
|
|
70
|
+
};
|
|
63
71
|
/** Compose surrounding navigation while keeping a single message context. */
|
|
64
72
|
renderLayout?: (conversation: ReactNode) => ReactNode;
|
|
65
73
|
/** Language of controls and accessibility labels; separate from translated message content. */
|
|
@@ -175,7 +183,8 @@ export declare function MessagingBubble({ message, pendingStatus, previousMessag
|
|
|
175
183
|
export declare function MessagingList(): React.JSX.Element;
|
|
176
184
|
export declare function MessagingComposer(): React.JSX.Element;
|
|
177
185
|
/** Account navigation. Mount in the inbox/app toolbar, never in a contact menu. */
|
|
178
|
-
export declare function MessagingAccountMenu({ onNewGroup, onMaps, onPublicRooms }?: {
|
|
186
|
+
export declare function MessagingAccountMenu({ onNewConversation, onNewGroup, onMaps, onPublicRooms }?: {
|
|
187
|
+
onNewConversation?: () => void;
|
|
179
188
|
onNewGroup?: () => void;
|
|
180
189
|
onMaps?: () => void;
|
|
181
190
|
onPublicRooms?: () => void;
|