@robono/messaging-react-native 0.9.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 +4 -0
- package/README.md +52 -0
- package/SBOM.spdx.json +494 -206
- package/dist/account.js +2 -1
- package/dist/attachment-menu.js +2 -1
- package/dist/call-message.js +2 -1
- package/dist/calling.js +4 -2
- package/dist/conversation-details.d.ts +1 -0
- package/dist/conversation-details.js +3 -2
- package/dist/custom-message.d.ts +7 -0
- package/dist/custom-message.js +53 -0
- package/dist/customization.d.ts +49 -0
- package/dist/customization.js +1 -0
- package/dist/expo-connected-calling.js +4 -2
- package/dist/expo-messenger.d.ts +4 -1
- package/dist/expo-messenger.js +34 -8
- package/dist/expo-screen.js +20 -18
- package/dist/expo-social.js +2 -1
- package/dist/extras.js +2 -1
- package/dist/forward.js +2 -1
- package/dist/group-editor.js +2 -1
- package/dist/header-calls.js +2 -1
- package/dist/icons.js +2 -1
- package/dist/inbox-search.d.ts +3 -1
- package/dist/inbox-search.js +30 -9
- package/dist/inbox-view.d.ts +21 -0
- package/dist/inbox-view.js +32 -0
- package/dist/inbox.d.ts +6 -1
- package/dist/inbox.js +24 -17
- package/dist/index.d.ts +10 -3
- package/dist/index.js +40 -29
- package/dist/maps-connection.js +2 -1
- package/dist/media-ui.js +2 -1
- package/dist/message-menu.js +2 -1
- package/dist/native-outbox.d.ts +1 -1
- package/dist/polls.js +4 -3
- package/dist/popup.js +2 -1
- package/dist/public-rooms.d.ts +10 -0
- package/dist/public-rooms.js +78 -0
- package/dist/quick-send.js +2 -1
- package/dist/room-moderation.d.ts +22 -0
- package/dist/room-moderation.js +87 -0
- package/dist/safety.js +2 -1
- package/dist/saved.js +2 -1
- package/dist/scheduled.js +2 -1
- package/dist/search.js +2 -1
- package/dist/social.js +2 -1
- package/dist/translation-settings.js +2 -1
- package/dist/typography.d.ts +12 -0
- package/dist/typography.js +31 -0
- package/dist/voice-composer.js +2 -1
- package/package.json +6 -6
package/dist/inbox-search.js
CHANGED
|
@@ -1,11 +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';
|
|
2
4
|
import { useRobonoLocale } from './localization.js';
|
|
3
5
|
import { useEffect, useRef, useState } from 'react';
|
|
4
|
-
import { ActivityIndicator, FlatList, Pressable,
|
|
6
|
+
import { ActivityIndicator, FlatList, Pressable, View } from 'react-native';
|
|
5
7
|
/** Main-screen search covers retained messages beyond the current inbox page. */
|
|
6
|
-
export function InboxMessageSearch({ client, query, appearance, name, chats, onSelect }) {
|
|
8
|
+
export function InboxMessageSearch({ client, query, appearance, name, chats, onSelect, viewMemory }) {
|
|
7
9
|
const { text: uiText, locale: uiLocale, number: uiNumber, count: uiCount } = useRobonoLocale();
|
|
8
|
-
const c = appearance.colors, [rows, setRows] = useState([]), [next, setNext] = useState(null), [busy, setBusy] = useState(
|
|
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);
|
|
9
12
|
const controller = useRef(null), generation = useRef(0);
|
|
10
13
|
async function search(more = false) {
|
|
11
14
|
controller.current?.abort();
|
|
@@ -15,15 +18,33 @@ export function InboxMessageSearch({ client, query, appearance, name, chats, onS
|
|
|
15
18
|
setBusy(true);
|
|
16
19
|
setError('');
|
|
17
20
|
try {
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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()]);
|
|
22
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);
|
|
23
43
|
}
|
|
24
44
|
catch (e) {
|
|
25
45
|
if (version === generation.current && !control.signal.aborted) {
|
|
26
|
-
|
|
46
|
+
if (!more || e?.status === 401 || e?.status === 403)
|
|
47
|
+
setRows([]);
|
|
27
48
|
setError(e instanceof Error ? e.message : uiText("Messages could not be searched."));
|
|
28
49
|
}
|
|
29
50
|
}
|
|
@@ -40,5 +61,5 @@ export function InboxMessageSearch({ client, query, appearance, name, chats, onS
|
|
|
40
61
|
const timer = setTimeout(() => void search(), 300);
|
|
41
62
|
return () => { clearTimeout(timer); ++generation.current; controller.current?.abort(); };
|
|
42
63
|
}, [client, query]);
|
|
43
|
-
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: 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(), 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") }) })] }) });
|
|
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") }) })] }) });
|
|
44
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,21 +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';
|
|
2
5
|
import { useRobonoLocale } from './localization.js';
|
|
3
6
|
import { MessageMenuRow } from './message-menu.js';
|
|
4
7
|
import { InboxMessageSearch } from './inbox-search.js';
|
|
5
8
|
import { MessageIcon } from './icons.js';
|
|
6
9
|
import { useEffect, useRef, useState, useSyncExternalStore } from 'react';
|
|
7
|
-
import { AppState, FlatList, Image, Platform, Pressable, ScrollView,
|
|
10
|
+
import { AppState, FlatList, Image, Platform, Pressable, ScrollView, View } from 'react-native';
|
|
8
11
|
import { defaultMessagingSettings } from '@robono/messaging';
|
|
9
12
|
import { MessagingPopup, MessagingMenuSurface, MessagingPopupHeaderBoundary, messagingPopupColors, messagingPopupBackdrop } from './popup.js';
|
|
10
13
|
/** Compact Messenger chat rows: avatar, name, last message, time and unread badge. */
|
|
11
|
-
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 }) {
|
|
12
15
|
const { text: uiText, locale: uiLocale, number: uiNumber, count: uiCount } = useRobonoLocale();
|
|
13
16
|
const state = useSyncExternalStore(client.subscribe, client.getSnapshot, client.getSnapshot), design = appearance ?? state.appearance ?? defaultMessagingSettings().appearance, c = design.colors;
|
|
14
17
|
useEffect(() => { const update = (value) => { if (active && (value === null || value === 'active'))
|
|
15
18
|
client.start();
|
|
16
19
|
else
|
|
17
20
|
client.stop(); }; update(AppState.currentState); const sub = AppState.addEventListener('change', update); return () => { sub.remove(); client.stop(); }; }, [client, active]);
|
|
18
|
-
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);
|
|
19
26
|
const [selected, setSelected] = useState(null), [confirm, setConfirm] = useState(false), [busy, setBusy] = useState(false), [error, setError] = useState('');
|
|
20
27
|
const title = (row) => row.kind === 'group' ? row.name ?? 'Group' : contacts[row.contactId ?? '']?.name ?? row.contactId ?? 'Chat';
|
|
21
28
|
const rows = useRef(new Map()), pending = useRef(null);
|
|
@@ -54,18 +61,18 @@ export function MessagingInbox({ client, userId, contacts = {}, appearance, acti
|
|
|
54
61
|
} }
|
|
55
62
|
const preview = (row) => { const m = row.lastMessage; if (!m)
|
|
56
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; };
|
|
57
|
-
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: 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, { 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 ? 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 }) => {
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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") }) })] })] })] })] }) });
|
|
71
78
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { MessagingCustomization } from './customization.js';
|
|
1
2
|
export { RobonoLocaleProvider, useRobonoLocale } from './localization.js';
|
|
2
3
|
import { type MessagingAccount } from './account.js';
|
|
3
4
|
import { type MessagingCallActions } from './header-calls.js';
|
|
@@ -33,7 +34,8 @@ export interface TranslationVoiceProps {
|
|
|
33
34
|
showWaveform?: boolean;
|
|
34
35
|
autoPlay?: boolean;
|
|
35
36
|
}
|
|
36
|
-
export
|
|
37
|
+
export type { MessagingCustomization, MessageRegionProps, HeaderRegionProps, CustomMessageProps, CustomMessageAction } from './customization.js';
|
|
38
|
+
export interface MessagingComponents extends MessagingCustomization {
|
|
37
39
|
TrackingMap?: ComponentType<TrackingMapProps>;
|
|
38
40
|
PrivacyLayer?: ComponentType<PrivacyLayerProps>;
|
|
39
41
|
SocialPlayer?: ComponentType<SocialPlayerProps>;
|
|
@@ -58,6 +60,8 @@ export interface AttachmentChoice {
|
|
|
58
60
|
durationMs?: number;
|
|
59
61
|
}
|
|
60
62
|
export interface MessagingScreenProps {
|
|
63
|
+
/** Compose surrounding navigation while keeping a single message context. */
|
|
64
|
+
renderLayout?: (conversation: ReactNode) => ReactNode;
|
|
61
65
|
/** Language of controls and accessibility labels; separate from translated message content. */
|
|
62
66
|
interfaceLocale?: import('@robono/messaging').RobonoLocale;
|
|
63
67
|
/** Optional tactile feedback at the quote threshold; supplied by the native screen. */
|
|
@@ -131,7 +135,7 @@ export interface MessagingScreenProps {
|
|
|
131
135
|
openAttachment?: (message: MessagingMessage) => Promise<void>;
|
|
132
136
|
openSavedAttachment?: (record: SavedMessage) => Promise<void>;
|
|
133
137
|
}
|
|
134
|
-
interface MessagingContext extends MessagingScreenProps {
|
|
138
|
+
export interface MessagingContext extends MessagingScreenProps {
|
|
135
139
|
state: MessagingState;
|
|
136
140
|
discussionPreview?: boolean;
|
|
137
141
|
discussionRoot?: string;
|
|
@@ -171,11 +175,14 @@ export declare function MessagingBubble({ message, pendingStatus, previousMessag
|
|
|
171
175
|
export declare function MessagingList(): React.JSX.Element;
|
|
172
176
|
export declare function MessagingComposer(): React.JSX.Element;
|
|
173
177
|
/** Account navigation. Mount in the inbox/app toolbar, never in a contact menu. */
|
|
174
|
-
export declare function MessagingAccountMenu({ onNewGroup, onMaps }?: {
|
|
178
|
+
export declare function MessagingAccountMenu({ onNewGroup, onMaps, onPublicRooms }?: {
|
|
175
179
|
onNewGroup?: () => void;
|
|
176
180
|
onMaps?: () => void;
|
|
181
|
+
onPublicRooms?: () => void;
|
|
177
182
|
}): React.JSX.Element;
|
|
178
183
|
export declare function MessagingConversation(): React.JSX.Element;
|
|
179
184
|
/** Embed inside your app's safe area and navigation. No engine, push or global audio side effects. */
|
|
180
185
|
export declare function MessagingScreen(props: MessagingScreenProps): React.JSX.Element;
|
|
181
186
|
export { MessagingInbox, type MessagingInboxProps } from './inbox.js';
|
|
187
|
+
export type { PlaybackProps } from './media-ui.js';
|
|
188
|
+
export type { InboxViewMemory } from './inbox-view.js';
|