@robono/messaging-react-native 0.2.0 → 0.3.1
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 +55 -0
- package/README.md +84 -7
- package/SBOM.spdx.json +1290 -234
- package/dist/account.d.ts +15 -0
- package/dist/account.js +27 -0
- package/dist/call-message.js +7 -1
- package/dist/calling.js +5 -9
- package/dist/conversation-details.d.ts +24 -0
- package/dist/conversation-details.js +83 -0
- package/dist/emoji-text.d.ts +2 -0
- package/dist/emoji-text.js +8 -0
- package/dist/expo-history.d.ts +3 -0
- package/dist/expo-history.js +59 -0
- package/dist/expo-media.d.ts +1 -0
- package/dist/expo-media.js +46 -43
- package/dist/expo-messenger.d.ts +35 -0
- package/dist/expo-messenger.js +39 -0
- package/dist/expo-notifications.d.ts +31 -0
- package/dist/expo-notifications.js +92 -0
- package/dist/expo-outbox.js +2 -1
- package/dist/expo-privacy.d.ts +4 -0
- package/dist/expo-privacy.js +7 -0
- package/dist/expo-screen.d.ts +42 -0
- package/dist/expo-screen.js +557 -0
- package/dist/expo.d.ts +2 -42
- package/dist/expo.js +2 -549
- package/dist/extras-lifecycle.d.ts +2 -0
- package/dist/extras-lifecycle.js +27 -0
- package/dist/extras.d.ts +49 -0
- package/dist/extras.js +49 -0
- package/dist/forward.js +4 -3
- package/dist/group-editor.d.ts +18 -0
- package/dist/group-editor.js +98 -0
- package/dist/header-calls.d.ts +18 -0
- package/dist/header-calls.js +45 -0
- package/dist/icons.d.ts +2 -0
- package/dist/icons.js +2 -0
- package/dist/inbox-search.d.ts +11 -0
- package/dist/inbox-search.js +42 -0
- package/dist/inbox.d.ts +5 -2
- package/dist/inbox.js +19 -15
- package/dist/index.d.ts +33 -3
- package/dist/index.js +359 -141
- package/dist/media-controller.d.ts +3 -0
- package/dist/media-controller.js +39 -0
- package/dist/media-ui.d.ts +14 -3
- package/dist/media-ui.js +90 -28
- package/dist/message-menu.d.ts +28 -0
- package/dist/message-menu.js +24 -0
- package/dist/message-order.d.ts +3 -0
- package/dist/message-order.js +10 -0
- package/dist/messenger-video-view.d.ts +8 -0
- package/dist/messenger-video-view.js +46 -0
- package/dist/native-outbox.d.ts +8 -2
- package/dist/native-outbox.js +3 -1
- package/dist/photo-gesture.d.ts +20 -0
- package/dist/photo-gesture.js +52 -0
- package/dist/polls.d.ts +2 -1
- package/dist/polls.js +19 -17
- package/dist/popup.d.ts +43 -1
- package/dist/popup.js +72 -6
- package/dist/quick-send.d.ts +15 -0
- package/dist/quick-send.js +26 -0
- package/dist/safety.d.ts +9 -1
- package/dist/safety.js +21 -6
- package/dist/saved.d.ts +1 -1
- package/dist/saved.js +9 -4
- package/dist/search.d.ts +2 -2
- package/dist/search.js +32 -14
- package/dist/social.d.ts +9 -3
- package/dist/social.js +11 -15
- package/dist/translation-settings.d.ts +12 -0
- package/dist/translation-settings.js +16 -0
- package/dist/use-quote-gesture.d.ts +5 -1
- package/dist/use-quote-gesture.js +35 -17
- package/dist/visual/WeatherArtwork.d.ts +9 -0
- package/dist/visual/WeatherArtwork.js +43 -0
- package/dist/visual/weather-artwork.d.ts +17 -0
- package/dist/visual/weather-artwork.js +86 -0
- package/dist/visual/weather-conditions.d.ts +5 -0
- package/dist/visual/weather-conditions.js +2 -0
- package/dist/voice-composer.js +2 -2
- package/dist/voice-waveform.d.ts +2 -0
- package/dist/voice-waveform.js +21 -0
- package/package.json +24 -4
package/dist/extras.js
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { forwardRef, useEffect, useImperativeHandle, useState } from 'react';
|
|
3
|
+
import { ActivityIndicator, Image, Linking, Platform, Pressable, ScrollView, Text, View } from 'react-native';
|
|
4
|
+
import { currentTrackers, trackerFresh, trackerETA, weatherTemperature } from '@robono/messaging';
|
|
5
|
+
import { MessagingPopup, messagingPopupColors } from './popup.js';
|
|
6
|
+
import { MessageIcon } from './icons.js';
|
|
7
|
+
import WeatherArtwork from './visual/WeatherArtwork.js';
|
|
8
|
+
import { weatherConditions } from './visual/weather-conditions.js';
|
|
9
|
+
function ForecastIcon({ period, size = 32 }) {
|
|
10
|
+
const condition = period.condition.trim().toLowerCase();
|
|
11
|
+
const match = weatherConditions.find(c => [c.day, c.night].some(name => name.toLowerCase() === condition));
|
|
12
|
+
return _jsx(View, { accessible: true, accessibilityLabel: period.condition, style: { width: size, height: size, flexShrink: 0 }, children: _jsx(WeatherArtwork, { code: period.code ?? match?.code, isDay: period.isDay ?? condition !== 'clear', cloud: period.cloud, windKph: period.windKph, size: size }) });
|
|
13
|
+
}
|
|
14
|
+
export const privacyReadingStripHeight = 74;
|
|
15
|
+
/** Messenger's fixed 74px reading strip. Scrolling moves messages through it. */
|
|
16
|
+
export function MessagingPrivacyLayer({ children, enabled, background }) { return _jsxs(View, { style: { flex: 1, minHeight: 0, overflow: 'hidden' }, children: [children, enabled && _jsx(View, { pointerEvents: "none", testID: "privacy-lens-mask", style: { position: 'absolute', top: 0, left: 0, right: 0, bottom: privacyReadingStripHeight, backgroundColor: Platform.OS === 'web' ? background + '30' : background, ...(Platform.OS === 'web' ? { backdropFilter: 'blur(9px)', WebkitBackdropFilter: 'blur(9px)' } : {}) } })] }); }
|
|
17
|
+
export async function openChatAddress(address) { const query = encodeURIComponent(address); if (Platform.OS === 'ios') {
|
|
18
|
+
try {
|
|
19
|
+
await Linking.openURL(`geo-navigation:///place?address=${query}`);
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
catch { }
|
|
23
|
+
await Linking.openURL(`https://maps.apple.com/?q=${query}`);
|
|
24
|
+
}
|
|
25
|
+
else
|
|
26
|
+
await Linking.openURL(`https://www.google.com/maps/search/?api=1&query=${query}`); }
|
|
27
|
+
export function ExtraPopup({ title, onClose, background, foreground, children, expanded = true }) { return _jsx(MessagingPopup, { expanded: expanded, title: title, foreground: foreground, background: background, onClose: onClose, children: children }); }
|
|
28
|
+
export const MessageExtra = forwardRef(function MessageExtra({ addressBubbleHeight = 44, extra, messageId, userId, extras, ink, accent, background, foreground, footer, renderAddress, Map, onCall, onLongPress }, ref) {
|
|
29
|
+
const [mapHeight, setMapHeight] = useState(230);
|
|
30
|
+
const [open, setOpen] = useState(false), [fahrenheit, setFahrenheit] = useState(extra.weather?.fahrenheit ?? false), [imperial, setImperial] = useState(false), [now, setNow] = useState(Date.now()), [error, setError] = useState(''), [busy, setBusy] = useState(false), [selected, setSelected] = useState();
|
|
31
|
+
useImperativeHandle(ref, () => ({ open: () => setOpen(true) }));
|
|
32
|
+
useEffect(() => { const timer = setInterval(() => setNow(Date.now()), 1000); return () => clearInterval(timer); }, []);
|
|
33
|
+
const weather = extra.weather, address = extra.address, call = extra.call, trackers = currentTrackers(address?.trackers ?? [], now), mine = trackers.find(t => t.userId === userId);
|
|
34
|
+
const act = async (fn) => { if (busy)
|
|
35
|
+
return; setBusy(true); setError(''); try {
|
|
36
|
+
await fn();
|
|
37
|
+
}
|
|
38
|
+
catch (e) {
|
|
39
|
+
setError(e instanceof Error ? e.message : 'Please try again.');
|
|
40
|
+
}
|
|
41
|
+
finally {
|
|
42
|
+
setBusy(false);
|
|
43
|
+
} };
|
|
44
|
+
const text = { color: messagingPopupColors.foreground, fontSize: 15 }, small = { color: messagingPopupColors.foreground, fontSize: 12, opacity: .7 }, row = { flexDirection: 'row', alignItems: 'center', gap: 8 }, title = { color: ink, fontSize: 16, fontWeight: '600' };
|
|
45
|
+
const avatarSize = Math.max(8, Math.min(32, addressBubbleHeight - 34));
|
|
46
|
+
const addressAction = _jsxs(View, { testID: "address-tracking", style: { flexShrink: 0, maxWidth: 136, height: trackers.length ? addressBubbleHeight : 32, justifyContent: 'center', alignItems: 'center', gap: 2, overflow: 'hidden' }, children: [_jsxs(Pressable, { accessibilityRole: "button", accessibilityLabel: mine ? 'End tracking' : 'Allow tracking', accessibilityHint: "Share your live location with this chat for 15 minutes", disabled: busy || !mine && !extras.getSnapshot().available.tracking, hitSlop: { top: 6, bottom: 6, left: 0, right: 0 }, onPress: event => { event.stopPropagation(); void act(() => mine ? extras.stopTracking(mine.sessionId) : extras.startTracking(messageId)); }, accessibilityState: { busy, disabled: busy || !mine && !extras.getSnapshot().available.tracking }, style: ({ pressed }) => ({ minHeight: 32, paddingHorizontal: 10, paddingVertical: 5, borderRadius: 16, borderWidth: 1, borderColor: accent + (mine ? 'b0' : '70'), backgroundColor: accent + (pressed ? '30' : mine ? '20' : '0d'), flexDirection: 'row', alignItems: 'center', justifyContent: 'center', gap: 6, opacity: (busy || !mine && !extras.getSnapshot().available.tracking) ? 0.45 : 1 }), children: [busy ? _jsx(ActivityIndicator, { size: 16, color: accent }) : _jsx(MessageIcon, { name: mine ? 'stop' : 'location-outline', size: 16, color: accent }), _jsx(Text, { numberOfLines: 1, style: { color: foreground, fontSize: 12, lineHeight: 18, fontWeight: '600' }, children: mine ? 'End tracking' : 'Allow tracking' })] }), trackers.length > 0 && _jsx(View, { testID: "tracking-map-avatars", style: { flexDirection: 'row', flexWrap: 'nowrap', justifyContent: 'center', gap: 2 }, children: trackers.slice(0, 4).map(t => _jsx(Pressable, { accessibilityRole: "button", accessibilityLabel: 'Show ' + (t.userId === userId ? 'your' : t.name + "’s") + ' location on map', onPress: event => { event.stopPropagation(); setSelected(t.userId); setOpen(true); }, style: ({ pressed }) => ({ width: avatarSize, height: avatarSize, borderRadius: avatarSize / 2, alignItems: 'center', justifyContent: 'center', backgroundColor: accent + (pressed ? '30' : '12'), borderWidth: 1, borderColor: accent + '70' }), children: t.avatarUrl ? _jsx(Image, { source: { uri: t.avatarUrl }, style: { width: avatarSize - 4, height: avatarSize - 4, borderRadius: avatarSize / 2 } }) : _jsx(Text, { style: { color: foreground, fontSize: Math.min(18, avatarSize * .55), fontWeight: '600' }, children: t.name.slice(0, 1).toUpperCase() }) }, t.sessionId)) })] });
|
|
47
|
+
const addressParticipants = trackers.length > 0 && _jsx(View, { testID: "address-participants", pointerEvents: "none", style: { alignSelf: 'stretch' }, children: _jsx(Text, { style: { color: ink, fontSize: 13, lineHeight: 18 }, children: trackers.map(t => `${t.userId === userId ? 'You' : t.name} · ${trackerETA(t, now)}`).join(' · ') }) });
|
|
48
|
+
return _jsxs(_Fragment, { children: [weather && _jsxs(Pressable, { testID: "weather-card", accessibilityRole: "button", accessibilityLabel: "Open weather", onLongPress: onLongPress, onPress: () => setOpen(true), style: { width: 268, maxWidth: '100%', paddingHorizontal: 4, paddingVertical: 2 }, children: [_jsxs(View, { style: row, children: [_jsx(ForecastIcon, { period: weather.current, size: 34 }), _jsxs(View, { style: { flex: 1 }, children: [_jsxs(Text, { style: title, children: [weatherTemperature(weather.current.c, weather.fahrenheit ?? false), " \u00B7 ", weather.current.condition] }), _jsx(Text, { style: { color: ink, fontSize: 12, opacity: .75 }, children: weather.place })] })] }), _jsxs(View, { style: { ...row, justifyContent: 'space-between', marginTop: 2 }, children: [_jsx(Text, { style: { color: ink, fontSize: 11 }, children: weather.current.detail }), footer] })] }), address && (renderAddress ? renderAddress(addressAction, addressParticipants) : _jsxs(_Fragment, { children: [addressAction, addressParticipants] })), call && _jsxs(Pressable, { testID: "call-card", accessibilityRole: "button", accessibilityLabel: "Open call details", onLongPress: onLongPress, onPress: () => setOpen(true), style: { ...row, width: 268, maxWidth: '100%', padding: 4 }, children: [_jsx(MessageIcon, { name: call.kind === 'video' ? 'video' : 'call', color: call.outcome === 'missed' ? '#c94040' : accent, size: 26 }), _jsxs(View, { style: { flex: 1 }, children: [_jsxs(Text, { style: title, children: [call.outcome === 'missed' ? 'Missed ' : call.direction === 'incoming' ? 'Incoming ' : 'Outgoing ', call.kind === 'video' ? 'video' : 'voice', " call"] }), _jsx(Text, { style: { color: ink, fontSize: 12 }, children: call.outcome === 'completed' ? `${Math.floor(call.durationSeconds / 60)}:${String(Math.floor(call.durationSeconds % 60)).padStart(2, '0')}` : call.outcome })] }), footer] }), open && _jsxs(ExtraPopup, { expanded: !!address || !!weather, title: weather ? 'Weather' : address ? 'Live tracking' : 'Call details', onClose: () => setOpen(false), background: background, foreground: foreground, children: [address && _jsxs(View, { style: { flex: 1, minHeight: 0 }, children: [_jsx(Pressable, { accessibilityRole: "link", accessibilityLabel: "Open destination in maps", onPress: () => void act(() => openChatAddress(address.text)), children: _jsx(Text, { style: { ...text, color: accent, marginBottom: 10 }, numberOfLines: 2, children: address.text }) }), _jsx(View, { testID: "tracking-map-area", onLayout: event => setMapHeight(Math.max(1, Math.round(event.nativeEvent.layout.height))), style: { flex: 1, minHeight: 120, overflow: 'hidden' }, children: address.destination && Map ? _jsx(Map, { imperial: imperial, now: now, height: mapHeight, destination: address.destination, trackers: trackers.filter(t => trackerFresh(t, now)), accent: accent, selectedUserId: selected, onSelect: setSelected }) : _jsx(Text, { style: small, children: "The map is unavailable." }) }), _jsxs(View, { style: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between' }, children: [_jsx(Text, { style: { color: messagingPopupColors.foreground, fontSize: 14 }, children: "Sharing ends after 15 minutes." }), _jsx(Pressable, { accessibilityRole: "button", accessibilityLabel: "Change speed and distance units", onPress: () => setImperial(v => !v), style: { minHeight: 44, paddingHorizontal: 4, justifyContent: 'center' }, children: _jsx(Text, { style: { color: accent, fontSize: 17, fontWeight: '600' }, children: imperial ? 'km/h · km' : 'mph · mi' }) })] }), _jsxs(ScrollView, { style: { flexGrow: 0, maxHeight: 156 }, children: [!trackers.length && _jsx(Text, { style: text, children: "No one is sharing their location." }), trackers.map(t => _jsxs(Pressable, { onPress: () => setSelected(t.userId), accessibilityRole: "button", accessibilityLabel: 'Locate ' + t.name, style: { minHeight: 74, paddingVertical: 8, borderBottomWidth: 1, borderColor: messagingPopupColors.foreground + '18' }, children: [_jsxs(View, { style: { ...row, justifyContent: 'space-between' }, children: [_jsx(Text, { style: { color: messagingPopupColors.foreground, fontSize: 21, lineHeight: 27, fontWeight: '600', flexShrink: 1 }, children: t.userId === userId ? 'You' : t.name }), _jsx(Text, { style: { color: accent, fontSize: 25, lineHeight: 30, fontWeight: '700' }, children: trackerETA(t, now) })] }), _jsxs(View, { style: { ...row, justifyContent: 'space-between', alignItems: 'flex-start' }, children: [_jsx(Text, { style: { color: messagingPopupColors.foreground, fontSize: 19, lineHeight: 26, flex: 1 }, children: trackerFresh(t, now) ? t.speedMps === null ? 'Speed unavailable' : Math.round(t.speedMps * (imperial ? 2.23694 : 3.6)) + (imperial ? ' mph' : ' km/h') : 'Waiting for a fresh location' }), trackerFresh(t, now) && _jsx(Text, { testID: "tracking-distance", style: { color: messagingPopupColors.foreground, fontSize: 19, lineHeight: 26, textAlign: 'right', flexShrink: 1 }, children: t.distanceMeters === null ? 'Distance unavailable' : (t.distanceMeters / (imperial ? 1609.344 : 1000)).toFixed(1) + (imperial ? ' mi' : ' km') + ' away' })] })] }, t.sessionId))] })] }), !address && _jsxs(ScrollView, { style: weather ? { flex: 1, minHeight: 0 } : { flexShrink: 1 }, children: [weather && (weather.expiresAt <= now ? _jsx(Text, { style: text, children: "Weather details expired." }) : _jsxs(_Fragment, { children: [_jsx(Text, { style: small, children: weather.place }), _jsxs(View, { style: { ...row, marginVertical: 8 }, children: [_jsx(ForecastIcon, { period: weather.current, size: 36 }), _jsxs(Text, { style: { ...text, flex: 1, fontWeight: '600' }, children: [weatherTemperature(weather.current.c, fahrenheit), " \u00B7 ", weather.current.condition] }), _jsx(Pressable, { accessibilityRole: "button", accessibilityLabel: "Change temperature unit", onPress: () => setFahrenheit(v => !v), style: { minWidth: 44, minHeight: 44, alignItems: 'center', justifyContent: 'center' }, children: _jsx(Text, { style: { color: accent, fontSize: 16, fontWeight: '600' }, children: fahrenheit ? '°C' : '°F' }) })] }), weather.alert && _jsx(Text, { style: { ...text, color: accent }, children: weather.alert }), _jsxs(View, { style: { marginTop: 20, paddingTop: 16, borderTopWidth: 1, borderTopColor: messagingPopupColors.foreground + '24' }, children: [_jsx(Text, { style: { ...text, fontWeight: '600' }, children: "Next three hours" }), _jsx(View, { style: { ...row, marginTop: 12, alignItems: 'flex-start' }, children: weather.hours.slice(0, 3).map(p => _jsxs(View, { style: { flex: 1, gap: 4 }, children: [_jsx(Text, { style: small, children: p.label }), _jsxs(View, { style: row, children: [_jsx(ForecastIcon, { period: p, size: 32 }), _jsx(Text, { style: text, children: weatherTemperature(p.c, fahrenheit) })] }), _jsx(Text, { style: small, children: p.detail })] }, p.at)) })] }), _jsxs(View, { style: { marginTop: 20, paddingTop: 16, borderTopWidth: 1, borderTopColor: messagingPopupColors.foreground + '24' }, children: [_jsx(Text, { style: { ...text, fontWeight: '600', marginBottom: 8 }, children: "Three-day forecast" }), weather.days.slice(0, 3).map(p => _jsxs(View, { style: { ...row, paddingVertical: 8 }, children: [_jsx(Text, { style: { ...text, width: 45 }, children: p.label }), _jsx(ForecastIcon, { period: p, size: 32 }), _jsxs(View, { style: { flex: 1 }, children: [_jsx(Text, { style: text, children: p.condition }), _jsx(Text, { style: small, children: p.detail })] }), _jsxs(Text, { style: text, children: [weatherTemperature(p.c, fahrenheit), p.lowC !== undefined ? ' / ' + weatherTemperature(p.lowC, fahrenheit) : ''] })] }, p.at))] }), !!weather.attribution && _jsx(Text, { style: { ...small, marginTop: 8 }, children: weather.attribution })] })), call && _jsxs(_Fragment, { children: [_jsxs(Text, { style: { ...text, fontWeight: '600', marginVertical: 8 }, children: [call.kind === 'video' ? 'Video' : 'Voice', " call \u00B7 ", call.outcome] }), _jsx(Text, { style: text, children: new Date(call.startedAt).toLocaleString() }), call.durationSeconds > 0 && _jsxs(Text, { style: { ...text, marginTop: 8 }, children: [Math.floor(call.durationSeconds / 60), " min ", Math.floor(call.durationSeconds % 60), " sec"] }), call.endedByWarning && _jsx(Text, { style: { ...small, marginTop: 10 }, children: "Ended after the network\u2019s time warning." }), onCall && _jsx(Pressable, { accessibilityRole: "button", accessibilityLabel: "Call again", onPress: () => { setOpen(false); onCall(call.kind); }, style: { minHeight: 44, justifyContent: 'center', marginTop: 10 }, children: _jsx(Text, { style: { color: accent, fontWeight: '600' }, children: "Call again" }) })] })] })] }), !!error && _jsx(ExtraPopup, { title: "Message", onClose: () => setError(''), background: background, foreground: foreground, children: _jsx(Text, { accessibilityRole: "alert", style: text, children: error }) })] });
|
|
49
|
+
});
|
package/dist/forward.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { messagingPopupColors } from './popup.js';
|
|
2
3
|
import { useEffect, useRef, useState } from 'react';
|
|
3
4
|
import { ActivityIndicator, Pressable, ScrollView, Text, View } from 'react-native';
|
|
4
5
|
import { MessagingPopup } from './popup.js';
|
|
@@ -6,7 +7,7 @@ import { MessagingPopup } from './popup.js';
|
|
|
6
7
|
export function MessagingForward({ messages, client, appearance, participants = {}, userId, onClose }) {
|
|
7
8
|
const [page, setPage] = useState(null), [loading, setLoading] = useState(false), [busy, setBusy] = useState(false), [error, setError] = useState('');
|
|
8
9
|
const live = useRef(true), inFlight = useRef(false), attempts = useRef(new Map()), done = useRef(new Set());
|
|
9
|
-
const c = appearance.colors;
|
|
10
|
+
const c = { ...appearance.colors, ...messagingPopupColors };
|
|
10
11
|
useEffect(() => { live.current = true; void load(false); return () => { live.current = false; }; }, []);
|
|
11
12
|
async function load(more) { if (inFlight.current)
|
|
12
13
|
return; inFlight.current = true; setLoading(true); setError(''); try {
|
|
@@ -56,6 +57,6 @@ export function MessagingForward({ messages, client, appearance, participants =
|
|
|
56
57
|
setBusy(false);
|
|
57
58
|
}
|
|
58
59
|
}
|
|
59
|
-
return _jsxs(MessagingPopup, { background: c.background, onClose: () => { if (!busy)
|
|
60
|
-
onClose(); }, children: [_jsx(
|
|
60
|
+
return _jsxs(MessagingPopup, { contentWidth: 640, title: "Forward to", foreground: c.foreground, closeLabel: "Close forwarding", closeDisabled: busy, background: c.background, onClose: () => { if (!busy)
|
|
61
|
+
onClose(); }, children: [_jsx(ScrollView, { style: { flex: 1, minHeight: 0, marginTop: 8 }, children: page?.conversations.filter(row => row.canSend).map(row => { const name = row.kind === 'group' ? row.name ?? 'Group' : row.contactId === userId ? 'You' : participants[row.contactId ?? '']?.name ?? row.contactId ?? 'Chat'; return _jsx(Pressable, { accessibilityRole: "button", accessibilityLabel: 'Forward to ' + name, disabled: busy || loading, onPress: () => void send(row), style: { minHeight: 52, justifyContent: 'center', paddingVertical: 12, paddingHorizontal: 8, borderBottomWidth: 1, borderBottomColor: '#ffffff14' }, children: _jsx(Text, { style: { color: c.foreground, fontSize: 16 }, children: name }) }, row.id); }) }), loading || busy ? _jsx(ActivityIndicator, { color: c.accent, style: { margin: 12 } }) : page && !page.conversations.some(row => row.canSend) && _jsx(Text, { style: { color: c.secondary, marginVertical: 12 }, children: "No chats available." }), !!error && _jsx(Text, { accessibilityRole: "alert", style: { color: c.foreground, marginTop: 8 }, children: error }), _jsxs(View, { style: { flexDirection: 'row', justifyContent: 'flex-end', gap: 20, marginTop: 8 }, children: [!page && error && _jsx(Pressable, { accessibilityRole: "button", onPress: () => void load(false), style: { minHeight: 44, justifyContent: 'center' }, children: _jsx(Text, { style: { color: c.accent }, children: "Try again" }) }), page?.hasMore && _jsx(Pressable, { accessibilityRole: "button", disabled: loading || busy, onPress: () => void load(true), style: { minHeight: 44, justifyContent: 'center' }, children: _jsx(Text, { style: { color: c.accent }, children: "More chats" }) })] })] });
|
|
61
62
|
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import type { MessagingClient, MessagingInboxClient, MessagingSettings, MessagingState } from '@robono/messaging';
|
|
3
|
+
export interface MessagingGroupEditorProps {
|
|
4
|
+
client: MessagingClient;
|
|
5
|
+
inbox?: MessagingInboxClient;
|
|
6
|
+
userId: string;
|
|
7
|
+
contacts: Record<string, {
|
|
8
|
+
name: string;
|
|
9
|
+
avatarUrl?: string;
|
|
10
|
+
}>;
|
|
11
|
+
conversation?: MessagingState['conversation'];
|
|
12
|
+
groups: MessagingSettings['groups'];
|
|
13
|
+
accent: string;
|
|
14
|
+
onClose: () => void;
|
|
15
|
+
onSaved: (conversationId: string, name: string) => void;
|
|
16
|
+
}
|
|
17
|
+
/** Messenger's name, group type and contact checklist; changes use member deltas. */
|
|
18
|
+
export declare function MessagingGroupEditor({ client, inbox, userId, contacts, conversation, groups, accent, onClose, onSaved }: MessagingGroupEditorProps): React.JSX.Element;
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useMemo, useRef, useState } from 'react';
|
|
3
|
+
import { ActivityIndicator, FlatList, Image, Pressable, Text, TextInput, View } from 'react-native';
|
|
4
|
+
import { MessageIcon } from './icons.js';
|
|
5
|
+
import { MessagingPopup, messagingPopupColors as c } from './popup.js';
|
|
6
|
+
/** Messenger's name, group type and contact checklist; changes use member deltas. */
|
|
7
|
+
export function MessagingGroupEditor({ client, inbox, userId, contacts, conversation, groups, accent, onClose, onSaved }) {
|
|
8
|
+
const editing = !!conversation, conversationId = client.getSnapshot().conversationId;
|
|
9
|
+
const [name, setName] = useState(conversation?.name ?? ''), [supergroup, setSupergroup] = useState(conversation?.supergroup ?? false);
|
|
10
|
+
const [revision, setRevision] = useState(conversation?.revision ?? 0), [count, setCount] = useState(conversation?.memberCount ?? 0);
|
|
11
|
+
const [original, setOriginal] = useState(new Set()), [added, setAdded] = useState(new Set()), [removed, setRemoved] = useState(new Set());
|
|
12
|
+
const [next, setNext] = useState(null), [ready, setReady] = useState(!editing), [loading, setLoading] = useState(false), [busy, setBusy] = useState(false), [error, setError] = useState(''), [query, setQuery] = useState('');
|
|
13
|
+
const generation = useRef(0), attempt = useRef(null);
|
|
14
|
+
const max = supergroup ? groups?.maxSupergroupParticipants ?? 2000 : groups?.maxParticipants ?? 50;
|
|
15
|
+
const membersCount = (editing ? count : 1) + added.size - removed.size;
|
|
16
|
+
async function load(more = false) {
|
|
17
|
+
const version = ++generation.current;
|
|
18
|
+
setLoading(true);
|
|
19
|
+
setError('');
|
|
20
|
+
try {
|
|
21
|
+
const page = await client.groupMembers(more && next ? next : undefined);
|
|
22
|
+
if (version !== generation.current)
|
|
23
|
+
return;
|
|
24
|
+
if (!page.managementAvailable || page.ownerId !== userId)
|
|
25
|
+
throw Error('Only the current group owner can change this group.');
|
|
26
|
+
if (more && page.revision !== revision)
|
|
27
|
+
throw Error('This group changed. Close and reopen it before editing.');
|
|
28
|
+
setOriginal(current => new Set([...(more ? current : []), ...(page.memberIds ?? page.members.map(m => m.userId))]));
|
|
29
|
+
setRevision(page.revision);
|
|
30
|
+
setCount(page.memberCount);
|
|
31
|
+
setNext(page.hasMore ? page.nextCursor : null);
|
|
32
|
+
setReady(true);
|
|
33
|
+
}
|
|
34
|
+
catch (e) {
|
|
35
|
+
if (version === generation.current) {
|
|
36
|
+
setReady(false);
|
|
37
|
+
setError(e instanceof Error ? e.message : 'Group members could not be loaded.');
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
finally {
|
|
41
|
+
if (version === generation.current)
|
|
42
|
+
setLoading(false);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
useEffect(() => { if (editing)
|
|
46
|
+
void load(); return () => { ++generation.current; }; }, [client, conversationId]);
|
|
47
|
+
const people = useMemo(() => [...new Set([...Object.keys(contacts), ...original])].filter(id => id !== userId &&
|
|
48
|
+
(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]);
|
|
49
|
+
const chosen = (id) => !removed.has(id) && (original.has(id) || added.has(id));
|
|
50
|
+
const toggle = (id) => {
|
|
51
|
+
if (!chosen(id) && membersCount >= max) {
|
|
52
|
+
setError(`This group can have up to ${max.toLocaleString()} members.`);
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
if (original.has(id))
|
|
56
|
+
setRemoved(current => { const value = new Set(current); if (value.has(id))
|
|
57
|
+
value.delete(id);
|
|
58
|
+
else
|
|
59
|
+
value.add(id); return value; });
|
|
60
|
+
else
|
|
61
|
+
setAdded(current => { const value = new Set(current); if (value.has(id))
|
|
62
|
+
value.delete(id);
|
|
63
|
+
else
|
|
64
|
+
value.add(id); return value; });
|
|
65
|
+
setError('');
|
|
66
|
+
};
|
|
67
|
+
async function save() {
|
|
68
|
+
if (busy || !ready || loading)
|
|
69
|
+
return;
|
|
70
|
+
setBusy(true);
|
|
71
|
+
setError('');
|
|
72
|
+
const value = { name: name.trim(), supergroup, ...(editing ? { revision, addMembers: [...added].sort(), removeMembers: [...removed].sort() } : { members: [...added].sort() }) };
|
|
73
|
+
const body = JSON.stringify(value);
|
|
74
|
+
if (attempt.current?.body !== body)
|
|
75
|
+
attempt.current = { body, id: client.createForwardId() };
|
|
76
|
+
try {
|
|
77
|
+
if (editing) {
|
|
78
|
+
await client.updateGroup({ ...value, revision, addMembers: [...added].sort(), removeMembers: [...removed].sort(), requestId: attempt.current.id });
|
|
79
|
+
onSaved(conversationId, value.name);
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
if (!inbox)
|
|
83
|
+
throw Error('The chat list is unavailable.');
|
|
84
|
+
const result = await inbox.createGroup({ name: value.name, supergroup, members: [...added].sort(), requestId: attempt.current.id });
|
|
85
|
+
onSaved(result.conversationId, value.name);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
catch (e) {
|
|
89
|
+
setError(e instanceof Error ? e.message : 'The group could not be saved.');
|
|
90
|
+
}
|
|
91
|
+
finally {
|
|
92
|
+
setBusy(false);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
const locked = busy || loading || !ready, canSave = !locked && !!name.trim() && membersCount >= 2 && membersCount <= max;
|
|
96
|
+
return _jsxs(MessagingPopup, { title: editing ? 'Edit group' : 'New group', background: c.background, closeDisabled: busy, onClose: () => { if (!busy)
|
|
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 ? 'Supergroup' : '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 ? 'Supergroup' : 'Group' })] }, String(value))) }), _jsxs(Text, { style: { color: c.secondary, fontSize: 14 }, children: [membersCount.toLocaleString(), " / ", max.toLocaleString(), " members"] }), _jsx(Text, { style: { color: c.foreground, fontSize: 17 }, children: "You \u00B7 Group owner" }), _jsx(TextInput, { accessibilityLabel: "Search group contacts", placeholder: "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 ?? '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 ?? '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: "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: "Load more members" }) }), !people.length && !loading && _jsx(Text, { style: { color: c.secondary }, children: query ? 'No matching contacts.' : 'Your app has not supplied any contacts to invite.' }), _jsxs(Text, { style: { color: c.secondary, fontSize: 13, lineHeight: 19 }, children: ["Only the group owner can change the name or members. ", groups?.historyForNewMembers === 'all' ? 'New members can see retained group history.' : '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 ? 'Save group' : '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 ? 'Save' : 'Create group' }) })] });
|
|
98
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
/** Supply callbacks only while the host's authenticated calling service allows them. */
|
|
3
|
+
export interface MessagingCallActions {
|
|
4
|
+
voice?: () => void | Promise<void>;
|
|
5
|
+
video?: () => void | Promise<void>;
|
|
6
|
+
/** Contact's callable number. Never used for group calls. */
|
|
7
|
+
phoneNumber?: string;
|
|
8
|
+
phone?: (number: string) => void | Promise<void>;
|
|
9
|
+
voiceLabel?: string;
|
|
10
|
+
videoLabel?: string;
|
|
11
|
+
}
|
|
12
|
+
/** Matches Messenger: video is separate; the phone button chooses internet/carrier. */
|
|
13
|
+
export declare function MessagingHeaderCalls({ actions, name, group, color }: {
|
|
14
|
+
actions?: MessagingCallActions;
|
|
15
|
+
name: string;
|
|
16
|
+
group: boolean;
|
|
17
|
+
color: string;
|
|
18
|
+
}): React.JSX.Element;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useRef, useState } from 'react';
|
|
3
|
+
import { Keyboard, Linking, Modal, Platform, Pressable, SafeAreaView, Text, View } from 'react-native';
|
|
4
|
+
import { MessageIcon } from './icons.js';
|
|
5
|
+
/** Matches Messenger: video is separate; the phone button chooses internet/carrier. */
|
|
6
|
+
export function MessagingHeaderCalls({ actions, name, group, color }) {
|
|
7
|
+
const [visible, setVisible] = useState(false), [error, setError] = useState('');
|
|
8
|
+
const pending = useRef(null), latest = useRef(actions);
|
|
9
|
+
latest.current = actions;
|
|
10
|
+
const phone = !group ? actions?.phoneNumber?.trim() : undefined;
|
|
11
|
+
const validPhone = phone && /^\+?[\d ().-]{3,30}$/.test(phone) ? phone : undefined;
|
|
12
|
+
const invoke = async (kind) => { const current = latest.current; try {
|
|
13
|
+
setError('');
|
|
14
|
+
if (kind === 'phone') {
|
|
15
|
+
if (group || !validPhone)
|
|
16
|
+
return;
|
|
17
|
+
if (current?.phone)
|
|
18
|
+
await current.phone(validPhone);
|
|
19
|
+
else
|
|
20
|
+
await Linking.openURL('tel:' + validPhone.replace(/[^+\d]/g, ''));
|
|
21
|
+
}
|
|
22
|
+
else
|
|
23
|
+
await current?.[kind]?.();
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
setError('The call could not start. Please try again.');
|
|
27
|
+
} };
|
|
28
|
+
const dismissed = () => { const kind = pending.current; pending.current = null; if (kind)
|
|
29
|
+
requestAnimationFrame(() => void invoke(kind)); };
|
|
30
|
+
useEffect(() => { if (!visible && Platform.OS !== 'ios')
|
|
31
|
+
dismissed(); }, [visible]);
|
|
32
|
+
useEffect(() => { if (!visible || Platform.OS !== 'web')
|
|
33
|
+
return; const close = (e) => { if (e.key === 'Escape') {
|
|
34
|
+
e.stopPropagation();
|
|
35
|
+
setVisible(false);
|
|
36
|
+
} }; document.addEventListener('keydown', close, true); return () => document.removeEventListener('keydown', close, true); }, [visible]);
|
|
37
|
+
useEffect(() => { pending.current = null; setVisible(false); setError(''); }, [name, group]);
|
|
38
|
+
const choose = (kind) => { pending.current = kind; setVisible(false); };
|
|
39
|
+
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 }) });
|
|
40
|
+
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 })] })] });
|
|
41
|
+
return _jsxs(_Fragment, { children: [actions?.video && button(actions.videoLabel ?? 'Video call', 'video', () => void invoke('video')), (actions?.voice || validPhone) && button('Call ' + name, 'phone', () => { if (!validPhone) {
|
|
42
|
+
void invoke('voice');
|
|
43
|
+
return;
|
|
44
|
+
} Keyboard.dismiss(); setVisible(true); }), _jsx(Modal, { visible: visible || !!error, transparent: true, animationType: "fade", onDismiss: dismissed, onRequestClose: () => { setVisible(false); setError(''); }, children: _jsxs(SafeAreaView, { style: { flex: 1, justifyContent: 'flex-end' }, children: [_jsx(Pressable, { accessibilityLabel: "Close call options", onPress: () => { setVisible(false); setError(''); }, style: { position: 'absolute', inset: 0, backgroundColor: '#0009' } }), _jsxs(View, { accessibilityViewIsModal: true, style: { width: '100%', maxWidth: 560, alignSelf: 'center', backgroundColor: '#111111', borderRadius: 28, padding: 20, gap: 8 }, children: [_jsxs(View, { style: { flexDirection: 'row', alignItems: 'center', gap: 8 }, children: [_jsxs(Text, { accessibilityRole: "header", style: { flex: 1, color: '#F7F7F7', fontSize: 22, fontWeight: '600' }, children: ["Call ", name] }), _jsx(Pressable, { accessibilityRole: "button", accessibilityLabel: "Close call choices", onPress: () => { setVisible(false); setError(''); }, style: { width: 44, height: 44, alignItems: 'center', justifyContent: 'center' }, children: _jsx(MessageIcon, { name: "close", color: "#A3A3A3" }) })] }), error ? _jsx(Text, { accessibilityRole: "alert", style: { color: '#F7F7F7', fontSize: 16 }, children: error }) : _jsxs(_Fragment, { children: [actions?.voice && row('voice', actions.voiceLabel ?? 'Robono call', 'Wi-Fi or mobile data'), validPhone && row('phone', 'Phone call', validPhone)] })] })] }) })] });
|
|
45
|
+
}
|
package/dist/icons.d.ts
CHANGED
|
@@ -5,3 +5,5 @@ export declare function duration(ms?: number): string;
|
|
|
5
5
|
export declare function fileSize(bytes?: number): string;
|
|
6
6
|
/** Neutral app chrome stays legible for light and dark customer palettes. */
|
|
7
7
|
export declare function chatSurface(background: string): "#1e262b" | "#ffffff";
|
|
8
|
+
/** Messenger shades the rounded input field, while the surrounding tools use the chat background. */
|
|
9
|
+
export declare function composerSurface(background: string): "#ffffff" | "#111111";
|
package/dist/icons.js
CHANGED
|
@@ -8,3 +8,5 @@ export function chatSurface(background) {
|
|
|
8
8
|
const rgb = background.slice(1).match(/../g)?.map(v => parseInt(v, 16)) ?? [255, 255, 255];
|
|
9
9
|
return rgb[0] * 0.299 + rgb[1] * 0.587 + rgb[2] * 0.114 < 128 ? "#1e262b" : "#ffffff";
|
|
10
10
|
}
|
|
11
|
+
/** Messenger shades the rounded input field, while the surrounding tools use the chat background. */
|
|
12
|
+
export function composerSurface(background) { return chatSurface(background) === "#ffffff" ? "#ffffff" : "#111111"; }
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import type { ConversationSummary, MessagingInboxClient, MessagingAppearance } from '@robono/messaging';
|
|
3
|
+
/** Main-screen search covers retained messages beyond the current inbox page. */
|
|
4
|
+
export declare function InboxMessageSearch({ client, query, appearance, name, chats, onSelect }: {
|
|
5
|
+
client: MessagingInboxClient;
|
|
6
|
+
query: string;
|
|
7
|
+
appearance: MessagingAppearance;
|
|
8
|
+
name: (chat: ConversationSummary) => string;
|
|
9
|
+
chats: ConversationSummary[];
|
|
10
|
+
onSelect: (chat: ConversationSummary, messageId?: string) => void;
|
|
11
|
+
}): React.JSX.Element;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useRef, useState } from 'react';
|
|
3
|
+
import { ActivityIndicator, FlatList, Pressable, Text, View } from 'react-native';
|
|
4
|
+
/** Main-screen search covers retained messages beyond the current inbox page. */
|
|
5
|
+
export function InboxMessageSearch({ client, query, appearance, name, chats, onSelect }) {
|
|
6
|
+
const c = appearance.colors, [rows, setRows] = useState([]), [next, setNext] = useState(null), [busy, setBusy] = useState(false), [error, setError] = useState('');
|
|
7
|
+
const controller = useRef(null), generation = useRef(0);
|
|
8
|
+
async function search(more = false) {
|
|
9
|
+
controller.current?.abort();
|
|
10
|
+
const control = new AbortController();
|
|
11
|
+
controller.current = control;
|
|
12
|
+
const version = ++generation.current;
|
|
13
|
+
setBusy(true);
|
|
14
|
+
setError('');
|
|
15
|
+
try {
|
|
16
|
+
const page = await client.search(query, { ...(more && next ? { before: next } : {}), signal: control.signal });
|
|
17
|
+
if (version !== generation.current)
|
|
18
|
+
return;
|
|
19
|
+
setRows(current => more ? [...new Map([...current, ...page.results].map(row => [row.message.conversationId + ':' + row.message.id, row])).values()] : page.results);
|
|
20
|
+
setNext(page.hasMore ? page.nextCursor : null);
|
|
21
|
+
}
|
|
22
|
+
catch (e) {
|
|
23
|
+
if (version === generation.current && !control.signal.aborted) {
|
|
24
|
+
setRows([]);
|
|
25
|
+
setError(e instanceof Error ? e.message : 'Messages could not be searched.');
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
finally {
|
|
29
|
+
if (version === generation.current)
|
|
30
|
+
setBusy(false);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
useEffect(() => {
|
|
34
|
+
setRows([]);
|
|
35
|
+
setNext(null);
|
|
36
|
+
setError('');
|
|
37
|
+
setBusy(true);
|
|
38
|
+
const timer = setTimeout(() => void search(), 300);
|
|
39
|
+
return () => { clearTimeout(timer); ++generation.current; controller.current?.abort(); };
|
|
40
|
+
}, [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: `Message in ${name(item.conversation)}: ${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() })] }), _jsx(Text, { numberOfLines: 3, style: { fontSize: 15, lineHeight: 21, color: c.secondary }, children: item.message.text })] }), ListFooterComponent: _jsxs(View, { children: [busy && _jsx(ActivityIndicator, { accessibilityLabel: "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: "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: "More results" }) })] }) });
|
|
42
|
+
}
|
package/dist/inbox.d.ts
CHANGED
|
@@ -7,10 +7,13 @@ export interface MessagingInboxProps {
|
|
|
7
7
|
name: string;
|
|
8
8
|
avatarUrl?: string;
|
|
9
9
|
}>;
|
|
10
|
+
companyName?: string;
|
|
11
|
+
headerActions?: React.ReactNode;
|
|
12
|
+
searchText?: (conversation: ConversationSummary) => string;
|
|
10
13
|
appearance?: MessagingAppearance;
|
|
11
14
|
active?: boolean;
|
|
12
|
-
onSelect: (conversation: ConversationSummary) => void;
|
|
15
|
+
onSelect: (conversation: ConversationSummary, messageId?: string) => void;
|
|
13
16
|
onDeleted?: (conversationId: string) => Promise<void> | void;
|
|
14
17
|
}
|
|
15
18
|
/** Compact Messenger chat rows: avatar, name, last message, time and unread badge. */
|
|
16
|
-
export declare function MessagingInbox({ client, userId, contacts, appearance, active, onSelect, onDeleted }: MessagingInboxProps): React.JSX.Element;
|
|
19
|
+
export declare function MessagingInbox({ client, userId, contacts, appearance, active, onSelect, onDeleted, companyName, headerActions, searchText }: MessagingInboxProps): React.JSX.Element;
|
package/dist/inbox.js
CHANGED
|
@@ -1,20 +1,22 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { InboxMessageSearch } from './inbox-search.js';
|
|
3
|
+
import { MessageIcon } from './icons.js';
|
|
2
4
|
import { useEffect, useState, useSyncExternalStore } from 'react';
|
|
3
|
-
import { AppState, FlatList, Image,
|
|
5
|
+
import { AppState, FlatList, Image, Platform, Pressable, Text, TextInput, View } from 'react-native';
|
|
4
6
|
import { defaultMessagingSettings } from '@robono/messaging';
|
|
5
|
-
import { MessagingPopup } from './popup.js';
|
|
7
|
+
import { MessagingPopup, MessagingPopupHeaderBoundary, messagingPopupColors } from './popup.js';
|
|
6
8
|
/** Compact Messenger chat rows: avatar, name, last message, time and unread badge. */
|
|
7
|
-
export function MessagingInbox({ client, userId, contacts = {}, appearance, active = true, onSelect, onDeleted }) {
|
|
9
|
+
export function MessagingInbox({ client, userId, contacts = {}, appearance, active = true, onSelect, onDeleted, companyName, headerActions, searchText }) {
|
|
8
10
|
const state = useSyncExternalStore(client.subscribe, client.getSnapshot, client.getSnapshot), design = appearance ?? state.appearance ?? defaultMessagingSettings().appearance, c = design.colors;
|
|
9
11
|
useEffect(() => { const update = (value) => { if (active && (value === null || value === 'active'))
|
|
10
12
|
client.start();
|
|
11
13
|
else
|
|
12
14
|
client.stop(); }; update(AppState.currentState); const sub = AppState.addEventListener('change', update); return () => { sub.remove(); client.stop(); }; }, [client, active]);
|
|
13
|
-
const [
|
|
14
|
-
const
|
|
15
|
+
const [query, setQuery] = useState('');
|
|
16
|
+
const [selected, setSelected] = useState(null), [confirm, setConfirm] = useState(false), [busy, setBusy] = useState(false), [error, setError] = useState('');
|
|
15
17
|
const title = (row) => row.kind === 'group' ? row.name ?? 'Group' : contacts[row.contactId ?? '']?.name ?? row.contactId ?? 'Chat';
|
|
16
|
-
const openOptions = (row
|
|
17
|
-
return; setSelected(row);
|
|
18
|
+
const openOptions = (row) => { if (!state.inboxActionsAvailable)
|
|
19
|
+
return; setSelected(row); setConfirm(false); setError(''); };
|
|
18
20
|
useEffect(() => { if (selected && !state.conversations.some(c => c.id === selected.id) && !busy && !error)
|
|
19
21
|
setSelected(null); }, [state.conversations, busy, selected, error]);
|
|
20
22
|
async function act(remove) { if (!selected || busy)
|
|
@@ -36,13 +38,15 @@ export function MessagingInbox({ client, userId, contacts = {}, appearance, acti
|
|
|
36
38
|
} }
|
|
37
39
|
const preview = (row) => { const m = row.lastMessage; if (!m)
|
|
38
40
|
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; };
|
|
39
|
-
return _jsxs(View, { style: { flex: 1, minHeight: 0, backgroundColor: c.background }, children: [_jsx(Text, { accessibilityRole: "header", style: { color: c.foreground, fontSize: 20, fontWeight: '700',
|
|
41
|
+
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 ? 'Loading chats…' : state.error ? '' : query ? 'No matching chats.' : '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 ? 'Loading…' : 'More chats' }) }) : null, renderItem: ({ item }) => {
|
|
40
42
|
const person = item.contactId ? contacts[item.contactId] : undefined, title = item.kind === 'group' ? item.name ?? 'Group' : person?.name ?? item.contactId ?? 'Chat';
|
|
41
43
|
const date = item.lastMessage ? new Date(item.lastMessage.createdAt) : null;
|
|
42
|
-
return _jsxs(Pressable, { accessibilityRole: "button", accessibilityLabel: `${title}${item.unreadCount ? `, ${item.unreadCount} unread` : ''}`, onPress: () => onSelect(item), onLongPress:
|
|
43
|
-
openOptions(item
|
|
44
|
-
} }), selected &&
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
44
|
+
return _jsxs(Pressable, { accessibilityRole: "button", accessibilityLabel: `${title}${item.unreadCount ? `, ${item.unreadCount} unread` : ''}`, onPress: () => onSelect(item), onLongPress: () => openOptions(item), accessibilityActions: state.inboxActionsAvailable ? [{ name: 'showOptions', label: 'Chat options' }] : [], onAccessibilityAction: e => { if (e.nativeEvent.actionName === 'showOptions')
|
|
45
|
+
openOptions(item); }, ...(Platform.OS === 'web' ? { onContextMenu: (e) => { e.preventDefault(); openOptions(item); } } : {}), style: { flexDirection: 'row', alignItems: 'center', gap: 12, paddingHorizontal: 12, paddingVertical: 10, minHeight: 68 }, 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 ? ' · 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(undefined, { hour: 'numeric', minute: '2-digit' }) : date.toLocaleDateString(undefined, { 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 }) })] })] });
|
|
46
|
+
} }), selected && _jsxs(MessagingPopup, { compact: true, contentWidth: 640, title: confirm ? "Delete chat with " + title(selected) + "?" : title(selected), foreground: c.foreground, closeLabel: confirm ? 'Close delete chat' : 'Close chat options', closeDisabled: busy, background: c.background, onClose: () => { if (!busy) {
|
|
47
|
+
if (confirm)
|
|
48
|
+
setConfirm(false);
|
|
49
|
+
else
|
|
50
|
+
setSelected(null);
|
|
51
|
+
} }, children: [!confirm ? _jsxs(_Fragment, { children: [_jsx(Pressable, { accessibilityRole: "button", disabled: busy, onPress: () => void act(false), style: { minHeight: 44, justifyContent: 'center' }, children: _jsx(Text, { style: { color: messagingPopupColors.foreground, fontSize: 16 }, children: selected.muted ? 'Unmute' : 'Mute' }) }), _jsx(Pressable, { accessibilityRole: "button", disabled: busy, onPress: () => setConfirm(true), style: { minHeight: 44, justifyContent: 'center' }, children: _jsx(Text, { style: { color: messagingPopupColors.foreground, fontSize: 16 }, children: "Delete chat" }) })] }) : _jsx(Text, { style: { color: messagingPopupColors.secondary, marginTop: 8 }, children: "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 }), confirm && _jsxs(View, { style: { flexDirection: 'row', gap: 8, marginTop: 12 }, children: [_jsx(Pressable, { accessibilityRole: "button", disabled: busy, onPress: () => setConfirm(false), 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: "Cancel" }) }), _jsx(Pressable, { accessibilityRole: "button", accessibilityLabel: "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: "Delete" }) })] })] })] });
|
|
48
52
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
import { type MessagingAccount } from './account.js';
|
|
2
|
+
import { type MessagingCallActions } from './header-calls.js';
|
|
3
|
+
export type { MessagingAccount } from './account.js';
|
|
4
|
+
export type { MessagingCallActions } from './header-calls.js';
|
|
5
|
+
import { type TrackingMapProps, type PrivacyLayerProps } from './extras.js';
|
|
6
|
+
export { MessagingPrivacyLayer } from './extras.js';
|
|
7
|
+
export type { TrackingMapProps, PrivacyLayerProps } from './extras.js';
|
|
1
8
|
import { type SocialPlayerProps } from './social.js';
|
|
2
9
|
export type { SocialPlayerProps } from './social.js';
|
|
3
10
|
export { MessagingSearch } from './search.js';
|
|
@@ -11,7 +18,7 @@ export type { VoiceComposerProps } from "./voice-composer.js";
|
|
|
11
18
|
import { type VoiceBubbleProps } from "./media-ui.js";
|
|
12
19
|
export { VoicePlayback, MediaViewer, MediaPlaybackControls, ZoomablePhoto } from "./media-ui.js";
|
|
13
20
|
export type { VoiceBubbleProps } from "./media-ui.js";
|
|
14
|
-
import { type MessageKind, type MessageMedia, type MessagingAppearance, type MessagingClient, type MessagingMessage, type MessagingState, type MessageTransform, type SavedMessage } from "@robono/messaging";
|
|
21
|
+
import { type MessagingExtrasClient, type MessagingExtrasState, type MessageKind, type MessageMedia, type MessagingAppearance, type MessagingClient, type MessagingMessage, type MessagingState, type MessageTransform, type SavedMessage } from "@robono/messaging";
|
|
15
22
|
export interface VoiceAttachmentProps {
|
|
16
23
|
media: MessageMedia;
|
|
17
24
|
appearance: MessagingAppearance;
|
|
@@ -23,8 +30,11 @@ export interface TranslationVoiceProps {
|
|
|
23
30
|
ink: string;
|
|
24
31
|
accent: string;
|
|
25
32
|
showWaveform?: boolean;
|
|
33
|
+
autoPlay?: boolean;
|
|
26
34
|
}
|
|
27
35
|
export interface MessagingComponents {
|
|
36
|
+
TrackingMap?: ComponentType<TrackingMapProps>;
|
|
37
|
+
PrivacyLayer?: ComponentType<PrivacyLayerProps>;
|
|
28
38
|
SocialPlayer?: ComponentType<SocialPlayerProps>;
|
|
29
39
|
TranslationVoice?: ComponentType<TranslationVoiceProps>;
|
|
30
40
|
VoiceAttachment?: ComponentType<VoiceAttachmentProps>;
|
|
@@ -47,6 +57,12 @@ export interface AttachmentChoice {
|
|
|
47
57
|
durationMs?: number;
|
|
48
58
|
}
|
|
49
59
|
export interface MessagingScreenProps {
|
|
60
|
+
companyName?: string;
|
|
61
|
+
account?: MessagingAccount;
|
|
62
|
+
callActions?: MessagingCallActions;
|
|
63
|
+
/** Authenticated weather, tracking, privacy and call-history controller. */
|
|
64
|
+
extras?: MessagingExtrasClient;
|
|
65
|
+
onCallHistory?: (kind: "voice" | "video") => void;
|
|
50
66
|
client: MessagingClient;
|
|
51
67
|
/** Browser adapter supplies the hosting origin; native embeds use Robono's domain. */
|
|
52
68
|
socialOrigin?: string;
|
|
@@ -58,11 +74,13 @@ export interface MessagingScreenProps {
|
|
|
58
74
|
contact: {
|
|
59
75
|
name: string;
|
|
60
76
|
avatarUrl?: string;
|
|
77
|
+
language?: string;
|
|
61
78
|
};
|
|
62
79
|
/** Resolve group member names through your app's authorized contact directory. */
|
|
63
80
|
participants?: Record<string, {
|
|
64
81
|
name: string;
|
|
65
82
|
avatarUrl?: string;
|
|
83
|
+
language?: string;
|
|
66
84
|
}>;
|
|
67
85
|
appearance?: MessagingAppearance;
|
|
68
86
|
components?: MessagingComponents;
|
|
@@ -85,13 +103,14 @@ export interface MessagingScreenProps {
|
|
|
85
103
|
attachmentChoices?: AttachmentChoice[];
|
|
86
104
|
/** Pick, capture or record, then upload to your private store. Return an opaque asset reference. */
|
|
87
105
|
pickAttachment?: (kind: Exclude<MessageKind, "text" | "poll">, options: {
|
|
106
|
+
maxCount?: number;
|
|
88
107
|
maxDurationMs?: number;
|
|
89
108
|
maxFileBytes?: number;
|
|
90
109
|
allowedMimeTypes?: string[];
|
|
91
110
|
source?: "camera" | "call-video";
|
|
92
111
|
attachmentId?: string;
|
|
93
112
|
intent?: "send" | "preview" | "attach";
|
|
94
|
-
}) => Promise<MessageMedia | null>;
|
|
113
|
+
}) => Promise<MessageMedia | MessageMedia[] | null>;
|
|
95
114
|
/** Release local draft resources after removal or submission; keep uploaded assets needed by retries. */
|
|
96
115
|
releaseAttachment?: (media: MessageMedia) => void | Promise<void>;
|
|
97
116
|
/** Upload a locally selected attachment after its outgoing bubble is visible. */
|
|
@@ -108,6 +127,10 @@ export interface MessagingScreenProps {
|
|
|
108
127
|
}
|
|
109
128
|
interface MessagingContext extends MessagingScreenProps {
|
|
110
129
|
state: MessagingState;
|
|
130
|
+
discussionPreview?: boolean;
|
|
131
|
+
discussionRoot?: string;
|
|
132
|
+
openDiscussion: (threadRoot?: string) => void;
|
|
133
|
+
extrasState: MessagingExtrasState | null;
|
|
111
134
|
design: MessagingAppearance;
|
|
112
135
|
reply: MessagingMessage | null;
|
|
113
136
|
editing: MessagingMessage | null;
|
|
@@ -133,12 +156,19 @@ export declare function messagingHeaderColors(design: MessagingAppearance, group
|
|
|
133
156
|
export declare function MessagingHeader({ children }?: {
|
|
134
157
|
children?: ReactNode;
|
|
135
158
|
}): React.JSX.Element;
|
|
136
|
-
export declare function MessagingBubble({ message, pendingStatus }: {
|
|
159
|
+
export declare function MessagingBubble({ message, pendingStatus, previousMessage, nextMessage }: {
|
|
137
160
|
message: MessagingMessage;
|
|
138
161
|
pendingStatus?: 'sending' | 'failed';
|
|
162
|
+
previousMessage?: MessagingMessage;
|
|
163
|
+
nextMessage?: MessagingMessage;
|
|
139
164
|
}): React.JSX.Element;
|
|
140
165
|
export declare function MessagingList(): React.JSX.Element;
|
|
141
166
|
export declare function MessagingComposer(): React.JSX.Element;
|
|
167
|
+
/** Account navigation. Mount in the inbox/app toolbar, never in a contact menu. */
|
|
168
|
+
export declare function MessagingAccountMenu({ onNewGroup }?: {
|
|
169
|
+
onNewGroup?: () => void;
|
|
170
|
+
}): React.JSX.Element;
|
|
171
|
+
export declare function MessagingConversation(): React.JSX.Element;
|
|
142
172
|
/** Embed inside your app's safe area and navigation. No engine, push or global audio side effects. */
|
|
143
173
|
export declare function MessagingScreen(props: MessagingScreenProps): React.JSX.Element;
|
|
144
174
|
export { MessagingInbox, type MessagingInboxProps } from './inbox.js';
|