@robono/messaging-react-native 0.8.0 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +8 -0
- package/README.md +92 -37
- package/SBOM.spdx.json +577 -217
- package/dist/account.js +7 -4
- package/dist/attachment-menu.js +5 -2
- package/dist/call-message.js +8 -5
- package/dist/calling.js +6 -2
- package/dist/conversation-details.d.ts +1 -0
- package/dist/conversation-details.js +11 -7
- package/dist/custom-message.d.ts +7 -0
- package/dist/custom-message.js +53 -0
- package/dist/customization.d.ts +49 -0
- package/dist/customization.js +1 -0
- package/dist/expo-connected-calling.js +7 -2
- package/dist/expo-extras.js +4 -1
- package/dist/expo-media.js +27 -5
- package/dist/expo-messenger.d.ts +41 -2
- package/dist/expo-messenger.js +86 -10
- package/dist/expo-outbox.js +22 -1
- package/dist/expo-screen.d.ts +1 -1
- package/dist/expo-screen.js +42 -31
- package/dist/expo-social.js +5 -2
- package/dist/expo.d.ts +1 -1
- package/dist/expo.js +1 -1
- package/dist/extras.js +11 -7
- package/dist/forward.js +8 -5
- package/dist/group-editor.js +9 -6
- package/dist/header-calls.js +9 -6
- package/dist/icons.js +2 -1
- package/dist/inbox-search.d.ts +3 -1
- package/dist/inbox-search.js +33 -10
- package/dist/inbox-view.d.ts +21 -0
- package/dist/inbox-view.js +32 -0
- package/dist/inbox.d.ts +6 -1
- package/dist/inbox.js +27 -18
- package/dist/index.d.ts +16 -6
- package/dist/index.js +122 -81
- package/dist/localization.d.ts +24 -0
- package/dist/localization.js +12 -0
- package/dist/maps-connection.js +5 -2
- package/dist/media-ui.js +15 -8
- package/dist/message-menu.js +5 -2
- package/dist/native-outbox.d.ts +10 -2
- package/dist/native-outbox.js +7 -0
- package/dist/polls.js +21 -16
- package/dist/popup.js +8 -4
- package/dist/public-rooms.d.ts +10 -0
- package/dist/public-rooms.js +78 -0
- package/dist/quick-send.js +6 -3
- package/dist/room-moderation.d.ts +22 -0
- package/dist/room-moderation.js +87 -0
- package/dist/safety.js +11 -8
- package/dist/saved.js +7 -4
- package/dist/scheduled.js +19 -14
- package/dist/search.js +7 -4
- package/dist/social.js +9 -5
- package/dist/translation-settings.js +7 -4
- package/dist/typography.d.ts +12 -0
- package/dist/typography.js +31 -0
- package/dist/voice-composer.js +11 -6
- package/package.json +10 -8
- package/setup.plugin.cjs +20 -0
package/dist/group-editor.js
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Text, TextInput } from './typography.js';
|
|
3
|
+
import { useRobonoLocale } from './localization.js';
|
|
2
4
|
import { useEffect, useMemo, useRef, useState } from 'react';
|
|
3
|
-
import { ActivityIndicator, FlatList, Image, Pressable,
|
|
5
|
+
import { ActivityIndicator, FlatList, Image, Pressable, View } from 'react-native';
|
|
4
6
|
import { MessageIcon } from './icons.js';
|
|
5
7
|
import { MessagingPopup, messagingPopupColors as c } from './popup.js';
|
|
6
8
|
/** Messenger's name, group type and contact checklist; changes use member deltas. */
|
|
7
9
|
export function MessagingGroupEditor({ client, inbox, userId, contacts, conversation, groups, accent, onClose, onSaved }) {
|
|
10
|
+
const { text: uiText, locale: uiLocale, number: uiNumber, count: uiCount } = useRobonoLocale();
|
|
8
11
|
const editing = !!conversation, conversationId = client.getSnapshot().conversationId;
|
|
9
12
|
const [name, setName] = useState(conversation?.name ?? ''), [supergroup, setSupergroup] = useState(conversation?.supergroup ?? false);
|
|
10
13
|
const [revision, setRevision] = useState(conversation?.revision ?? 0), [count, setCount] = useState(conversation?.memberCount ?? 0);
|
|
@@ -34,7 +37,7 @@ export function MessagingGroupEditor({ client, inbox, userId, contacts, conversa
|
|
|
34
37
|
catch (e) {
|
|
35
38
|
if (version === generation.current) {
|
|
36
39
|
setReady(false);
|
|
37
|
-
setError(e instanceof Error ? e.message :
|
|
40
|
+
setError(e instanceof Error ? e.message : uiText("Group members could not be loaded."));
|
|
38
41
|
}
|
|
39
42
|
}
|
|
40
43
|
finally {
|
|
@@ -49,7 +52,7 @@ export function MessagingGroupEditor({ client, inbox, userId, contacts, conversa
|
|
|
49
52
|
const chosen = (id) => !removed.has(id) && (original.has(id) || added.has(id));
|
|
50
53
|
const toggle = (id) => {
|
|
51
54
|
if (!chosen(id) && membersCount >= max) {
|
|
52
|
-
setError(
|
|
55
|
+
setError(uiText("This group can have up to {value1} members.", { value1: max.toLocaleString(uiLocale) }));
|
|
53
56
|
return;
|
|
54
57
|
}
|
|
55
58
|
if (original.has(id))
|
|
@@ -86,13 +89,13 @@ export function MessagingGroupEditor({ client, inbox, userId, contacts, conversa
|
|
|
86
89
|
}
|
|
87
90
|
}
|
|
88
91
|
catch (e) {
|
|
89
|
-
setError(e instanceof Error ? e.message :
|
|
92
|
+
setError(e instanceof Error ? e.message : uiText("The group could not be saved."));
|
|
90
93
|
}
|
|
91
94
|
finally {
|
|
92
95
|
setBusy(false);
|
|
93
96
|
}
|
|
94
97
|
}
|
|
95
98
|
const locked = busy || loading || !ready, canSave = !locked && !!name.trim() && membersCount >= 2 && membersCount <= max;
|
|
96
|
-
return _jsxs(MessagingPopup, { title: editing ?
|
|
97
|
-
onClose(); }, form: true, children: [_jsx(FlatList, { data: people, keyExtractor: id => id, keyboardShouldPersistTaps: "handled", style: { flex: 1 }, contentContainerStyle: { paddingHorizontal: 8, paddingBottom: 16 }, initialNumToRender: 15, ListHeaderComponent: _jsxs(View, { style: { gap: 12, paddingTop: 12 }, children: [_jsx(TextInput, { accessibilityLabel: "Group name", placeholder: "Group name", placeholderTextColor: c.secondary, maxLength: 100, value: name, onChangeText: setName, editable: !locked, style: { color: c.foreground, fontSize: 21, minHeight: 48, paddingHorizontal: 4, borderBottomWidth: 1, borderBottomColor: '#ffffff30' } }), groups?.supergroupsEnabled && _jsx(View, { style: { flexDirection: 'row', gap: 20 }, children: [false, true].map(value => _jsxs(Pressable, { accessibilityRole: "radio", accessibilityLabel: value ?
|
|
99
|
+
return _jsxs(MessagingPopup, { title: editing ? uiText("Edit group") : uiText("New group"), background: c.background, closeDisabled: busy, onClose: () => { if (!busy)
|
|
100
|
+
onClose(); }, form: true, children: [_jsx(FlatList, { data: people, keyExtractor: id => id, keyboardShouldPersistTaps: "handled", style: { flex: 1 }, contentContainerStyle: { paddingHorizontal: 8, paddingBottom: 16 }, initialNumToRender: 15, ListHeaderComponent: _jsxs(View, { style: { gap: 12, paddingTop: 12 }, children: [_jsx(TextInput, { accessibilityLabel: uiText("Group name"), placeholder: uiText("Group name"), placeholderTextColor: c.secondary, maxLength: 100, value: name, onChangeText: setName, editable: !locked, style: { color: c.foreground, fontSize: 21, minHeight: 48, paddingHorizontal: 4, borderBottomWidth: 1, borderBottomColor: '#ffffff30' } }), groups?.supergroupsEnabled && _jsx(View, { style: { flexDirection: 'row', gap: 20 }, children: [false, true].map(value => _jsxs(Pressable, { accessibilityRole: "radio", accessibilityLabel: value ? uiText("Supergroup") : uiText("Standard group"), accessibilityState: { checked: supergroup === value, disabled: locked }, disabled: locked, onPress: () => setSupergroup(value), style: { minHeight: 44, flexDirection: 'row', alignItems: 'center', gap: 8 }, children: [_jsx(MessageIcon, { name: supergroup === value ? 'radio-button-on' : 'radio-button-off', color: accent, size: 22 }), _jsx(Text, { style: { color: c.foreground, fontSize: 16 }, children: value ? uiText("Supergroup") : uiText("Group") })] }, String(value))) }), _jsx(Text, { style: { color: c.secondary, fontSize: 14 }, children: uiText('{count} / {limit} members', { count: membersCount, limit: max }) }), _jsx(Text, { style: { color: c.foreground, fontSize: 17 }, children: uiText("You · Group owner") }), _jsx(TextInput, { accessibilityLabel: uiText("Search group contacts"), placeholder: uiText("Search contacts"), placeholderTextColor: c.secondary, value: query, onChangeText: setQuery, style: { color: c.foreground, fontSize: 16, minHeight: 44, borderBottomWidth: 1, borderBottomColor: '#ffffff30' } })] }), renderItem: ({ item: id }) => _jsxs(Pressable, { accessibilityRole: "checkbox", accessibilityLabel: contacts[id]?.name ?? uiText("Member"), accessibilityState: { checked: chosen(id), disabled: locked }, disabled: locked, onPress: () => toggle(id), style: { minHeight: 56, flexDirection: 'row', alignItems: 'center', gap: 10 }, children: [contacts[id]?.avatarUrl ? _jsx(Image, { source: { uri: contacts[id].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, fontSize: 18 }, children: (contacts[id]?.name ?? 'Member').slice(0, 1) }) }), _jsx(Text, { style: { flex: 1, color: c.foreground, fontSize: 16 }, children: contacts[id]?.name ?? uiText("Member") }), _jsx(MessageIcon, { name: chosen(id) ? 'checkmark-circle' : 'ellipse-outline', color: chosen(id) ? accent : c.secondary, size: 24 })] }), ListFooterComponent: _jsxs(View, { style: { gap: 12, paddingTop: 12 }, children: [loading && _jsx(ActivityIndicator, { accessibilityLabel: uiText("Loading group members"), color: accent }), next && _jsx(Pressable, { accessibilityRole: "button", disabled: loading || busy, onPress: () => void load(true), style: { minHeight: 44, justifyContent: 'center' }, children: _jsx(Text, { style: { color: accent }, children: uiText("Load more members") }) }), !people.length && !loading && _jsx(Text, { style: { color: c.secondary }, children: query ? uiText("No matching contacts.") : uiText("Your app has not supplied any contacts to invite.") }), _jsxs(Text, { style: { color: c.secondary, fontSize: 13, lineHeight: 19 }, children: [uiText("Only the group owner can change the name or members."), groups?.historyForNewMembers === 'all' ? uiText("New members can see retained group history.") : uiText("New members see messages sent after they join.")] }), !!error && _jsx(Text, { accessibilityRole: "alert", style: { color: c.foreground, fontSize: 15 }, children: error })] }) }), _jsx(Pressable, { accessibilityRole: "button", accessibilityLabel: editing ? uiText("Save group") : uiText("Create group"), disabled: !canSave, onPress: () => void save(), style: { minHeight: 48, margin: 8, borderRadius: 12, backgroundColor: accent, alignItems: 'center', justifyContent: 'center', opacity: canSave ? 1 : .4 }, children: busy ? _jsx(ActivityIndicator, { color: "#000" }) : _jsx(Text, { style: { color: '#000', fontSize: 17, fontWeight: '600' }, children: editing ? uiText("Save") : uiText("Create group") }) })] });
|
|
98
101
|
}
|
package/dist/header-calls.js
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { Text } from './typography.js';
|
|
3
|
+
import { useRobonoLocale } from './localization.js';
|
|
2
4
|
import { useEffect, useRef, useState } from 'react';
|
|
3
|
-
import { Keyboard, Linking, Platform, Pressable,
|
|
5
|
+
import { Keyboard, Linking, Platform, Pressable, View } from 'react-native';
|
|
4
6
|
import { MessageIcon } from './icons.js';
|
|
5
7
|
import { MessagingPopup } from './popup.js';
|
|
6
8
|
/** Matches Messenger: video is separate; the phone button chooses internet/carrier. */
|
|
7
9
|
export function MessagingHeaderCalls({ actions, name, group, color }) {
|
|
10
|
+
const { text: uiText, locale: uiLocale, number: uiNumber, count: uiCount } = useRobonoLocale();
|
|
8
11
|
const [visible, setVisible] = useState(false), [error, setError] = useState('');
|
|
9
12
|
const pending = useRef(null), latest = useRef(actions);
|
|
10
13
|
latest.current = actions;
|
|
@@ -24,7 +27,7 @@ export function MessagingHeaderCalls({ actions, name, group, color }) {
|
|
|
24
27
|
await current?.[kind]?.();
|
|
25
28
|
}
|
|
26
29
|
catch {
|
|
27
|
-
setError(
|
|
30
|
+
setError(uiText("The call could not start. Please try again."));
|
|
28
31
|
} };
|
|
29
32
|
const dismissed = () => { const kind = pending.current; pending.current = null; if (kind)
|
|
30
33
|
requestAnimationFrame(() => void invoke(kind)); };
|
|
@@ -37,10 +40,10 @@ export function MessagingHeaderCalls({ actions, name, group, color }) {
|
|
|
37
40
|
} }; document.addEventListener('keydown', close, true); return () => document.removeEventListener('keydown', close, true); }, [visible]);
|
|
38
41
|
useEffect(() => { pending.current = null; setVisible(false); setError(''); }, [name, group]);
|
|
39
42
|
const choose = (kind) => { pending.current = kind; setVisible(false); };
|
|
40
|
-
const button = (label, icon, press) => _jsx(Pressable, { accessibilityRole: "button", accessibilityLabel: label, onPress: press, style: { width: icon === 'video' ? 42 : 44, height: icon === 'video' ? 42 : 44, alignItems: 'center', justifyContent: 'center' }, children: _jsx(MessageIcon, { name: icon, size: 23, color: color }) });
|
|
41
|
-
const row = (kind, label, detail) => _jsxs(Pressable, { accessibilityRole: "button", accessibilityLabel: label, onPress: () => choose(kind), style: ({ pressed }) => ({ flexDirection: 'row', alignItems: 'center', gap: 16, minHeight: 80, padding: 12, borderRadius: 18, backgroundColor: pressed ? '#1D1D1D' : 'transparent' }), children: [_jsx(View, { style: { width: 48, height: 48, borderRadius: 24, backgroundColor: '#1D1D1D', alignItems: 'center', justifyContent: 'center' }, children: _jsx(MessageIcon, { name: kind === 'phone' ? 'phone-portrait-outline' : 'phone', color: "#F7F7F7", size: 24 }) }), _jsxs(View, { style: { flex: 1, gap: 4 }, children: [_jsx(Text, { style: { color: '#F7F7F7', fontSize: 18, fontWeight: '600' }, children: label }), _jsx(Text, { style: { color: '#A3A3A3', fontSize: 15 }, children: detail })] })] });
|
|
42
|
-
return _jsxs(_Fragment, { children: [actions?.video && button(actions.videoLabel ?? 'Video call', 'video', () => void invoke('video')), (actions?.voice || validPhone) && button('Call '
|
|
43
|
+
const button = (label, icon, press) => _jsx(Pressable, { accessibilityRole: "button", accessibilityLabel: uiText(label), onPress: press, style: { width: icon === 'video' ? 42 : 44, height: icon === 'video' ? 42 : 44, alignItems: 'center', justifyContent: 'center' }, children: _jsx(MessageIcon, { name: icon, size: 23, color: color }) });
|
|
44
|
+
const row = (kind, label, detail) => _jsxs(Pressable, { accessibilityRole: "button", accessibilityLabel: uiText(label), onPress: () => choose(kind), style: ({ pressed }) => ({ flexDirection: 'row', alignItems: 'center', gap: 16, minHeight: 80, padding: 12, borderRadius: 18, backgroundColor: pressed ? '#1D1D1D' : 'transparent' }), children: [_jsx(View, { style: { width: 48, height: 48, borderRadius: 24, backgroundColor: '#1D1D1D', alignItems: 'center', justifyContent: 'center' }, children: _jsx(MessageIcon, { name: kind === 'phone' ? 'phone-portrait-outline' : 'phone', color: "#F7F7F7", size: 24 }) }), _jsxs(View, { style: { flex: 1, gap: 4 }, children: [_jsx(Text, { style: { color: '#F7F7F7', fontSize: 18, fontWeight: '600' }, children: uiText(label) }), _jsx(Text, { style: { color: '#A3A3A3', fontSize: 15 }, children: uiText(detail) })] })] });
|
|
45
|
+
return _jsxs(_Fragment, { children: [actions?.video && button(actions.videoLabel ?? 'Video call', 'video', () => void invoke('video')), (actions?.voice || validPhone) && button(uiText('Call {name}', { name }), 'phone', () => { if (!validPhone) {
|
|
43
46
|
void invoke('voice');
|
|
44
47
|
return;
|
|
45
|
-
} Keyboard.dismiss(); setVisible(true); }), _jsx(MessagingPopup, { visible: visible || !!error, compact: true, title: 'Call '
|
|
48
|
+
} Keyboard.dismiss(); setVisible(true); }), _jsx(MessagingPopup, { visible: visible || !!error, compact: true, title: uiText('Call {name}', { name }), background: "#111111", onDismiss: dismissed, onClose: () => { pending.current = null; setVisible(false); setError(''); }, children: error ? _jsx(Text, { accessibilityRole: "alert", style: { color: '#F7F7F7', fontSize: 16, paddingTop: 12 }, children: error }) : _jsxs(_Fragment, { children: [actions?.voice && row('voice', actions.voiceLabel ?? 'Internet call', 'Wi-Fi or mobile data'), validPhone && row('phone', 'Phone call', validPhone)] }) })] });
|
|
46
49
|
}
|
package/dist/icons.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { RobonoIcon } from "./visual/RobonoIcon.js";
|
|
3
|
-
|
|
3
|
+
import { useRobonoLocale } from './localization.js';
|
|
4
|
+
export function MessageIcon(props) { const { direction } = useRobonoLocale(); return _jsx(RobonoIcon, { ...props, style: [props.style, props.name === 'back' && direction === 'rtl' ? { transform: [{ scaleX: -1 }] } : undefined] }); }
|
|
4
5
|
export function duration(ms = 0) { const seconds = Math.ceil(ms / 1000); return `${Math.floor(seconds / 60)}:${String(seconds % 60).padStart(2, '0')}`; }
|
|
5
6
|
export function fileSize(bytes = 0) { return bytes >= 1048576 ? `${(bytes / 1048576).toFixed(1)} MB` : bytes >= 1024 ? `${Math.ceil(bytes / 1024)} KB` : `${bytes} B`; }
|
|
6
7
|
/** Neutral app chrome stays legible for light and dark customer palettes. */
|
package/dist/inbox-search.d.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
import { type InboxViewMemory } from './inbox-view.js';
|
|
1
2
|
import React from 'react';
|
|
2
3
|
import type { ConversationSummary, MessagingInboxClient, MessagingAppearance } from '@robono/messaging';
|
|
3
4
|
/** Main-screen search covers retained messages beyond the current inbox page. */
|
|
4
|
-
export declare function InboxMessageSearch({ client, query, appearance, name, chats, onSelect }: {
|
|
5
|
+
export declare function InboxMessageSearch({ client, query, appearance, name, chats, onSelect, viewMemory }: {
|
|
6
|
+
viewMemory: InboxViewMemory;
|
|
5
7
|
client: MessagingInboxClient;
|
|
6
8
|
query: string;
|
|
7
9
|
appearance: MessagingAppearance;
|
package/dist/inbox-search.js
CHANGED
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useInboxScroll } from './inbox-view.js';
|
|
3
|
+
import { Text } from './typography.js';
|
|
4
|
+
import { useRobonoLocale } from './localization.js';
|
|
2
5
|
import { useEffect, useRef, useState } from 'react';
|
|
3
|
-
import { ActivityIndicator, FlatList, Pressable,
|
|
6
|
+
import { ActivityIndicator, FlatList, Pressable, View } from 'react-native';
|
|
4
7
|
/** Main-screen search covers retained messages beyond the current inbox page. */
|
|
5
|
-
export function InboxMessageSearch({ client, query, appearance, name, chats, onSelect }) {
|
|
6
|
-
const
|
|
8
|
+
export function InboxMessageSearch({ client, query, appearance, name, chats, onSelect, viewMemory }) {
|
|
9
|
+
const { text: uiText, locale: uiLocale, number: uiNumber, count: uiCount } = useRobonoLocale();
|
|
10
|
+
const c = appearance.colors, [rows, setRows] = useState([]), [next, setNext] = useState(null), [busy, setBusy] = useState(true), [error, setError] = useState('');
|
|
11
|
+
const scroll = useInboxScroll(viewMemory, 'search:' + query, rows.length > 0 && !busy, !next);
|
|
7
12
|
const controller = useRef(null), generation = useRef(0);
|
|
8
13
|
async function search(more = false) {
|
|
9
14
|
controller.current?.abort();
|
|
@@ -13,16 +18,34 @@ export function InboxMessageSearch({ client, query, appearance, name, chats, onS
|
|
|
13
18
|
setBusy(true);
|
|
14
19
|
setError('');
|
|
15
20
|
try {
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
21
|
+
const collected = new Map(more ? rows.map(row => [row.message.conversationId + ':' + row.message.id, row]) : []);
|
|
22
|
+
const target = more ? 0 : Math.max(20, viewMemory.searchCounts.get(query) ?? 20), seen = new Set();
|
|
23
|
+
let before = more ? next : null, page;
|
|
24
|
+
do {
|
|
25
|
+
page = await client.search(query, { ...(before ? { before } : {}), signal: control.signal });
|
|
26
|
+
if (version !== generation.current || control.signal.aborted)
|
|
27
|
+
return;
|
|
28
|
+
for (const row of page.results)
|
|
29
|
+
collected.set(row.message.conversationId + ':' + row.message.id, row);
|
|
30
|
+
if (more || !page.hasMore || collected.size >= target)
|
|
31
|
+
break;
|
|
32
|
+
if (!page.nextCursor || seen.has(page.nextCursor) || seen.size > Math.ceil(target / 20) + 2)
|
|
33
|
+
throw Error(uiText('Messages could not be searched.'));
|
|
34
|
+
seen.add(page.nextCursor);
|
|
35
|
+
before = page.nextCursor;
|
|
36
|
+
} while (true);
|
|
37
|
+
setRows([...collected.values()]);
|
|
20
38
|
setNext(page.hasMore ? page.nextCursor : null);
|
|
39
|
+
viewMemory.searchCounts.delete(query);
|
|
40
|
+
viewMemory.searchCounts.set(query, collected.size);
|
|
41
|
+
while (viewMemory.searchCounts.size > 12)
|
|
42
|
+
viewMemory.searchCounts.delete(viewMemory.searchCounts.keys().next().value);
|
|
21
43
|
}
|
|
22
44
|
catch (e) {
|
|
23
45
|
if (version === generation.current && !control.signal.aborted) {
|
|
24
|
-
|
|
25
|
-
|
|
46
|
+
if (!more || e?.status === 401 || e?.status === 403)
|
|
47
|
+
setRows([]);
|
|
48
|
+
setError(e instanceof Error ? e.message : uiText("Messages could not be searched."));
|
|
26
49
|
}
|
|
27
50
|
}
|
|
28
51
|
finally {
|
|
@@ -38,5 +61,5 @@ export function InboxMessageSearch({ client, query, appearance, name, chats, onS
|
|
|
38
61
|
const timer = setTimeout(() => void search(), 300);
|
|
39
62
|
return () => { clearTimeout(timer); ++generation.current; controller.current?.abort(); };
|
|
40
63
|
}, [client, query]);
|
|
41
|
-
return _jsx(FlatList, { style: { flex: 1 }, data: rows, keyExtractor: row => row.message.conversationId + ':' + row.message.id, keyboardShouldPersistTaps: "handled", ListHeaderComponent: _jsxs(View, { children: [chats.length > 0 && _jsxs(_Fragment, { children: [_jsx(Text, { style: { paddingHorizontal: 16, paddingVertical: 8, color: c.secondary, fontSize: 14, fontWeight: '600' }, children: "Chats" }), chats.map(chat => _jsx(Pressable, { accessibilityRole: "button", onPress: () => onSelect(chat), style: { minHeight: 48, paddingHorizontal: 16, justifyContent: 'center' }, children: _jsx(Text, { style: { color: c.foreground, fontSize: 16 }, children: name(chat) }) }, chat.id))] }), _jsx(Text, { style: { paddingHorizontal: 16, paddingVertical: 12, color: c.secondary, fontSize: 14, fontWeight: '600' }, children: "Messages" })] }), ListEmptyComponent: !busy && !error ? _jsx(Text, { style: { padding: 16, color: c.secondary }, children: "No matching messages." }) : null, renderItem: ({ item }) => _jsxs(Pressable, { accessibilityRole: "button", accessibilityLabel:
|
|
64
|
+
return _jsx(FlatList, { ...scroll, testID: "messaging-inbox-search-results", style: { flex: 1 }, data: rows, keyExtractor: row => row.message.conversationId + ':' + row.message.id, keyboardShouldPersistTaps: "handled", ListHeaderComponent: _jsxs(View, { children: [chats.length > 0 && _jsxs(_Fragment, { children: [_jsx(Text, { style: { paddingHorizontal: 16, paddingVertical: 8, color: c.secondary, fontSize: 14, fontWeight: '600' }, children: uiText("Chats") }), chats.map(chat => _jsx(Pressable, { accessibilityRole: "button", onPress: () => onSelect(chat), style: { minHeight: 48, paddingHorizontal: 16, justifyContent: 'center' }, children: _jsx(Text, { style: { color: c.foreground, fontSize: 16 }, children: name(chat) }) }, chat.id))] }), _jsx(Text, { style: { paddingHorizontal: 16, paddingVertical: 12, color: c.secondary, fontSize: 14, fontWeight: '600' }, children: uiText("Messages") })] }), ListEmptyComponent: !busy && !error ? _jsx(Text, { style: { padding: 16, color: c.secondary }, children: uiText("No matching messages.") }) : null, renderItem: ({ item }) => _jsxs(Pressable, { accessibilityRole: "button", accessibilityLabel: uiText("Message in {value1}: {value2}", { value1: name(item.conversation), value2: item.message.text ?? '' }), onPress: () => onSelect(item.conversation, item.message.id), style: { paddingHorizontal: 16, paddingVertical: 12, gap: 5, borderBottomWidth: 1, borderBottomColor: c.foreground + '16' }, children: [_jsxs(View, { style: { flexDirection: 'row', gap: 8 }, children: [_jsx(Text, { numberOfLines: 1, style: { flex: 1, fontSize: 16, fontWeight: '600', color: c.foreground }, children: name(item.conversation) }), _jsx(Text, { style: { fontSize: 12, color: c.secondary }, children: new Date(item.message.createdAt).toLocaleDateString(uiLocale) })] }), _jsx(Text, { numberOfLines: 3, style: { fontSize: 15, lineHeight: 21, color: c.secondary }, children: item.message.text })] }), ListFooterComponent: _jsxs(View, { children: [busy && _jsx(ActivityIndicator, { accessibilityLabel: uiText("Searching all messages"), color: c.accent, style: { padding: 16 } }), !!error && _jsxs(_Fragment, { children: [_jsx(Text, { accessibilityRole: "alert", style: { padding: 16, color: c.foreground }, children: error }), _jsx(Pressable, { accessibilityRole: "button", onPress: () => void search(rows.length > 0 && !!next), style: { minHeight: 44, paddingHorizontal: 16, justifyContent: 'center' }, children: _jsx(Text, { style: { color: c.accent }, children: uiText("Try again") }) })] }), next && !busy && !error && _jsx(Pressable, { accessibilityRole: "button", onPress: () => void search(true), style: { minHeight: 48, alignItems: 'center', justifyContent: 'center' }, children: _jsx(Text, { style: { color: c.accent }, children: uiText("More results") }) })] }) });
|
|
42
65
|
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { FlatList, NativeScrollEvent, NativeSyntheticEvent } from 'react-native';
|
|
2
|
+
/** Ephemeral navigation state. Never persist it across signed-in accounts. */
|
|
3
|
+
export interface InboxViewMemory {
|
|
4
|
+
query: string;
|
|
5
|
+
offsets: Map<string, number>;
|
|
6
|
+
searchCounts: Map<string, number>;
|
|
7
|
+
}
|
|
8
|
+
export declare const createInboxViewMemory: () => InboxViewMemory;
|
|
9
|
+
export declare function useInboxScroll(memory: InboxViewMemory, key: string, ready: boolean, complete: boolean): {
|
|
10
|
+
ref: import("react").RefObject<FlatList<any> | null>;
|
|
11
|
+
scrollEventThrottle: number;
|
|
12
|
+
onLayout: (event: {
|
|
13
|
+
nativeEvent: {
|
|
14
|
+
layout: {
|
|
15
|
+
height: number;
|
|
16
|
+
};
|
|
17
|
+
};
|
|
18
|
+
}) => void;
|
|
19
|
+
onContentSizeChange: (_width: number, height: number) => void;
|
|
20
|
+
onScroll: (event: NativeSyntheticEvent<NativeScrollEvent>) => void;
|
|
21
|
+
};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { useLayoutEffect, useRef } from 'react';
|
|
2
|
+
export const createInboxViewMemory = () => ({ query: '', offsets: new Map(), searchCounts: new Map() });
|
|
3
|
+
export function useInboxScroll(memory, key, ready, complete) {
|
|
4
|
+
const list = useRef(null);
|
|
5
|
+
const state = useRef({ key, memory, wanted: memory.offsets.get(key) ?? 0, restored: false, height: 0, content: 0 });
|
|
6
|
+
if (state.current.key !== key || state.current.memory !== memory)
|
|
7
|
+
state.current = { key, memory, wanted: memory.offsets.get(key) ?? 0, restored: false, height: 0, content: 0 };
|
|
8
|
+
const restore = () => {
|
|
9
|
+
const s = state.current;
|
|
10
|
+
if (s.restored || !s.height || !ready)
|
|
11
|
+
return;
|
|
12
|
+
const max = Math.max(0, s.content - s.height);
|
|
13
|
+
if (max < s.wanted && !complete)
|
|
14
|
+
return;
|
|
15
|
+
s.restored = true;
|
|
16
|
+
list.current?.scrollToOffset({ offset: Math.min(max, s.wanted), animated: false });
|
|
17
|
+
};
|
|
18
|
+
useLayoutEffect(restore, [ready, complete, key, memory]);
|
|
19
|
+
return { ref: list, scrollEventThrottle: 32,
|
|
20
|
+
onLayout: (event) => { state.current.height = event.nativeEvent.layout.height; restore(); },
|
|
21
|
+
onContentSizeChange: (_width, height) => { state.current.content = height; restore(); },
|
|
22
|
+
onScroll: (event) => {
|
|
23
|
+
if (!state.current.restored)
|
|
24
|
+
return;
|
|
25
|
+
const value = Math.max(0, event.nativeEvent.contentOffset.y);
|
|
26
|
+
memory.offsets.delete(key);
|
|
27
|
+
memory.offsets.set(key, value);
|
|
28
|
+
while (memory.offsets.size > 12)
|
|
29
|
+
memory.offsets.delete(memory.offsets.keys().next().value);
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
}
|
package/dist/inbox.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type InboxViewMemory } from './inbox-view.js';
|
|
1
2
|
import React from 'react';
|
|
2
3
|
import { type MessagingAppearance, type MessagingInboxClient, type ConversationSummary } from '@robono/messaging';
|
|
3
4
|
export interface MessagingInboxProps {
|
|
@@ -7,6 +8,10 @@ export interface MessagingInboxProps {
|
|
|
7
8
|
name: string;
|
|
8
9
|
avatarUrl?: string;
|
|
9
10
|
}>;
|
|
11
|
+
viewMemory?: InboxViewMemory;
|
|
12
|
+
query?: string;
|
|
13
|
+
onSearchChange?: (value: string) => void;
|
|
14
|
+
selectedId?: string;
|
|
10
15
|
companyName?: string;
|
|
11
16
|
headerActions?: React.ReactNode;
|
|
12
17
|
searchText?: (conversation: ConversationSummary) => string;
|
|
@@ -16,4 +21,4 @@ export interface MessagingInboxProps {
|
|
|
16
21
|
onDeleted?: (conversationId: string) => Promise<void> | void;
|
|
17
22
|
}
|
|
18
23
|
/** Compact Messenger chat rows: avatar, name, last message, time and unread badge. */
|
|
19
|
-
export declare function MessagingInbox({ client, userId, contacts, appearance, active, onSelect, onDeleted, companyName, headerActions, searchText }: MessagingInboxProps): React.JSX.Element;
|
|
24
|
+
export declare function MessagingInbox({ client, userId, contacts, appearance, active, onSelect, onDeleted, companyName, headerActions, searchText, selectedId, query: controlledQuery, onSearchChange, viewMemory }: MessagingInboxProps): React.JSX.Element;
|
package/dist/inbox.js
CHANGED
|
@@ -1,19 +1,28 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { createInboxViewMemory, useInboxScroll } from './inbox-view.js';
|
|
3
|
+
import { MessagingTypography } from './typography.js';
|
|
4
|
+
import { Text, TextInput } from './typography.js';
|
|
5
|
+
import { useRobonoLocale } from './localization.js';
|
|
2
6
|
import { MessageMenuRow } from './message-menu.js';
|
|
3
7
|
import { InboxMessageSearch } from './inbox-search.js';
|
|
4
8
|
import { MessageIcon } from './icons.js';
|
|
5
9
|
import { useEffect, useRef, useState, useSyncExternalStore } from 'react';
|
|
6
|
-
import { AppState, FlatList, Image, Platform, Pressable, ScrollView,
|
|
10
|
+
import { AppState, FlatList, Image, Platform, Pressable, ScrollView, View } from 'react-native';
|
|
7
11
|
import { defaultMessagingSettings } from '@robono/messaging';
|
|
8
12
|
import { MessagingPopup, MessagingMenuSurface, MessagingPopupHeaderBoundary, messagingPopupColors, messagingPopupBackdrop } from './popup.js';
|
|
9
13
|
/** Compact Messenger chat rows: avatar, name, last message, time and unread badge. */
|
|
10
|
-
export function MessagingInbox({ client, userId, contacts = {}, appearance, active = true, onSelect, onDeleted, companyName, headerActions, searchText }) {
|
|
14
|
+
export function MessagingInbox({ client, userId, contacts = {}, appearance, active = true, onSelect, onDeleted, companyName, headerActions, searchText, selectedId, query: controlledQuery, onSearchChange, viewMemory }) {
|
|
15
|
+
const { text: uiText, locale: uiLocale, number: uiNumber, count: uiCount } = useRobonoLocale();
|
|
11
16
|
const state = useSyncExternalStore(client.subscribe, client.getSnapshot, client.getSnapshot), design = appearance ?? state.appearance ?? defaultMessagingSettings().appearance, c = design.colors;
|
|
12
17
|
useEffect(() => { const update = (value) => { if (active && (value === null || value === 'active'))
|
|
13
18
|
client.start();
|
|
14
19
|
else
|
|
15
20
|
client.stop(); }; update(AppState.currentState); const sub = AppState.addEventListener('change', update); return () => { sub.remove(); client.stop(); }; }, [client, active]);
|
|
16
|
-
const
|
|
21
|
+
const ownMemory = useRef(createInboxViewMemory()), memory = viewMemory ?? ownMemory.current;
|
|
22
|
+
const [localQuery, setLocalQuery] = useState(memory.query);
|
|
23
|
+
const query = controlledQuery ?? localQuery;
|
|
24
|
+
const setQuery = (value) => { memory.query = value; (onSearchChange ?? setLocalQuery)(value); };
|
|
25
|
+
const scroll = useInboxScroll(memory, 'chats:' + query.trim(), state.conversations.length > 0 && !state.loading, !state.hasMore);
|
|
17
26
|
const [selected, setSelected] = useState(null), [confirm, setConfirm] = useState(false), [busy, setBusy] = useState(false), [error, setError] = useState('');
|
|
18
27
|
const title = (row) => row.kind === 'group' ? row.name ?? 'Group' : contacts[row.contactId ?? '']?.name ?? row.contactId ?? 'Chat';
|
|
19
28
|
const rows = useRef(new Map()), pending = useRef(null);
|
|
@@ -45,25 +54,25 @@ export function MessagingInbox({ client, userId, contacts = {}, appearance, acti
|
|
|
45
54
|
setConfirm(false);
|
|
46
55
|
}
|
|
47
56
|
catch (e) {
|
|
48
|
-
setError(e instanceof Error ? e.message :
|
|
57
|
+
setError(e instanceof Error ? e.message : uiText("Chat options could not be saved."));
|
|
49
58
|
}
|
|
50
59
|
finally {
|
|
51
60
|
setBusy(false);
|
|
52
61
|
} }
|
|
53
62
|
const preview = (row) => { const m = row.lastMessage; if (!m)
|
|
54
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; };
|
|
55
|
-
return _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: "Search chats", placeholder: state.globalSearchAvailable ? "Search chats and messages" : "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: "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: "Try again" }) })] }), query.trim() && state.globalSearchAvailable ? _jsx(InboxMessageSearch, { 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, { 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 ?
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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';
|
|
66
|
+
const date = item.lastMessage ? new Date(item.lastMessage.createdAt) : null;
|
|
67
|
+
return _jsxs(Pressable, { ref: node => { if (node)
|
|
68
|
+
rows.current.set(item.id, node);
|
|
69
|
+
else
|
|
70
|
+
rows.current.delete(item.id); }, accessibilityRole: "button", accessibilityState: { selected: item.id === selectedId }, accessibilityLabel: item.unreadCount ? uiText('{name}, {count} unread', { name: title, count: uiNumber(item.unreadCount) }) : title, onPress: () => onSelect(item), onLongPress: () => openOptions(item), accessibilityActions: state.inboxActionsAvailable ? [{ name: 'showOptions', label: uiText("Chat options") }] : [], onAccessibilityAction: e => { if (e.nativeEvent.actionName === 'showOptions')
|
|
71
|
+
openOptions(item); }, ...(Platform.OS === 'web' ? { onContextMenu: (e) => { e.preventDefault(); openOptions(item); } } : {}), style: { flexDirection: 'row', alignItems: 'center', gap: 12, paddingHorizontal: 12, paddingVertical: 10, minHeight: 68, backgroundColor: item.id === selectedId ? c.accent + '15' : undefined }, children: [person?.avatarUrl ? _jsx(Image, { source: { uri: person.avatarUrl }, style: { width: 40, height: 40, borderRadius: 20 } }) : _jsx(View, { style: { width: 40, height: 40, borderRadius: 20, backgroundColor: c.accent + '20', alignItems: 'center', justifyContent: 'center' }, children: _jsx(Text, { style: { color: c.accent, fontSize: 18, fontWeight: '700' }, children: title.slice(0, 1).toUpperCase() }) }), _jsxs(View, { style: { flex: 1, minWidth: 0 }, children: [_jsxs(Text, { numberOfLines: 1, style: { color: c.foreground, fontSize: 16, fontWeight: '700' }, children: [title, item.muted ? uiText(" · Muted") : ''] }), _jsx(Text, { numberOfLines: 1, style: { color: c.secondary, marginTop: 3, fontSize: 14 }, children: preview(item) })] }), _jsxs(View, { style: { alignItems: 'flex-end', gap: 4 }, children: [_jsx(Text, { style: { color: item.unreadCount ? c.accent : c.secondary, fontSize: 12 }, children: date ? date.toDateString() === new Date().toDateString() ? date.toLocaleTimeString(uiLocale, { hour: 'numeric', minute: '2-digit' }) : date.toLocaleDateString(uiLocale, { month: 'short', day: 'numeric' }) : '' }), item.unreadCount > 0 && _jsx(View, { style: { minWidth: 20, height: 20, borderRadius: 10, borderWidth: 1.5, borderColor: c.accent, alignItems: 'center', justifyContent: 'center', paddingHorizontal: 5 }, children: _jsx(Text, { style: { color: c.accent, fontSize: 10, fontWeight: '900' }, children: item.unreadCount > 99 ? '99+' : item.unreadCount }) })] })] });
|
|
72
|
+
} }), selected && _jsxs(_Fragment, { children: [_jsx(MessagingMenuSurface, { visible: menuVisible, onDismiss: dismissed, onClose: () => { if (!busy)
|
|
73
|
+
setSelected(null); }, children: rect => {
|
|
74
|
+
const width = Math.min(218, rect.width - 24), top = Math.max(12, Math.min(anchor.y - rect.top, rect.height - 124));
|
|
75
|
+
return _jsxs(View, { style: { flex: 1 }, children: [_jsx(Pressable, { accessibilityLabel: uiText("Close chat options"), onPress: () => { if (!busy)
|
|
76
|
+
setSelected(null); }, style: { position: 'absolute', inset: 0, backgroundColor: messagingPopupBackdrop } }), _jsx(View, { testID: "messaging-inbox-dropdown", style: { position: 'absolute', left: Math.max(12, Math.min(anchor.x - rect.left, rect.width - width - 12)), top, width, maxHeight: Math.max(44, rect.height - top - 12), borderRadius: 16, borderWidth: .5, borderColor: '#ffffff22', backgroundColor: '#151515', paddingVertical: 4, overflow: 'hidden' }, children: _jsxs(ScrollView, { keyboardShouldPersistTaps: "handled", children: [_jsx(MessageMenuRow, { icon: selected.muted ? 'notifications-outline' : 'notifications-off-outline', label: selected.muted ? uiText("Unmute") : uiText("Mute"), disabled: busy, onPress: () => void act(false) }), _jsx(MessageMenuRow, { icon: "trash-outline", label: uiText("Delete chat"), danger: true, divider: true, disabled: busy, onPress: showConfirmation }), !!error && _jsx(Text, { accessibilityRole: "alert", style: { color: '#ffffff', padding: 12 }, children: error })] }) })] });
|
|
77
|
+
} }), confirm && _jsxs(MessagingPopup, { visible: confirmVisible, onDismiss: dismissed, compact: true, title: uiText('Delete chat with {name}?', { name: title(selected) }), foreground: c.foreground, closeLabel: uiText("Close delete chat"), closeDisabled: busy, background: c.background, onClose: returnToMenu, children: [_jsx(Text, { style: { color: messagingPopupColors.secondary, marginTop: 8 }, children: uiText("This removes the chat and its history from your view. Other people keep their copy. New messages can bring the chat back.") }), !!error && _jsx(Text, { accessibilityRole: "alert", style: { color: messagingPopupColors.foreground, paddingVertical: 8 }, children: error }), _jsxs(View, { style: { flexDirection: 'row', gap: 8, marginTop: 12 }, children: [_jsx(Pressable, { accessibilityRole: "button", disabled: busy, onPress: returnToMenu, style: { flex: 1, minHeight: 44, borderRadius: 12, backgroundColor: messagingPopupColors.incoming, alignItems: 'center', justifyContent: 'center' }, children: _jsx(Text, { style: { color: messagingPopupColors.foreground, fontSize: 16, fontWeight: '600' }, children: uiText("Cancel") }) }), _jsx(Pressable, { accessibilityRole: "button", accessibilityLabel: uiText("Confirm delete chat"), disabled: busy, onPress: () => void act(true), style: { flex: 1, minHeight: 44, borderRadius: 12, backgroundColor: '#b72d34', alignItems: 'center', justifyContent: 'center' }, children: _jsx(Text, { style: { color: '#ffffff', fontSize: 16, fontWeight: '600' }, children: uiText("Delete") }) })] })] })] })] }) });
|
|
69
78
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { MessagingCustomization } from './customization.js';
|
|
2
|
+
export { RobonoLocaleProvider, useRobonoLocale } from './localization.js';
|
|
1
3
|
import { type MessagingAccount } from './account.js';
|
|
2
4
|
import { type MessagingCallActions } from './header-calls.js';
|
|
3
5
|
export type { MessagingAccount } from './account.js';
|
|
@@ -32,7 +34,8 @@ export interface TranslationVoiceProps {
|
|
|
32
34
|
showWaveform?: boolean;
|
|
33
35
|
autoPlay?: boolean;
|
|
34
36
|
}
|
|
35
|
-
export
|
|
37
|
+
export type { MessagingCustomization, MessageRegionProps, HeaderRegionProps, CustomMessageProps, CustomMessageAction } from './customization.js';
|
|
38
|
+
export interface MessagingComponents extends MessagingCustomization {
|
|
36
39
|
TrackingMap?: ComponentType<TrackingMapProps>;
|
|
37
40
|
PrivacyLayer?: ComponentType<PrivacyLayerProps>;
|
|
38
41
|
SocialPlayer?: ComponentType<SocialPlayerProps>;
|
|
@@ -57,6 +60,10 @@ export interface AttachmentChoice {
|
|
|
57
60
|
durationMs?: number;
|
|
58
61
|
}
|
|
59
62
|
export interface MessagingScreenProps {
|
|
63
|
+
/** Compose surrounding navigation while keeping a single message context. */
|
|
64
|
+
renderLayout?: (conversation: ReactNode) => ReactNode;
|
|
65
|
+
/** Language of controls and accessibility labels; separate from translated message content. */
|
|
66
|
+
interfaceLocale?: import('@robono/messaging').RobonoLocale;
|
|
60
67
|
/** Optional tactile feedback at the quote threshold; supplied by the native screen. */
|
|
61
68
|
onQuoteReady?: () => void;
|
|
62
69
|
companyName?: string;
|
|
@@ -128,7 +135,7 @@ export interface MessagingScreenProps {
|
|
|
128
135
|
openAttachment?: (message: MessagingMessage) => Promise<void>;
|
|
129
136
|
openSavedAttachment?: (record: SavedMessage) => Promise<void>;
|
|
130
137
|
}
|
|
131
|
-
interface MessagingContext extends MessagingScreenProps {
|
|
138
|
+
export interface MessagingContext extends MessagingScreenProps {
|
|
132
139
|
state: MessagingState;
|
|
133
140
|
discussionPreview?: boolean;
|
|
134
141
|
discussionRoot?: string;
|
|
@@ -149,8 +156,8 @@ interface MessagingContext extends MessagingScreenProps {
|
|
|
149
156
|
setReply: (message: MessagingMessage | null) => void;
|
|
150
157
|
}
|
|
151
158
|
export declare function useMessagingUI(): MessagingContext;
|
|
152
|
-
export declare function MessagingProvider(
|
|
153
|
-
children: ReactNode;
|
|
159
|
+
export declare function MessagingProvider(props: MessagingScreenProps & {
|
|
160
|
+
children: React.ReactNode;
|
|
154
161
|
}): React.JSX.Element;
|
|
155
162
|
export declare function messagingHeaderColors(design: MessagingAppearance, group: boolean): {
|
|
156
163
|
background: string;
|
|
@@ -161,18 +168,21 @@ export declare function MessagingHeader({ children }?: {
|
|
|
161
168
|
}): React.JSX.Element;
|
|
162
169
|
export declare function MessagingBubble({ message, pendingStatus, previousMessage, nextMessage }: {
|
|
163
170
|
message: MessagingMessage;
|
|
164
|
-
pendingStatus?: 'sending' | 'failed';
|
|
171
|
+
pendingStatus?: 'sending' | 'queued' | 'failed';
|
|
165
172
|
previousMessage?: MessagingMessage;
|
|
166
173
|
nextMessage?: MessagingMessage;
|
|
167
174
|
}): React.JSX.Element;
|
|
168
175
|
export declare function MessagingList(): React.JSX.Element;
|
|
169
176
|
export declare function MessagingComposer(): React.JSX.Element;
|
|
170
177
|
/** Account navigation. Mount in the inbox/app toolbar, never in a contact menu. */
|
|
171
|
-
export declare function MessagingAccountMenu({ onNewGroup, onMaps }?: {
|
|
178
|
+
export declare function MessagingAccountMenu({ onNewGroup, onMaps, onPublicRooms }?: {
|
|
172
179
|
onNewGroup?: () => void;
|
|
173
180
|
onMaps?: () => void;
|
|
181
|
+
onPublicRooms?: () => void;
|
|
174
182
|
}): React.JSX.Element;
|
|
175
183
|
export declare function MessagingConversation(): React.JSX.Element;
|
|
176
184
|
/** Embed inside your app's safe area and navigation. No engine, push or global audio side effects. */
|
|
177
185
|
export declare function MessagingScreen(props: MessagingScreenProps): React.JSX.Element;
|
|
178
186
|
export { MessagingInbox, type MessagingInboxProps } from './inbox.js';
|
|
187
|
+
export type { PlaybackProps } from './media-ui.js';
|
|
188
|
+
export type { InboxViewMemory } from './inbox-view.js';
|