@robono/messaging-react-native 0.6.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +4 -0
- package/README.md +61 -5
- package/SBOM.spdx.json +411 -171
- package/dist/attachment-menu.d.ts +16 -0
- package/dist/attachment-menu.js +71 -0
- package/dist/expo-connected-calling.d.ts +32 -0
- package/dist/expo-connected-calling.js +41 -0
- package/dist/expo-extras.d.ts +18 -0
- package/dist/expo-extras.js +32 -0
- package/dist/expo-media.js +4 -4
- package/dist/expo-messenger.d.ts +34 -3
- package/dist/expo-messenger.js +26 -9
- package/dist/expo-notifications.d.ts +2 -1
- package/dist/expo-notifications.js +16 -5
- package/dist/expo-screen.js +10 -3
- package/dist/expo-tracking-task.d.ts +1 -0
- package/dist/expo-tracking-task.js +1 -0
- package/dist/extras-lifecycle.js +3 -3
- package/dist/extras.js +24 -4
- package/dist/header-calls.js +3 -2
- package/dist/inbox.js +28 -11
- package/dist/index.d.ts +6 -2
- package/dist/index.js +31 -36
- package/dist/maps-connection.d.ts +12 -0
- package/dist/maps-connection.js +42 -0
- package/dist/media-controller.d.ts +2 -2
- package/dist/media-controller.js +3 -3
- package/dist/message-menu.js +8 -4
- package/dist/popup.d.ts +25 -0
- package/dist/popup.js +71 -2
- package/dist/quick-send.js +3 -3
- package/dist/safety.js +3 -2
- package/dist/use-quote-gesture.d.ts +1 -1
- package/dist/use-quote-gesture.js +9 -3
- package/package.json +13 -4
package/dist/extras.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
-
import { forwardRef, useEffect, useImperativeHandle, useState } from 'react';
|
|
2
|
+
import { forwardRef, useEffect, useImperativeHandle, useRef, useState } from 'react';
|
|
3
3
|
import { ActivityIndicator, Image, Linking, Platform, Pressable, ScrollView, Text, View } from 'react-native';
|
|
4
4
|
import { currentTrackers, trackerFresh, trackerETA, weatherTemperature } from '@robono/messaging';
|
|
5
5
|
import { MessagingPopup, messagingPopupColors } from './popup.js';
|
|
@@ -26,7 +26,8 @@ else
|
|
|
26
26
|
await Linking.openURL(`https://www.google.com/maps/search/?api=1&query=${query}`); }
|
|
27
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
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);
|
|
29
|
+
const [mapHeight, setMapHeight] = useState(230), [pendingShare, setPendingShare] = useState(false);
|
|
30
|
+
const resolution = useRef(null), shareIntent = useRef(false);
|
|
30
31
|
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
32
|
useImperativeHandle(ref, () => ({ open: () => setOpen(true) }));
|
|
32
33
|
useEffect(() => { const timer = setInterval(() => setNow(Date.now()), 1000); return () => clearInterval(timer); }, []);
|
|
@@ -36,14 +37,33 @@ export const MessageExtra = forwardRef(function MessageExtra({ addressBubbleHeig
|
|
|
36
37
|
await fn();
|
|
37
38
|
}
|
|
38
39
|
catch (e) {
|
|
40
|
+
shareIntent.current = false;
|
|
41
|
+
setPendingShare(false);
|
|
39
42
|
setError(e instanceof Error ? e.message : 'Please try again.');
|
|
40
43
|
}
|
|
41
44
|
finally {
|
|
42
45
|
setBusy(false);
|
|
43
46
|
} };
|
|
44
47
|
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' };
|
|
48
|
+
useEffect(() => { if (!open || !address || address.destination || address.candidates?.length || resolution.current === messageId || !extras.getSnapshot().available.tracking)
|
|
49
|
+
return; resolution.current = messageId; void act(async () => { await extras.resolveAddress(messageId); }); }, [open, messageId, address?.destination]);
|
|
50
|
+
const share = async () => { if (mine) {
|
|
51
|
+
await extras.stopTracking(mine.sessionId);
|
|
52
|
+
return;
|
|
53
|
+
} if (!address?.destination) {
|
|
54
|
+
shareIntent.current = true;
|
|
55
|
+
setPendingShare(true);
|
|
56
|
+
const resolved = await extras.resolveAddress(messageId);
|
|
57
|
+
if (!resolved.destination) {
|
|
58
|
+
setOpen(true);
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
} await extras.startTracking(messageId); setPendingShare(false); };
|
|
45
62
|
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(
|
|
63
|
+
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(share); }, 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
64
|
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: () =>
|
|
65
|
+
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: () => { shareIntent.current = false; setPendingShare(false); resolution.current = null; setError(''); setOpen(false); }, background: background, foreground: foreground, children: [!!error && _jsxs(View, { style: { paddingVertical: 8 }, children: [_jsx(Text, { accessibilityRole: "alert", style: text, children: error }), address && !address.destination && _jsx(Pressable, { accessibilityRole: "button", disabled: busy, onPress: () => void act(async () => { await extras.resolveAddress(messageId); }), style: { minHeight: 44, justifyContent: 'center' }, children: _jsx(Text, { style: { color: accent }, children: "Try again" }) })] }), address && _jsxs(View, { style: { flex: 1, minHeight: 0 }, children: [!address.destination && !!address.candidates?.length && _jsxs(View, { style: { gap: 8, paddingVertical: 12 }, children: [_jsx(Text, { style: { ...text, fontSize: 17, fontWeight: '600' }, children: "Choose the destination" }), address.candidates.map((place, index) => _jsx(Pressable, { accessibilityRole: "button", disabled: busy, onPress: () => void act(async () => { await extras.resolveAddress(messageId, index); if (pendingShare && shareIntent.current) {
|
|
66
|
+
await extras.startTracking(messageId);
|
|
67
|
+
setPendingShare(false);
|
|
68
|
+
} }), style: { minHeight: 48, padding: 12, borderRadius: 10, borderWidth: 1, borderColor: accent }, children: _jsx(Text, { style: { ...text, color: accent }, children: place.address }) }, index))] }), _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: busy ? 'Finding the destination…' : address.candidates?.length ? 'Choose the matching destination above.' : 'The map is unavailable. Check Maps setup or try again.' }) }), _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 && !open && _jsx(ExtraPopup, { title: "Message", onClose: () => setError(''), background: background, foreground: foreground, children: _jsx(Text, { accessibilityRole: "alert", style: text, children: error }) })] });
|
|
49
69
|
});
|
package/dist/header-calls.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
import { useEffect, useRef, useState } from 'react';
|
|
3
|
-
import { Keyboard, Linking,
|
|
3
|
+
import { Keyboard, Linking, Platform, Pressable, Text, View } from 'react-native';
|
|
4
4
|
import { MessageIcon } from './icons.js';
|
|
5
|
+
import { MessagingPopup } from './popup.js';
|
|
5
6
|
/** Matches Messenger: video is separate; the phone button chooses internet/carrier. */
|
|
6
7
|
export function MessagingHeaderCalls({ actions, name, group, color }) {
|
|
7
8
|
const [visible, setVisible] = useState(false), [error, setError] = useState('');
|
|
@@ -41,5 +42,5 @@ export function MessagingHeaderCalls({ actions, name, group, color }) {
|
|
|
41
42
|
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
43
|
void invoke('voice');
|
|
43
44
|
return;
|
|
44
|
-
} Keyboard.dismiss(); setVisible(true); }), _jsx(
|
|
45
|
+
} Keyboard.dismiss(); setVisible(true); }), _jsx(MessagingPopup, { visible: visible || !!error, compact: true, title: 'Call ' + name, background: "#111111", onDismiss: dismissed, onClose: () => { pending.current = null; setVisible(false); setError(''); }, children: error ? _jsx(Text, { accessibilityRole: "alert", style: { color: '#F7F7F7', fontSize: 16, paddingTop: 12 }, children: error }) : _jsxs(_Fragment, { children: [actions?.voice && row('voice', actions.voiceLabel ?? 'Internet call', 'Wi-Fi or mobile data'), validPhone && row('phone', 'Phone call', validPhone)] }) })] });
|
|
45
46
|
}
|
package/dist/inbox.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { MessageMenuRow } from './message-menu.js';
|
|
2
3
|
import { InboxMessageSearch } from './inbox-search.js';
|
|
3
4
|
import { MessageIcon } from './icons.js';
|
|
4
|
-
import { useEffect, useState, useSyncExternalStore } from 'react';
|
|
5
|
-
import { AppState, FlatList, Image, Platform, Pressable, Text, TextInput, View } from 'react-native';
|
|
5
|
+
import { useEffect, useRef, useState, useSyncExternalStore } from 'react';
|
|
6
|
+
import { AppState, FlatList, Image, Platform, Pressable, ScrollView, Text, TextInput, View } from 'react-native';
|
|
6
7
|
import { defaultMessagingSettings } from '@robono/messaging';
|
|
7
|
-
import { MessagingPopup, MessagingPopupHeaderBoundary, messagingPopupColors } from './popup.js';
|
|
8
|
+
import { MessagingPopup, MessagingMenuSurface, MessagingPopupHeaderBoundary, messagingPopupColors, messagingPopupBackdrop } from './popup.js';
|
|
8
9
|
/** Compact Messenger chat rows: avatar, name, last message, time and unread badge. */
|
|
9
10
|
export function MessagingInbox({ client, userId, contacts = {}, appearance, active = true, onSelect, onDeleted, companyName, headerActions, searchText }) {
|
|
10
11
|
const state = useSyncExternalStore(client.subscribe, client.getSnapshot, client.getSnapshot), design = appearance ?? state.appearance ?? defaultMessagingSettings().appearance, c = design.colors;
|
|
@@ -15,8 +16,21 @@ export function MessagingInbox({ client, userId, contacts = {}, appearance, acti
|
|
|
15
16
|
const [query, setQuery] = useState('');
|
|
16
17
|
const [selected, setSelected] = useState(null), [confirm, setConfirm] = useState(false), [busy, setBusy] = useState(false), [error, setError] = useState('');
|
|
17
18
|
const title = (row) => row.kind === 'group' ? row.name ?? 'Group' : contacts[row.contactId ?? '']?.name ?? row.contactId ?? 'Chat';
|
|
19
|
+
const rows = useRef(new Map()), pending = useRef(null);
|
|
20
|
+
const [menuVisible, setMenuVisible] = useState(false), [confirmVisible, setConfirmVisible] = useState(false), [anchor, setAnchor] = useState({ x: 12, y: 88 });
|
|
18
21
|
const openOptions = (row) => { if (!state.inboxActionsAvailable)
|
|
19
|
-
return; setSelected(row); setConfirm(false); setError(''); };
|
|
22
|
+
return; rows.current.get(row.id)?.measureInWindow((x, y, width, height) => { setAnchor({ x: x + width - 218, y: y + height / 2 }); setSelected(row); setConfirm(false); setMenuVisible(true); setError(''); }); };
|
|
23
|
+
const showConfirmation = () => { pending.current = 'confirm'; setMenuVisible(false); };
|
|
24
|
+
const returnToMenu = () => { if (busy)
|
|
25
|
+
return; pending.current = 'menu'; setConfirmVisible(false); };
|
|
26
|
+
const dismissed = () => { const next = pending.current; pending.current = null; if (next === 'confirm') {
|
|
27
|
+
setConfirm(true);
|
|
28
|
+
setConfirmVisible(true);
|
|
29
|
+
}
|
|
30
|
+
else if (next === 'menu') {
|
|
31
|
+
setConfirm(false);
|
|
32
|
+
setMenuVisible(true);
|
|
33
|
+
} };
|
|
20
34
|
useEffect(() => { if (selected && !state.conversations.some(c => c.id === selected.id) && !busy && !error)
|
|
21
35
|
setSelected(null); }, [state.conversations, busy, selected, error]);
|
|
22
36
|
async function act(remove) { if (!selected || busy)
|
|
@@ -41,12 +55,15 @@ export function MessagingInbox({ client, userId, contacts = {}, appearance, acti
|
|
|
41
55
|
return _jsxs(View, { style: { flex: 1, minHeight: 0, backgroundColor: c.background }, children: [_jsx(MessagingPopupHeaderBoundary, { children: _jsxs(View, { testID: "messaging-inbox-header", style: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', minHeight: 64, paddingHorizontal: 16, paddingVertical: 8, gap: 8 }, children: [_jsx(Text, { accessibilityRole: "header", numberOfLines: 1, style: { flex: 1, color: c.foreground, fontSize: 20, fontWeight: '700' }, children: companyName ?? design.name }), headerActions] }) }), _jsxs(View, { style: { margin: 12, marginTop: 8, flexDirection: 'row', alignItems: 'center', gap: 8, borderRadius: 10, paddingHorizontal: 10, backgroundColor: c.foreground + '0b' }, children: [_jsx(MessageIcon, { name: "search", size: 20, color: c.secondary }), _jsx(TextInput, { accessibilityLabel: "Search chats", placeholder: state.globalSearchAvailable ? "Search chats and messages" : "Search chats", placeholderTextColor: c.secondary, value: query, onChangeText: setQuery, maxLength: 200, autoCapitalize: "none", autoCorrect: false, style: { flex: 1, minHeight: 44, color: c.foreground, fontSize: 16 } }), !!query && _jsx(Pressable, { accessibilityRole: "button", accessibilityLabel: "Clear chat search", onPress: () => setQuery(''), style: { width: 44, height: 44, alignItems: 'center', justifyContent: 'center' }, children: _jsx(MessageIcon, { name: "close", size: 20, color: c.secondary }) })] }), state.error && _jsxs(View, { style: { padding: 12 }, children: [_jsx(Text, { accessibilityRole: "alert", style: { color: c.foreground }, children: state.error }), _jsx(Pressable, { accessibilityRole: "button", onPress: () => void client.refresh().catch(() => { }), children: _jsx(Text, { style: { color: c.accent, paddingVertical: 8 }, children: "Try again" }) })] }), query.trim() && state.globalSearchAvailable ? _jsx(InboxMessageSearch, { client: client, query: query.trim(), appearance: design, name: title, chats: state.conversations.filter(row => title(row).toLocaleLowerCase().includes(query.trim().toLocaleLowerCase())), onSelect: onSelect }, userId) : _jsx(FlatList, { data: state.conversations.filter(row => !query.trim() || [title(row), row.lastMessage?.text ?? '', searchText?.(row) ?? ''].join(' ').toLocaleLowerCase().includes(query.trim().toLocaleLowerCase())), keyExtractor: row => row.id, contentContainerStyle: { flexGrow: 1 }, ItemSeparatorComponent: () => _jsx(View, { style: { height: 1, backgroundColor: c.foreground + '12', marginLeft: 64 } }), ListEmptyComponent: _jsx(Text, { style: { padding: 20, textAlign: 'center', color: c.secondary }, children: state.loading ? '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 }) => {
|
|
42
56
|
const person = item.contactId ? contacts[item.contactId] : undefined, title = item.kind === 'group' ? item.name ?? 'Group' : person?.name ?? item.contactId ?? 'Chat';
|
|
43
57
|
const date = item.lastMessage ? new Date(item.lastMessage.createdAt) : null;
|
|
44
|
-
return _jsxs(Pressable, {
|
|
58
|
+
return _jsxs(Pressable, { ref: node => { if (node)
|
|
59
|
+
rows.current.set(item.id, node);
|
|
60
|
+
else
|
|
61
|
+
rows.current.delete(item.id); }, 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
62
|
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(
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
63
|
+
} }), selected && _jsxs(_Fragment, { children: [_jsx(MessagingMenuSurface, { visible: menuVisible, onDismiss: dismissed, onClose: () => { if (!busy)
|
|
64
|
+
setSelected(null); }, children: rect => {
|
|
65
|
+
const width = Math.min(218, rect.width - 24), top = Math.max(12, Math.min(anchor.y - rect.top, rect.height - 124));
|
|
66
|
+
return _jsxs(View, { style: { flex: 1 }, children: [_jsx(Pressable, { accessibilityLabel: "Close chat options", onPress: () => { if (!busy)
|
|
67
|
+
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 ? 'Unmute' : 'Mute', disabled: busy, onPress: () => void act(false) }), _jsx(MessageMenuRow, { icon: "trash-outline", label: "Delete chat", danger: true, divider: true, disabled: busy, onPress: showConfirmation }), !!error && _jsx(Text, { accessibilityRole: "alert", style: { color: '#ffffff', padding: 12 }, children: error })] }) })] });
|
|
68
|
+
} }), confirm && _jsxs(MessagingPopup, { visible: confirmVisible, onDismiss: dismissed, compact: true, title: "Delete chat with " + title(selected) + "?", foreground: c.foreground, closeLabel: "Close delete chat", closeDisabled: busy, background: c.background, onClose: returnToMenu, children: [_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 }), _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: "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" }) })] })] })] })] });
|
|
52
69
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -57,6 +57,8 @@ export interface AttachmentChoice {
|
|
|
57
57
|
durationMs?: number;
|
|
58
58
|
}
|
|
59
59
|
export interface MessagingScreenProps {
|
|
60
|
+
/** Optional tactile feedback at the quote threshold; supplied by the native screen. */
|
|
61
|
+
onQuoteReady?: () => void;
|
|
60
62
|
companyName?: string;
|
|
61
63
|
account?: MessagingAccount;
|
|
62
64
|
callActions?: MessagingCallActions;
|
|
@@ -99,10 +101,11 @@ export interface MessagingScreenProps {
|
|
|
99
101
|
};
|
|
100
102
|
/** Native clipboard adapter. Supplied by ExpoMessagingScreen. Web uses its clipboard. */
|
|
101
103
|
copyText?: (text: string) => Promise<void>;
|
|
102
|
-
/** Optional
|
|
104
|
+
/** Optional metadata used to resolve caller-owned draft thumbnails. The supplied menu opens the device library. */
|
|
103
105
|
attachmentChoices?: AttachmentChoice[];
|
|
104
106
|
/** Pick, capture or record, then upload to your private store. Return an opaque asset reference. */
|
|
105
107
|
pickAttachment?: (kind: Exclude<MessageKind, "text" | "poll">, options: {
|
|
108
|
+
mixedLibrary?: boolean;
|
|
106
109
|
maxCount?: number;
|
|
107
110
|
maxDurationMs?: number;
|
|
108
111
|
maxFileBytes?: number;
|
|
@@ -165,8 +168,9 @@ export declare function MessagingBubble({ message, pendingStatus, previousMessag
|
|
|
165
168
|
export declare function MessagingList(): React.JSX.Element;
|
|
166
169
|
export declare function MessagingComposer(): React.JSX.Element;
|
|
167
170
|
/** Account navigation. Mount in the inbox/app toolbar, never in a contact menu. */
|
|
168
|
-
export declare function MessagingAccountMenu({ onNewGroup }?: {
|
|
171
|
+
export declare function MessagingAccountMenu({ onNewGroup, onMaps }?: {
|
|
169
172
|
onNewGroup?: () => void;
|
|
173
|
+
onMaps?: () => void;
|
|
170
174
|
}): React.JSX.Element;
|
|
171
175
|
export declare function MessagingConversation(): React.JSX.Element;
|
|
172
176
|
/** Embed inside your app's safe area and navigation. No engine, push or global audio side effects. */
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { MessagingMenuSurface } from './popup.js';
|
|
2
3
|
import { ScheduleTimePicker, ScheduledMessages } from './scheduled.js';
|
|
3
4
|
import { MessagingGroupEditor } from './group-editor.js';
|
|
4
5
|
import { ConversationDetails } from './conversation-details.js';
|
|
@@ -8,6 +9,7 @@ import { retainExtrasLifecycle } from './extras-lifecycle.js';
|
|
|
8
9
|
import { isBigEmojiText } from './emoji-text.js';
|
|
9
10
|
import { MessageMenu, MessageMenuRow, messageMenuPosition } from './message-menu.js';
|
|
10
11
|
import { TranslationSettings } from './translation-settings.js';
|
|
12
|
+
import { AttachmentMenu } from './attachment-menu.js';
|
|
11
13
|
import { QuickSendButton } from './quick-send.js';
|
|
12
14
|
import { MessageExtra, MessagingPrivacyLayer, privacyReadingStripHeight, openChatAddress, ExtraPopup } from './extras.js';
|
|
13
15
|
export { MessagingPrivacyLayer } from './extras.js';
|
|
@@ -24,7 +26,7 @@ export { MessagingSafety } from './safety.js';
|
|
|
24
26
|
import { PollMessage, PollComposer } from "./polls.js";
|
|
25
27
|
export { PollMessage, PollComposer } from "./polls.js";
|
|
26
28
|
import { createContext, useContext, useEffect, useRef, useState, useSyncExternalStore, } from "react";
|
|
27
|
-
import { Animated, ActivityIndicator, AppState, BackHandler, FlatList, Image, KeyboardAvoidingView, Keyboard, Linking,
|
|
29
|
+
import { Animated, ActivityIndicator, AppState, BackHandler, FlatList, Image, KeyboardAvoidingView, Keyboard, Linking, Platform, Pressable, ScrollView, Text, TextInput, View, useWindowDimensions, } from "react-native";
|
|
28
30
|
import { useQuoteGesture, messageInfoRevealWidth, messageReplyRevealWidth } from "./use-quote-gesture.js";
|
|
29
31
|
import { MessageIcon, duration, fileSize, chatSurface, composerSurface } from "./icons.js";
|
|
30
32
|
import { VoiceMessageComposer } from "./voice-composer.js";
|
|
@@ -321,8 +323,10 @@ function MediaContent({ message, ink, footer, onOpen, onLongPress }) {
|
|
|
321
323
|
return _jsxs(View, { style: { flexDirection: 'row', alignItems: 'center', gap: 12, width: 220, minHeight: 56, padding: 8, borderRadius: 6, backgroundColor: ink + '09' }, children: [_jsx(MessageIcon, { name: "document", color: design.colors.accent, size: 30 }), _jsxs(View, { style: { flex: 1, gap: 4 }, children: [_jsx(Text, { numberOfLines: 2, style: { color: ink, fontSize: 14, fontWeight: '600' }, children: message.media?.fileName ?? 'File' }), _jsxs(Text, { style: { color: ink, fontSize: 12, opacity: 0.75 }, children: [message.media?.fileName?.split('.').pop()?.toUpperCase(), " \u00B7 ", fileSize(message.media?.byteSize)] })] })] });
|
|
322
324
|
}
|
|
323
325
|
function messageTimeLabel(message) { return (message.editedAt && !message.deleted ? 'edited · ' : '') + new Date(message.createdAt).toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' }); }
|
|
324
|
-
function MessageMetadata({
|
|
325
|
-
|
|
326
|
+
function MessageMetadata({ mine, ink, pendingStatus }) {
|
|
327
|
+
if (!mine || !pendingStatus)
|
|
328
|
+
return null;
|
|
329
|
+
return _jsx(View, { accessibilityLabel: pendingStatus === 'sending' ? 'Sending message' : 'Message not sent', style: { alignSelf: 'flex-end' }, children: _jsx(MessageIcon, { name: pendingStatus === 'sending' ? 'clock' : 'warning', color: pendingStatus === 'failed' ? '#c94040' : ink, size: 15 }) });
|
|
326
330
|
}
|
|
327
331
|
export function MessagingBubble({ message, pendingStatus, previousMessage, nextMessage }) {
|
|
328
332
|
const { userId, design, state, client, participants, openDiscussion, discussionPreview, discussionRoot, setReply, setEditing, setForwarding, setDeleting, selection, setSelection, saved, saving, toggleSaved, onMessageOptions, openAttachment, language, contact, copyText, components, extras, extrasState, onCallHistory } = useMessagingUI();
|
|
@@ -345,7 +349,6 @@ export function MessagingBubble({ message, pendingStatus, previousMessage, nextM
|
|
|
345
349
|
const canChange = !extra?.weather && !extra?.call && mine && !pendingStatus && !message.deleted && !!state.messageActionsAvailable && !!state.settings?.enabled && state.conversation?.canSend !== false && !message.readAt;
|
|
346
350
|
const anchor = useRef(null), viewport = useWindowDimensions();
|
|
347
351
|
const [menuPosition, setMenuPosition] = useState({ x: 12, y: 12, above: false, available: 400 }), [hovered, setHovered] = useState(false);
|
|
348
|
-
const [footerWidth, setFooterWidth] = useState(0);
|
|
349
352
|
const [addressBubbleHeight, setAddressBubbleHeight] = useState(44);
|
|
350
353
|
const openMenu = () => anchor.current?.measureInWindow((x, y, width, height) => {
|
|
351
354
|
setMenuPosition(messageMenuPosition(x, y, width, height, viewport.width, viewport.height, mine));
|
|
@@ -384,7 +387,7 @@ export function MessagingBubble({ message, pendingStatus, previousMessage, nextM
|
|
|
384
387
|
} }
|
|
385
388
|
const canInteract = !pendingStatus && !!state.settings?.enabled && state.conversation?.canSend !== false && !message.deleted;
|
|
386
389
|
const quoting = !!state.settings?.features.replies && canInteract;
|
|
387
|
-
const quoteGesture = useQuoteGesture(!pendingStatus && !selection.length, mine, quoting ? () => setReply(message) : undefined, extra ? () => extraRef.current?.open() : undefined);
|
|
390
|
+
const quoteGesture = useQuoteGesture(!pendingStatus && !selection.length, mine, quoting ? () => setReply(message) : undefined, extra ? () => extraRef.current?.open() : undefined, useMessagingUI().onQuoteReady);
|
|
388
391
|
async function open() { if (pendingStatus || quoteGesture.suppressPress())
|
|
389
392
|
return; if (!openAttachment) {
|
|
390
393
|
setError('This attachment cannot be opened.');
|
|
@@ -429,7 +432,7 @@ export function MessagingBubble({ message, pendingStatus, previousMessage, nextM
|
|
|
429
432
|
const bigEmoji = message.kind === 'text' && !message.replyTo && !extra && !canTranslate && !transformed && isBigEmojiText(message.text);
|
|
430
433
|
const showingTransform = !!transformed && !showOriginal && (transformed.kind === 'transcript' || canTranslate);
|
|
431
434
|
const languageActions = (canTranscript || canTranslate || transformed) && _jsxs(View, { testID: "message-language-actions", style: { flexDirection: 'row', flexWrap: 'wrap', justifyContent: voice && !showingTransform ? 'flex-end' : 'flex-start', columnGap: 8, rowGap: 0 }, children: [transformed && _jsx(Pressable, { accessibilityRole: "button", accessibilityLabel: showOriginal ? (transformed.kind === 'transcript' ? 'Show transcript' : 'Show translation') : 'Show original', onPress: () => { setTranslationAutoPlay(false); setShowOriginal(v => !v); }, hitSlop: 8, style: { minHeight: 17, paddingVertical: 1, justifyContent: 'center' }, children: _jsx(Text, { style: { color: c.accent, fontSize: 11, lineHeight: 15 }, children: showOriginal ? (transformed.kind === 'transcript' ? 'Show transcript' : 'Show translation') : 'Show original' }) }), canTranscript && !transformed && _jsx(Pressable, { accessibilityRole: "button", accessibilityLabel: "Read voice message", disabled: transforming, onPress: () => void transform('transcript'), hitSlop: 8, style: { minHeight: 17, paddingVertical: 1, justifyContent: 'center' }, children: _jsx(Text, { style: { color: c.accent, fontSize: 11, lineHeight: 15 }, children: transforming ? 'Transcribing…' : 'Transcript' }) }), canTranslate && transformed?.kind !== 'translation' && _jsx(Pressable, { accessibilityRole: "button", accessibilityLabel: translation.mode === 'voice' ? 'Play translation' : 'Translate message', disabled: transforming, onPress: () => void transform('translation', translation.mode === 'voice' ? 'voice' : 'text'), hitSlop: 8, style: { minHeight: 17, paddingVertical: 1, justifyContent: 'center' }, children: _jsx(Text, { style: { color: c.accent, fontSize: 11, lineHeight: 15 }, children: translation.mode === 'voice' ? 'Play Translation' : 'Translate' }) })] });
|
|
432
|
-
const originalContent = extra?.weather || extra?.call ? extraCard : message.kind === 'poll' ? _jsx(PollMessage, { onLongPress: menu, message: message, userId: userId, ink: ink, accent: c.accent, background: c.background, foreground: c.foreground, resolveVoterName: id => id === userId ? 'You' : participants?.[id]?.name ?? (state.conversation?.kind === 'direct' ? contextContactName : 'Chat member'), enabled: canInteract && !!state.settings?.features.poll, onVote: options => client.vote(message.id, options), onClose: () => client.closePoll(message.id) }) : message.kind === 'text' ? _jsxs(View, { testID: "message-original-text",
|
|
435
|
+
const originalContent = extra?.weather || extra?.call ? extraCard : message.kind === 'poll' ? _jsx(PollMessage, { onLongPress: menu, message: message, userId: userId, ink: ink, accent: c.accent, background: c.background, foreground: c.foreground, resolveVoterName: id => id === userId ? 'You' : participants?.[id]?.name ?? (state.conversation?.kind === 'direct' ? contextContactName : 'Chat member'), enabled: canInteract && !!state.settings?.features.poll, onVote: options => client.vote(message.id, options), onClose: () => client.closePoll(message.id) }) : message.kind === 'text' ? _jsxs(View, { testID: "message-original-text", children: [_jsx(Text, { selectable: true, style: { color: ink, fontSize: bigEmoji ? 48 : 16, lineHeight: bigEmoji ? 58 : 22 }, children: renderText(message.text ?? '') }), pendingStatus && footer] }) : _jsx(MediaContent, { message: message, ink: ink, accent: c.accent, footer: voice ? languageActions : undefined, onOpen: () => void open(), onLongPress: menu });
|
|
433
436
|
const content = showingTransform ? _jsxs(View, { children: [_jsx(Text, { style: { color: ink, fontSize: 11, opacity: .65, marginBottom: 2 }, children: transformed.kind === 'transcript' ? 'Transcript' : 'Translated · ' + supportedLanguageLabel(translationLanguage) }), transformed.audio && TranslationVoice ? _jsx(View, { testID: "message-translated-voice", children: _jsx(TranslationVoice, { transform: transformed, ink: ink, accent: c.accent, showWaveform: design.showWaveform !== false, autoPlay: translationAutoPlay }) }) : _jsx(Text, { testID: "message-language-result", selectable: true, style: { color: ink, fontSize: 16, lineHeight: 22 }, children: transformed.text }), !voice && footer] }) : originalContent;
|
|
434
437
|
const bubbleBody = _jsxs(_Fragment, { children: [!bigEmoji && (!mine || !pendingStatus) && _jsx(View, { testID: mine ? (message.readAt && state.settings?.features.readReceipts ? 'message-status-read' : 'message-status-sent') : 'message-status-incoming', pointerEvents: "none", accessible: false, style: { position: 'absolute', top: joinsBefore ? -5 : 0, bottom: joinsAfter ? -5 : 0, start: 0, end: 0, zIndex: 40, ...(mine ? { borderEndWidth: 2, borderEndColor: message.readAt && state.settings?.features.readReceipts ? c.accent : c.foreground, borderTopEndRadius: joinsBefore ? 0 : voice && !message.text && !showingTransform ? 24 : 8, borderBottomEndRadius: joinsAfter ? 0 : voice && !message.text && !showingTransform ? 24 : 8 } : { borderStartWidth: 2, borderStartColor: '#2B2B2B', borderTopStartRadius: joinsBefore ? 0 : voice && !message.text && !showingTransform ? 24 : 8, borderBottomStartRadius: joinsAfter ? 0 : voice && !message.text && !showingTransform ? 24 : 8 }) } }), message.replyTo && message.replyTo !== discussionRoot && _jsx(Pressable, { accessibilityRole: "button", accessibilityLabel: "Go to quoted message", onLongPress: menu, onPress: () => { void client.focusMessage(message.replyTo).catch(e => setError(e instanceof Error ? e.message : 'This message is unavailable.')); }, style: { borderLeftWidth: 3, borderLeftColor: c.accent, paddingLeft: 9, paddingVertical: 5, marginBottom: 7 }, children: _jsx(QuotedPreview, { message: state.messages.find(m => m.id === message.replyTo) ?? state.quotedMessages?.find(m => m.id === message.replyTo), ink: ink }) }), !mine && !grouped && state.conversation?.kind === 'group' && _jsx(Text, { style: { color: ink, fontSize: 12, fontWeight: '600', marginBottom: 5 }, children: message.deleted ? 'Former member' : participants?.[message.senderId]?.name ?? 'Member' }), extra?.address || extra?.weather || extra?.call || voice || message.kind === 'poll' ? content : _jsx(Pressable, { accessibilityRole: message.kind === 'text' ? undefined : 'button', accessibilityLabel: message.kind === 'text' ? undefined : `${mediaNames[message.kind]}: ${message.media?.fileName ?? 'Open attachment'}`, disabled: busy, onLongPress: menu, onPress: message.kind === 'text' ? () => { if (quoteGesture.suppressPress())
|
|
435
438
|
return; if (extra?.address)
|
|
@@ -600,14 +603,9 @@ function AttachmentStrip({ children, label, height, recent = false }) {
|
|
|
600
603
|
}, []);
|
|
601
604
|
return _jsx(ScrollView, { ref: scroll, horizontal: true, accessibilityLabel: label, directionalLockEnabled: true, alwaysBounceVertical: false, showsVerticalScrollIndicator: false, showsHorizontalScrollIndicator: false, style: { height, flexGrow: 0, flexShrink: 0, ...(recent ? { marginTop: 8 } : {}), ...(Platform.OS === 'web' ? { overflowY: 'hidden', touchAction: 'pan-x' } : {}) }, contentContainerStyle: { flexDirection: 'row', alignItems: 'flex-start', gap: 8, paddingHorizontal: recent ? 0 : 8, paddingBottom: recent ? 4 : 2 }, children: children });
|
|
602
605
|
}
|
|
603
|
-
function WeatherComposerAction({ onClose }) {
|
|
604
|
-
const { extras, client, design } = useMessagingUI();
|
|
605
|
-
const [busy, setBusy] = useState(false), [error, setError] = useState('');
|
|
606
|
-
return _jsxs(_Fragment, { children: [_jsxs(Pressable, { accessibilityRole: "button", accessibilityLabel: "Share weather", disabled: busy, onPress: () => { setBusy(true); void extras?.sendWeather().then(() => client.refresh()).then(onClose).catch(e => setError(e instanceof Error ? e.message : 'Weather could not be sent.')).finally(() => setBusy(false)); }, style: { flex: 1, minWidth: 0, minHeight: 84, alignItems: 'center', gap: 3 }, children: [_jsx(View, { style: { width: 56, height: 56, borderRadius: 28, backgroundColor: '#db7c2412', alignItems: 'center', justifyContent: 'center' }, children: busy ? _jsx(ActivityIndicator, { color: design.colors.accent }) : _jsx(MessageIcon, { name: "weather", color: design.colors.accent, size: 28 }) }), _jsx(Text, { style: { color: design.colors.foreground, fontSize: 13 }, children: "Weather" })] }), !!error && _jsx(ExtraPopup, { title: "Weather", background: design.colors.background, foreground: design.colors.foreground, onClose: () => setError(''), children: _jsx(Text, { style: { color: design.colors.foreground }, children: error }) })] });
|
|
607
|
-
}
|
|
608
606
|
export function MessagingComposer() {
|
|
609
607
|
const weatherAllowed = !!useMessagingUI().extrasState?.available.weather;
|
|
610
|
-
const { state, client, design, pickAttachment, attachmentChoices, attachmentPreview, releaseAttachment, prepareAttachment, retainDraftAttachments, reply, setReply, editing, setEditing, components, openAttachment, userId, participants } = useMessagingUI();
|
|
608
|
+
const { state, client, design, extras, pickAttachment, attachmentChoices, attachmentPreview, releaseAttachment, prepareAttachment, retainDraftAttachments, reply, setReply, editing, setEditing, components, openAttachment, userId, participants } = useMessagingUI();
|
|
611
609
|
const VoiceComposer = components?.VoiceComposer ?? VoiceMessageComposer;
|
|
612
610
|
const [savedDraft, setDraftState] = useState(state.draft ?? ""), [busy, setBusy] = useState(false), [error, setError] = useState("");
|
|
613
611
|
const [editDraft, setEditDraft] = useState('');
|
|
@@ -692,11 +690,7 @@ export function MessagingComposer() {
|
|
|
692
690
|
const [pollOpen, setPollOpen] = useState(false);
|
|
693
691
|
useEffect(() => setPollOpen(false), [state.conversationId]);
|
|
694
692
|
const [attachmentsOpen, setAttachmentsOpen] = useState(false);
|
|
695
|
-
const
|
|
696
|
-
useEffect(() => { if (!attachmentsOpen)
|
|
697
|
-
setAttachmentFilter(null); }, [attachmentsOpen]);
|
|
698
|
-
useEffect(() => { if (attachmentFilter && !state.settings?.features[attachmentFilter])
|
|
699
|
-
setAttachmentFilter(null); }, [attachmentFilter, state.settings]);
|
|
693
|
+
const attachmentButton = useRef(null);
|
|
700
694
|
const [inputHeight, setInputHeight] = useState(44);
|
|
701
695
|
useEffect(() => { if (!draft)
|
|
702
696
|
setInputHeight(42); }, [draft]);
|
|
@@ -831,7 +825,7 @@ export function MessagingComposer() {
|
|
|
831
825
|
setBusy(false);
|
|
832
826
|
}
|
|
833
827
|
}
|
|
834
|
-
async function pick(kind, source, attachmentId, intent = "send") {
|
|
828
|
+
async function pick(kind, source, attachmentId, intent = "send", mixedLibrary = false) {
|
|
835
829
|
if (!editing && selectedRef.current.length >= 10) {
|
|
836
830
|
setError('Choose up to 10 attachments per message.');
|
|
837
831
|
return;
|
|
@@ -862,7 +856,7 @@ export function MessagingComposer() {
|
|
|
862
856
|
try {
|
|
863
857
|
const result = await pickAttachment(kind, kind === "voice"
|
|
864
858
|
? { maxDurationMs: (state.settings?.voice?.maxDurationSeconds ?? 60) * 1000, intent: 'attach' }
|
|
865
|
-
: { maxCount: editing || source || attachmentId || intent === 'preview' ? 1 : 10 - selectedRef.current.length, ...(source ? { source } : {}), ...(attachmentId ? { attachmentId } : {}), ...(!source ? { intent } : {}), ...(state.settings?.attachments ?? defaultAttachmentLimits())[kind],
|
|
859
|
+
: { mixedLibrary, maxCount: editing || source || attachmentId || intent === 'preview' ? 1 : 10 - selectedRef.current.length, ...(source ? { source } : {}), ...(attachmentId ? { attachmentId } : {}), ...(!source ? { intent } : {}), ...(state.settings?.attachments ?? defaultAttachmentLimits())[kind],
|
|
866
860
|
...(kind === "video" ? { maxDurationMs: (state.settings?.attachments ?? defaultAttachmentLimits()).video.maxDurationSeconds * 1000 } : {}) });
|
|
867
861
|
const picked = result ? (Array.isArray(result) ? result : [result]) : [];
|
|
868
862
|
const current = client.getSnapshot(), limit = editId ? 1 : Math.max(0, 10 - selectedRef.current.length);
|
|
@@ -876,7 +870,7 @@ export function MessagingComposer() {
|
|
|
876
870
|
}
|
|
877
871
|
if (!picked.length)
|
|
878
872
|
return;
|
|
879
|
-
const additions = picked.map(media => ({ id: 'attachment-' + (++selectionSequence.current), kind: source === 'camera' ? (media.mimeType.startsWith('video/') ? 'video' : 'image') : kind, media, thumbnail: attachmentChoices?.find(item => item.id === attachmentId)?.thumbnailUri }));
|
|
873
|
+
const additions = picked.map(media => ({ id: 'attachment-' + (++selectionSequence.current), kind: source === 'camera' || mixedLibrary ? (media.mimeType.startsWith('video/') ? 'video' : 'image') : kind, media, thumbnail: attachmentChoices?.find(item => item.id === attachmentId)?.thumbnailUri }));
|
|
880
874
|
if (additions.some(item => !current.settings.features[item.kind])) {
|
|
881
875
|
picked.forEach(media => void releaseAttachment?.(media));
|
|
882
876
|
return;
|
|
@@ -914,12 +908,11 @@ export function MessagingComposer() {
|
|
|
914
908
|
}
|
|
915
909
|
function renderTextComposer(voice = { hasVoice: false, blocked: false }) {
|
|
916
910
|
const blocked = voice.blocked || busy;
|
|
917
|
-
return _jsxs(View, { style: { flexDirection: 'row', alignItems: 'flex-end', gap: 4 }, children: [_jsxs(View, { testID: "messaging-composer-field", style: { flex: 1, minWidth: 0, minHeight: 44, borderRadius: 22, backgroundColor: composerSurface(c.background), flexDirection: 'row', alignItems: 'flex-end', overflow: 'hidden' }, children: [(camera || library.image || library.video || library.document || !editing && (features?.poll || weatherAllowed)) && _jsx(Pressable, { accessibilityRole: "button", accessibilityLabel: "Attach", accessibilityState: { expanded: attachmentsOpen }, disabled: busy || blocked, onPress: () => { if (attachmentsOpen) {
|
|
911
|
+
return _jsxs(View, { style: { flexDirection: 'row', alignItems: 'flex-end', gap: 4 }, children: [_jsxs(View, { testID: "messaging-composer-field", style: { flex: 1, minWidth: 0, minHeight: 44, borderRadius: 22, backgroundColor: composerSurface(c.background), flexDirection: 'row', alignItems: 'flex-end', overflow: 'hidden' }, children: [(camera || library.image || library.video || library.document || !editing && (features?.poll || weatherAllowed)) && _jsx(Pressable, { ref: attachmentButton, accessibilityRole: "button", accessibilityLabel: "Attach", accessibilityState: { expanded: attachmentsOpen }, disabled: busy || blocked, onPress: () => { if (attachmentsOpen) {
|
|
918
912
|
setAttachmentsOpen(false);
|
|
919
913
|
input.current?.focus();
|
|
920
914
|
}
|
|
921
915
|
else {
|
|
922
|
-
Keyboard.dismiss();
|
|
923
916
|
setAttachmentsOpen(true);
|
|
924
917
|
} }, style: { width: 42, height: 44, flexShrink: 0, alignItems: 'center', justifyContent: 'center' }, children: _jsx(MessageIcon, { name: attachmentsOpen ? "keyboard" : "plus", color: c.accent, size: 25 }) }), features?.text && _jsx(TextInput, { ref: input, accessibilityLabel: design.labels.compose, placeholder: design.labels.compose, placeholderTextColor: c.secondary, value: draft, editable: !blocked, onChangeText: setDraft, onContentSizeChange: e => setInputHeight(Math.max(44, Math.min(120, e.nativeEvent.contentSize.height))), multiline: true, numberOfLines: 1, maxLength: 10000, ...(Platform.OS === 'web' ? { onKeyPress: (event) => { const native = event.nativeEvent; if (native.key === 'Enter' && !native.shiftKey && !native.isComposing && native.keyCode !== 229) {
|
|
925
918
|
event.preventDefault();
|
|
@@ -930,29 +923,31 @@ export function MessagingComposer() {
|
|
|
930
923
|
else
|
|
931
924
|
void send(); }, onQuickSend: emoji => { void submit({ kind: 'text', text: emoji }).catch(() => { }); } }, state.conversationId)] });
|
|
932
925
|
}
|
|
926
|
+
const attachmentActions = [];
|
|
927
|
+
if (library.image || library.video)
|
|
928
|
+
attachmentActions.push({ key: 'library', label: library.image && library.video ? 'Photos & Videos' : library.image ? 'Photos' : 'Videos', icon: 'image', run: () => { void pick(library.image ? 'image' : 'video', undefined, undefined, 'send', true); } });
|
|
929
|
+
if (camera)
|
|
930
|
+
attachmentActions.push({ key: 'camera', label: 'Camera', icon: 'camera', run: () => { void pick(features?.image ? 'image' : 'video', 'camera'); } });
|
|
931
|
+
if (library.document)
|
|
932
|
+
attachmentActions.push({ key: 'files', label: 'Files', icon: 'document', run: () => { void pick('document'); } });
|
|
933
|
+
if (!editing && features?.poll)
|
|
934
|
+
attachmentActions.push({ key: 'poll', label: state.conversation?.kind === 'group' ? 'Poll' : 'Yes/No question', icon: 'poll', run: () => setPollOpen(true) });
|
|
935
|
+
if (!editing && weatherAllowed)
|
|
936
|
+
attachmentActions.push({ key: 'weather', label: 'Weather', icon: 'weather', run: () => { setBusy(true); setError(''); void extras?.sendWeather().then(() => client.refresh()).catch(e => setError(e instanceof Error ? e.message : 'Weather could not be sent.')).finally(() => setBusy(false)); } });
|
|
933
937
|
if (!enabled || state.conversation?.canSend === false) {
|
|
934
938
|
return (_jsx(Text, { style: { padding: 16, color: c.secondary }, children: state.offline ? 'Offline · Recent messages only. Connect to send and load more.' : state.conversation?.safety?.blockedByMe ? 'You blocked this contact. Use contact options to unblock.' : state.conversation?.canSend === false ? (state.conversation.kind === 'group' ? 'Only members allowed to send can post in this group.' : 'Messaging is unavailable for this conversation.') : design.labels.unavailable }));
|
|
935
939
|
}
|
|
936
|
-
return (_jsxs(View, { testID: "messaging-composer", style: { paddingStart: 8, paddingEnd: 0, paddingVertical: 4, gap: 8, backgroundColor: c.background }, children: [mentionSuggestions.length > 0 && _jsx(ScrollView, { keyboardShouldPersistTaps: "handled", style: { maxHeight: 144 }, children: mentionSuggestions.map(id => _jsx(Pressable, { accessibilityRole: "button", accessibilityLabel: 'Mention ' + participants[id].name, onPress: () => chooseMention(id), style: { minHeight: 44, paddingHorizontal: 12, justifyContent: 'center', borderBottomWidth: .5, borderBottomColor: c.secondary + '30' }, children: _jsxs(Text, { style: { color: c.accent, fontSize: 16 }, children: ["@", participants[id].name] }) }, id)) }), mentionQuery !== undefined && !!mentionError && _jsx(Text, { style: { color: c.secondary, fontSize: 13 }, children: mentionError }), editing && _jsxs(View, { style: { flexDirection: 'row', alignItems: 'center', gap: 8 }, children: [_jsxs(Text, { numberOfLines: 1, style: { flex: 1, color: c.secondary }, children: ["Editing: ", editing.text || editing.media?.fileName] }), _jsx(Pressable, { accessibilityRole: "button", accessibilityLabel: "Cancel editing", disabled: busy, onPress: () => setEditing(null), style: { minHeight: 36, justifyContent: 'center' }, children: _jsx(Text, { style: { color: c.accent }, children: "Cancel" }) })] }), !editing && reply && (_jsxs(View, { testID: "composer-quote", style: { flexDirection: "row", gap: 8 }, children: [_jsx(View, { style: { flex: 1 }, children: _jsx(QuotedPreview, { message: reply, ink: c.secondary }) }), _jsx(Pressable, { accessibilityRole: "button", accessibilityLabel: "Cancel quote", onPress: () => setReply(null), children: _jsx(Text, { style: { color: c.foreground }, children: "Cancel" }) })] })), state.settings?.history?.mode === 'last_two' && _jsx(Text, { style: { color: c.secondary, fontSize: 11, textAlign: 'center' }, children: "Only the latest two messages are kept." }), editing ? _jsxs(_Fragment, { children: [renderAttachments(), renderTextComposer()] }) : features?.voice ? _jsx(VoiceComposer, { appearance: design, maxSeconds: state.settings?.voice?.maxDurationSeconds ?? 60, busy: busy, onRecord: () => { Keyboard.dismiss(); void pick('voice'); }, renderAttachments: renderAttachments, renderTextComposer: features?.text || selected.length ? renderTextComposer : undefined, actions: _jsx(_Fragment, { children: (camera || library.image || library.video || library.document || !editing && (features?.poll || weatherAllowed)) && _jsx(Pressable, { accessibilityRole: "button", accessibilityLabel: "Attach", accessibilityState: { expanded: attachmentsOpen }, disabled: busy, onPress: () => setAttachmentsOpen(!attachmentsOpen), style: { width: 44, height: 44, alignItems: 'center', justifyContent: 'center' }, children: _jsx(MessageIcon, { name: attachmentsOpen ? 'close' : 'plus', color: c.foreground, size: 25 }) }) }) }) : _jsxs(_Fragment, { children: [renderAttachments(), renderTextComposer()] }), attachmentsOpen && (
|
|
937
|
-
const color = { image: '#1677e8', video: '#7b58cf', camera: c.accent, document: '#079ec3' }[kind];
|
|
938
|
-
const label = { image: 'Photos', video: 'Videos', camera: 'Camera', document: 'Files' }[kind];
|
|
939
|
-
return _jsxs(Pressable, { accessibilityRole: "button", accessibilityLabel: kind === 'camera' ? 'Camera' : mediaNames[kind], accessibilityState: { selected: attachmentFilter === kind }, disabled: busy, onPress: () => { if (kind === 'camera')
|
|
940
|
-
void pick(features?.image ? 'image' : 'video', 'camera');
|
|
941
|
-
else if (!attachmentChoices)
|
|
942
|
-
void pick(kind);
|
|
943
|
-
else
|
|
944
|
-
setAttachmentFilter(attachmentFilter === kind ? null : kind); }, style: { flex: 1, minWidth: 0, minHeight: 84, alignItems: 'center', gap: 3 }, children: [_jsx(View, { style: { width: 56, height: 56, borderRadius: 28, backgroundColor: color + (attachmentFilter === kind ? '28' : '12'), borderWidth: attachmentFilter === kind ? 2 : 0, borderColor: color, alignItems: 'center', justifyContent: 'center' }, children: _jsx(MessageIcon, { name: kind, color: color, size: 28 }) }), _jsx(Text, { style: { color: c.foreground, fontSize: 13 }, children: label })] }, kind);
|
|
945
|
-
}), !editing && features?.poll && _jsxs(Pressable, { accessibilityRole: "button", accessibilityLabel: state.conversation?.kind === 'group' ? "Create poll" : "Ask Yes/No question", disabled: busy, onPress: () => { setAttachmentsOpen(false); setPollOpen(true); }, style: { flex: 1, minWidth: 0, minHeight: 84, alignItems: 'center', gap: 3 }, children: [_jsx(View, { style: { width: 56, height: 56, borderRadius: 28, backgroundColor: '#db7c2412', alignItems: 'center', justifyContent: 'center' }, children: _jsx(MessageIcon, { name: "poll", color: "#b76920", size: 28 }) }), _jsx(Text, { style: { color: c.foreground, fontSize: 13 }, children: state.conversation?.kind === 'group' ? 'Poll' : 'Yes/No' })] }), !editing && weatherAllowed && _jsx(WeatherComposerAction, { onClose: () => setAttachmentsOpen(false) })] }), !!attachmentChoices?.some(item => library[item.kind]) && _jsx(AttachmentStrip, { label: "Recent attachments", height: 116, recent: true, children: attachmentChoices.filter(item => library[item.kind] && (!attachmentFilter || item.kind === attachmentFilter)).map(item => _jsxs(View, { style: { width: 112, height: 112, borderRadius: 10, overflow: 'hidden', backgroundColor: c.incoming, borderWidth: 1, borderColor: c.secondary + '20' }, children: [_jsxs(Pressable, { accessibilityRole: "button", accessibilityLabel: `Choose ${item.name}`, disabled: busy, onPress: () => void pick(item.kind, undefined, item.id), style: { width: '100%', height: '100%' }, children: [item.thumbnailUri ? _jsx(Image, { source: { uri: item.thumbnailUri }, accessibilityLabel: item.name, resizeMode: "cover", style: { width: '100%', height: '100%' } }) : _jsxs(View, { style: { flex: 1, alignItems: 'center', justifyContent: 'center', padding: 9, gap: 8 }, children: [_jsx(MessageIcon, { name: item.kind, color: c.accent, size: 30 }), _jsx(Text, { numberOfLines: 2, style: { color: c.incomingText, fontSize: 11, textAlign: 'center' }, children: item.name })] }), item.kind === 'video' && _jsxs(View, { style: { position: 'absolute', bottom: 0, left: 0, right: 0, backgroundColor: '#0008', flexDirection: 'row', alignItems: 'center', gap: 5, padding: 6 }, children: [_jsx(MessageIcon, { name: "play", size: 13, color: "#fff" }), _jsx(Text, { style: { color: '#fff', fontSize: 11 }, children: duration(item.durationMs) })] })] }), _jsx(Pressable, { accessibilityRole: "button", accessibilityLabel: `Preview ${item.name}`, disabled: busy, onPress: () => void pick(item.kind, undefined, item.id, 'preview'), style: { position: 'absolute', top: 0, right: 0, width: 44, height: 44, alignItems: 'center', justifyContent: 'center' }, children: _jsx(View, { style: { width: 30, height: 30, borderRadius: 15, backgroundColor: '#111b', alignItems: 'center', justifyContent: 'center' }, children: _jsx(MessageIcon, { name: "search", size: 18, color: "#fff" }) }) })] }, item.id)) }), attachmentFilter && !attachmentChoices?.some(item => library[item.kind] && item.kind === attachmentFilter) && _jsxs(Text, { style: { color: c.secondary, paddingVertical: 16, textAlign: 'center' }, children: ["No recent ", attachmentFilter === 'image' ? 'photos' : attachmentFilter === 'video' ? 'videos' : 'files', "."] })] }), scheduleOpen && _jsx(ScheduleTimePicker, { initial: scheduleAttempt.current?.time, retryAt: scheduleError ? scheduleAttempt.current?.time : undefined, accent: c.accent, busy: busy, error: scheduleError, onSave: time => void schedule(time), onClose: () => setScheduleOpen(false) }), pollOpen && features?.poll && _jsx(PollComposer, { yesNo: state.conversation?.kind !== 'group', appearance: design, onSend: poll => submit({ kind: 'poll', poll }), onCancel: () => setPollOpen(false) }), error && (_jsx(Text, { accessibilityRole: "alert", style: { color: c.foreground }, children: error }))] }));
|
|
940
|
+
return (_jsxs(View, { testID: "messaging-composer", style: { paddingStart: 8, paddingEnd: 0, paddingVertical: 4, gap: 8, backgroundColor: c.background }, children: [mentionSuggestions.length > 0 && _jsx(ScrollView, { keyboardShouldPersistTaps: "handled", style: { maxHeight: 144 }, children: mentionSuggestions.map(id => _jsx(Pressable, { accessibilityRole: "button", accessibilityLabel: 'Mention ' + participants[id].name, onPress: () => chooseMention(id), style: { minHeight: 44, paddingHorizontal: 12, justifyContent: 'center', borderBottomWidth: .5, borderBottomColor: c.secondary + '30' }, children: _jsxs(Text, { style: { color: c.accent, fontSize: 16 }, children: ["@", participants[id].name] }) }, id)) }), mentionQuery !== undefined && !!mentionError && _jsx(Text, { style: { color: c.secondary, fontSize: 13 }, children: mentionError }), editing && _jsxs(View, { style: { flexDirection: 'row', alignItems: 'center', gap: 8 }, children: [_jsxs(Text, { numberOfLines: 1, style: { flex: 1, color: c.secondary }, children: ["Editing: ", editing.text || editing.media?.fileName] }), _jsx(Pressable, { accessibilityRole: "button", accessibilityLabel: "Cancel editing", disabled: busy, onPress: () => setEditing(null), style: { minHeight: 36, justifyContent: 'center' }, children: _jsx(Text, { style: { color: c.accent }, children: "Cancel" }) })] }), !editing && reply && (_jsxs(View, { testID: "composer-quote", style: { flexDirection: "row", gap: 8 }, children: [_jsx(View, { style: { flex: 1 }, children: _jsx(QuotedPreview, { message: reply, ink: c.secondary }) }), _jsx(Pressable, { accessibilityRole: "button", accessibilityLabel: "Cancel quote", onPress: () => setReply(null), children: _jsx(Text, { style: { color: c.foreground }, children: "Cancel" }) })] })), state.settings?.history?.mode === 'last_two' && _jsx(Text, { style: { color: c.secondary, fontSize: 11, textAlign: 'center' }, children: "Only the latest two messages are kept." }), editing ? _jsxs(_Fragment, { children: [renderAttachments(), renderTextComposer()] }) : features?.voice ? _jsx(VoiceComposer, { appearance: design, maxSeconds: state.settings?.voice?.maxDurationSeconds ?? 60, busy: busy, onRecord: () => { Keyboard.dismiss(); void pick('voice'); }, renderAttachments: renderAttachments, renderTextComposer: features?.text || selected.length ? renderTextComposer : undefined, actions: _jsx(_Fragment, { children: (camera || library.image || library.video || library.document || !editing && (features?.poll || weatherAllowed)) && _jsx(Pressable, { ref: attachmentButton, accessibilityRole: "button", accessibilityLabel: "Attach", accessibilityState: { expanded: attachmentsOpen }, disabled: busy, onPress: () => setAttachmentsOpen(!attachmentsOpen), style: { width: 44, height: 44, alignItems: 'center', justifyContent: 'center' }, children: _jsx(MessageIcon, { name: attachmentsOpen ? 'close' : 'plus', color: c.foreground, size: 25 }) }) }) }) : _jsxs(_Fragment, { children: [renderAttachments(), renderTextComposer()] }), attachmentsOpen && _jsx(AttachmentMenu, { anchor: attachmentButton, accent: c.accent, onClose: () => setAttachmentsOpen(false), actions: attachmentActions }), scheduleOpen && _jsx(ScheduleTimePicker, { initial: scheduleAttempt.current?.time, retryAt: scheduleError ? scheduleAttempt.current?.time : undefined, accent: c.accent, busy: busy, error: scheduleError, onSave: time => void schedule(time), onClose: () => setScheduleOpen(false) }), pollOpen && features?.poll && _jsx(PollComposer, { yesNo: state.conversation?.kind !== 'group', appearance: design, onSend: poll => submit({ kind: 'poll', poll }), onCancel: () => setPollOpen(false) }), error && (_jsx(Text, { accessibilityRole: "alert", style: { color: c.foreground }, children: error }))] }));
|
|
946
941
|
}
|
|
947
942
|
/** Account navigation. Mount in the inbox/app toolbar, never in a contact menu. */
|
|
948
|
-
export function MessagingAccountMenu({ onNewGroup } = {}) {
|
|
943
|
+
export function MessagingAccountMenu({ onNewGroup, onMaps } = {}) {
|
|
949
944
|
const { client, design, setSavedOpen, account, companyName } = useMessagingUI();
|
|
950
945
|
const [open, setOpen] = useState(false), [accountOpen, setAccountOpen] = useState(false), pending = useRef(null);
|
|
951
946
|
const button = useRef(null), viewport = useWindowDimensions(), [anchor, setAnchor] = useState({ x: 12, y: 12 });
|
|
952
947
|
const width = Math.min(218, viewport.width - 24);
|
|
953
|
-
const show = () => button.current?.measureInWindow((x, y, w, h) => { setAnchor({ x: Math.max(12, Math.min(x + w - width, viewport.width - width - 12)), y: Math.max(12, Math.min(y + h + 4, viewport.height - (client.hasSavedMessages() ? 116 : 72) - (onNewGroup ? 44 : 0))) }); setOpen(true); });
|
|
948
|
+
const show = () => button.current?.measureInWindow((x, y, w, h) => { setAnchor({ x: Math.max(12, Math.min(x + w - width, viewport.width - width - 12)), y: Math.max(12, Math.min(y + h + 4, viewport.height - (client.hasSavedMessages() ? 116 : 72) - (onNewGroup ? 44 : 0) - (onMaps ? 44 : 0))) }); setOpen(true); });
|
|
954
949
|
const dismissed = () => { const next = pending.current; if (!next)
|
|
955
|
-
return; pending.current = null; requestAnimationFrame(() => next === 'saved' ? setSavedOpen(true) : next === 'group' ? onNewGroup?.() : setAccountOpen(true)); };
|
|
950
|
+
return; pending.current = null; requestAnimationFrame(() => next === 'saved' ? setSavedOpen(true) : next === 'group' ? onNewGroup?.() : next === 'maps' ? onMaps?.() : setAccountOpen(true)); };
|
|
956
951
|
useEffect(() => { if (!open && Platform.OS !== 'ios')
|
|
957
952
|
dismissed(); }, [open]);
|
|
958
953
|
useEffect(() => { if (!open || Platform.OS !== 'web')
|
|
@@ -960,7 +955,7 @@ export function MessagingAccountMenu({ onNewGroup } = {}) {
|
|
|
960
955
|
event.stopPropagation();
|
|
961
956
|
setOpen(false);
|
|
962
957
|
} }; document.addEventListener('keydown', key, true); return () => document.removeEventListener('keydown', key, true); }, [open]);
|
|
963
|
-
return _jsxs(_Fragment, { children: [_jsx(Pressable, { ref: button, accessibilityRole: "button", accessibilityLabel: "Messaging menu", accessibilityState: { expanded: open }, onPress: show, style: { width: 44, height: 44, alignItems: 'center', justifyContent: 'center' }, children: _jsx(MessageIcon, { name: "more", color: design.colors.foreground, size: 23 }) }), _jsx(
|
|
958
|
+
return _jsxs(_Fragment, { children: [_jsx(Pressable, { ref: button, accessibilityRole: "button", accessibilityLabel: "Messaging menu", accessibilityState: { expanded: open }, onPress: show, style: { width: 44, height: 44, alignItems: 'center', justifyContent: 'center' }, children: _jsx(MessageIcon, { name: "more", color: design.colors.foreground, size: 23 }) }), _jsx(MessagingMenuSurface, { visible: open, onClose: () => setOpen(false), onDismiss: dismissed, children: rect => _jsxs(View, { accessibilityViewIsModal: true, style: { flex: 1 }, children: [_jsx(Pressable, { accessibilityLabel: "Close messaging menu", onPress: () => setOpen(false), style: { position: 'absolute', inset: 0 } }), _jsx(View, { testID: "messaging-account-dropdown", style: { position: 'absolute', left: Math.max(8, Math.min(anchor.x - rect.left, rect.width - Math.min(width, rect.width - 16) - 8)), top: Math.max(8, Math.min(anchor.y - rect.top, rect.height - (client.hasSavedMessages() ? 116 : 72) - (onNewGroup ? 44 : 0) - (onMaps ? 44 : 0) - 8)), width: Math.min(width, rect.width - 16), maxHeight: Math.max(44, rect.height - 16), borderRadius: 16, borderWidth: .5, borderColor: '#ffffff22', backgroundColor: '#151515', paddingVertical: 4, overflow: 'hidden', boxShadow: '0 8px 18px #0006' }, children: _jsxs(ScrollView, { keyboardShouldPersistTaps: "handled", children: [_jsxs(Pressable, { accessibilityRole: "button", accessibilityLabel: "Account", onPress: () => { pending.current = 'account'; setOpen(false); }, style: ({ pressed }) => ({ minHeight: 44, paddingHorizontal: 12, flexDirection: 'row', alignItems: 'center', gap: 10, backgroundColor: pressed ? '#ffffff12' : 'transparent' }), children: [_jsx(MessageIcon, { name: "person-circle-outline", color: "#f5f5f5", size: 22 }), _jsx(Text, { style: { color: '#f5f5f5', fontSize: 15, fontWeight: '600' }, children: "Account" })] }), onMaps && _jsxs(Pressable, { accessibilityRole: "button", accessibilityLabel: "Maps", onPress: () => { pending.current = 'maps'; setOpen(false); }, style: ({ pressed }) => ({ minHeight: 44, paddingHorizontal: 12, flexDirection: 'row', alignItems: 'center', gap: 10, backgroundColor: pressed ? '#ffffff12' : 'transparent' }), children: [_jsx(MessageIcon, { name: "location", color: "#f5f5f5", size: 22 }), _jsx(Text, { style: { color: '#f5f5f5', fontSize: 15, fontWeight: '600' }, children: "Maps" })] }), onNewGroup && _jsxs(Pressable, { accessibilityRole: "button", accessibilityLabel: "New group", onPress: () => { pending.current = 'group'; setOpen(false); }, style: ({ pressed }) => ({ minHeight: 44, paddingHorizontal: 12, flexDirection: 'row', alignItems: 'center', gap: 10, backgroundColor: pressed ? '#ffffff12' : 'transparent' }), children: [_jsx(MessageIcon, { name: "people-outline", color: "#f5f5f5", size: 22 }), _jsx(Text, { style: { color: '#f5f5f5', fontSize: 15, fontWeight: '600' }, children: "New group" })] }), client.hasSavedMessages() && _jsxs(Pressable, { accessibilityRole: "button", accessibilityLabel: "Saved messages", onPress: () => { pending.current = 'saved'; setOpen(false); }, style: ({ pressed }) => ({ minHeight: 44, paddingHorizontal: 12, flexDirection: 'row', alignItems: 'center', gap: 10, backgroundColor: pressed ? '#ffffff12' : 'transparent' }), children: [_jsx(MessageIcon, { name: "bookmark-outline", color: "#f5f5f5", size: 22 }), _jsx(Text, { style: { color: '#f5f5f5', fontSize: 15, fontWeight: '600' }, children: "Saved" })] })] }) })] }) }), accountOpen && _jsx(MessagingAccountScreen, { account: account, companyName: companyName ?? design.name, onClose: () => setAccountOpen(false) })] });
|
|
964
959
|
}
|
|
965
960
|
export function MessagingConversation() {
|
|
966
961
|
const { state, client, components, design, style } = useMessagingUI();
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import type { LocationWorkspace } from '@robono/location';
|
|
3
|
+
/** Recover settings after reconnect/foreground without capturing GPS or loading a map. */
|
|
4
|
+
export declare function useMessengerMaps(workspace?: LocationWorkspace): {
|
|
5
|
+
available: boolean;
|
|
6
|
+
error: string;
|
|
7
|
+
retry: () => Promise<void>;
|
|
8
|
+
};
|
|
9
|
+
export declare function MessengerMapsNotice({ error, retry }: {
|
|
10
|
+
error: string;
|
|
11
|
+
retry: () => Promise<void>;
|
|
12
|
+
}): React.JSX.Element | null;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
3
|
+
import { AppState, Platform, Pressable, Text, View } from 'react-native';
|
|
4
|
+
/** Recover settings after reconnect/foreground without capturing GPS or loading a map. */
|
|
5
|
+
export function useMessengerMaps(workspace) {
|
|
6
|
+
const [available, setAvailable] = useState(false), [error, setError] = useState('');
|
|
7
|
+
const current = useRef(workspace);
|
|
8
|
+
current.current = workspace;
|
|
9
|
+
const pending = useRef(null), mounted = useRef(true);
|
|
10
|
+
const refresh = useCallback(() => {
|
|
11
|
+
if (!workspace)
|
|
12
|
+
return Promise.resolve();
|
|
13
|
+
if (pending.current)
|
|
14
|
+
return pending.current;
|
|
15
|
+
const work = workspace.client.settings().then(({ settings }) => { if (mounted.current && current.current === workspace) {
|
|
16
|
+
setAvailable(!!settings?.enabled && settings.placement !== 'messaging');
|
|
17
|
+
setError('');
|
|
18
|
+
} }).catch(() => { if (mounted.current && current.current === workspace)
|
|
19
|
+
setError('Maps could not load.'); }).finally(() => { if (pending.current === work)
|
|
20
|
+
pending.current = null; });
|
|
21
|
+
pending.current = work;
|
|
22
|
+
return work;
|
|
23
|
+
}, [workspace]);
|
|
24
|
+
useEffect(() => {
|
|
25
|
+
mounted.current = true;
|
|
26
|
+
pending.current = null;
|
|
27
|
+
setAvailable(false);
|
|
28
|
+
setError('');
|
|
29
|
+
void refresh();
|
|
30
|
+
const subscription = AppState.addEventListener('change', state => { if (state === 'active')
|
|
31
|
+
void refresh(); });
|
|
32
|
+
const online = () => { void refresh(); };
|
|
33
|
+
if (Platform.OS === 'web' && typeof window !== 'undefined')
|
|
34
|
+
window.addEventListener('online', online);
|
|
35
|
+
return () => { mounted.current = false; subscription.remove(); if (Platform.OS === 'web' && typeof window !== 'undefined')
|
|
36
|
+
window.removeEventListener('online', online); };
|
|
37
|
+
}, [refresh]);
|
|
38
|
+
return { available, error, retry: refresh };
|
|
39
|
+
}
|
|
40
|
+
export function MessengerMapsNotice({ error, retry }) {
|
|
41
|
+
return error ? _jsxs(View, { style: { backgroundColor: '#171717', paddingHorizontal: 16, paddingVertical: 8, flexDirection: 'row', gap: 12, alignItems: 'center' }, children: [_jsx(Text, { accessibilityRole: "alert", style: { color: '#FAFAFA', flex: 1 }, children: error }), _jsx(Pressable, { accessibilityRole: "button", accessibilityLabel: "Retry Maps", onPress: () => void retry(), style: { padding: 8 }, children: _jsx(Text, { style: { color: '#FF8A2C' }, children: "Retry Maps" }) })] }) : null;
|
|
42
|
+
}
|
|
@@ -31,7 +31,7 @@ export interface MessagingMediaStorage {
|
|
|
31
31
|
export interface MessagingMediaDevice {
|
|
32
32
|
pick(kind: Exclude<MediaKind, 'voice'>, settings: MessagingSettings, signal: AbortSignal, source?: "camera" | "call-video"): Promise<LocalMessagingMedia | null>;
|
|
33
33
|
/** Ordered local selections. No upload or send occurs in the picker. */
|
|
34
|
-
pickMany?(kind: Exclude<MediaKind, 'voice'>, settings: MessagingSettings, signal: AbortSignal, maximum: number): Promise<LocalMessagingMedia[]>;
|
|
34
|
+
pickMany?(kind: Exclude<MediaKind, 'voice'>, settings: MessagingSettings, signal: AbortSignal, maximum: number, mixedLibrary?: boolean): Promise<LocalMessagingMedia[]>;
|
|
35
35
|
startRecording(maxDurationMs: number, signal: AbortSignal): Promise<void>;
|
|
36
36
|
recordingStatus(): {
|
|
37
37
|
durationMs: number;
|
|
@@ -101,7 +101,7 @@ export declare class MessagingMediaController {
|
|
|
101
101
|
private update;
|
|
102
102
|
private isCurrent;
|
|
103
103
|
pick(kind: MediaKind, source?: "camera" | "call-video", selectionIntent?: "preview" | "send" | "attach"): Promise<MessageMedia | null>;
|
|
104
|
-
pickMany(kind: Exclude<MediaKind, 'voice'>, maximum: number): Promise<MessageMedia[]>;
|
|
104
|
+
pickMany(kind: Exclude<MediaKind, 'voice'>, maximum: number, mixedLibrary?: boolean): Promise<MessageMedia[]>;
|
|
105
105
|
private check;
|
|
106
106
|
finishRecording(interrupted?: boolean): Promise<void>;
|
|
107
107
|
send(): Promise<void>;
|