@robono/messaging-react-native 0.8.0 → 0.9.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 +40 -37
- package/SBOM.spdx.json +229 -157
- package/dist/account.js +5 -3
- package/dist/attachment-menu.js +3 -1
- package/dist/call-message.js +6 -4
- package/dist/calling.js +3 -1
- package/dist/conversation-details.js +9 -6
- package/dist/expo-connected-calling.js +4 -1
- package/dist/expo-extras.js +4 -1
- package/dist/expo-media.js +27 -5
- package/dist/expo-messenger.d.ts +38 -2
- package/dist/expo-messenger.js +56 -6
- package/dist/expo-outbox.js +22 -1
- package/dist/expo-screen.d.ts +1 -1
- package/dist/expo-screen.js +23 -14
- package/dist/expo-social.js +3 -1
- package/dist/expo.d.ts +1 -1
- package/dist/expo.js +1 -1
- package/dist/extras.js +9 -6
- package/dist/forward.js +6 -4
- package/dist/group-editor.js +7 -5
- package/dist/header-calls.js +7 -5
- package/dist/inbox-search.js +4 -2
- package/dist/inbox.js +9 -7
- package/dist/index.d.ts +6 -3
- package/dist/index.js +91 -61
- package/dist/localization.d.ts +24 -0
- package/dist/localization.js +12 -0
- package/dist/maps-connection.js +3 -1
- package/dist/media-ui.js +13 -7
- package/dist/message-menu.js +3 -1
- package/dist/native-outbox.d.ts +9 -1
- package/dist/native-outbox.js +7 -0
- package/dist/polls.js +18 -14
- package/dist/popup.js +6 -3
- package/dist/quick-send.js +4 -2
- package/dist/safety.js +9 -7
- package/dist/saved.js +5 -3
- package/dist/scheduled.js +17 -13
- package/dist/search.js +5 -3
- package/dist/social.js +7 -4
- package/dist/translation-settings.js +5 -3
- package/dist/voice-composer.js +9 -5
- package/package.json +9 -7
- package/setup.plugin.cjs +20 -0
package/dist/index.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { useRobonoLocale, RobonoLocaleProvider, localizeMessagingAppearance } from './localization.js';
|
|
3
|
+
export { RobonoLocaleProvider, useRobonoLocale } from './localization.js';
|
|
2
4
|
import { MessagingMenuSurface } from './popup.js';
|
|
3
5
|
import { ScheduleTimePicker, ScheduledMessages } from './scheduled.js';
|
|
4
6
|
import { MessagingGroupEditor } from './group-editor.js';
|
|
@@ -43,7 +45,11 @@ export function useMessagingUI() {
|
|
|
43
45
|
throw Error("Wrap messaging components in MessagingProvider.");
|
|
44
46
|
return context;
|
|
45
47
|
}
|
|
46
|
-
export function MessagingProvider(
|
|
48
|
+
export function MessagingProvider(props) {
|
|
49
|
+
return _jsx(RobonoLocaleProvider, { value: props.interfaceLocale, children: _jsx(MessagingProviderContent, { ...props }) });
|
|
50
|
+
}
|
|
51
|
+
function MessagingProviderContent({ children, ...props }) {
|
|
52
|
+
const { text: uiText, locale: uiLocale, number: uiNumber, count: uiCount } = useRobonoLocale();
|
|
47
53
|
const state = useSyncExternalStore(props.client.subscribe, props.client.getSnapshot, props.client.getSnapshot);
|
|
48
54
|
const extrasState = useSyncExternalStore(props.extras?.subscribe ?? noSubscribe, props.extras?.getSnapshot ?? noExtras, props.extras?.getSnapshot ?? noExtras);
|
|
49
55
|
const messageIds = state.messages.filter(m => !m.deleted).map(m => m.id).join(',');
|
|
@@ -75,7 +81,7 @@ export function MessagingProvider({ children, ...props }) {
|
|
|
75
81
|
useEffect(() => { let live = true; setSaved([]); setSavedOpen(false); if (props.client.hasSavedMessages())
|
|
76
82
|
void props.client.savedMessages().then(rows => { if (live)
|
|
77
83
|
setSaved(rows); }).catch(() => { if (live)
|
|
78
|
-
setSavedError(
|
|
84
|
+
setSavedError(uiText("Saved messages could not be opened.")); }); return () => { live = false; }; }, [props.client, state.conversationId]);
|
|
79
85
|
async function toggleSaved(message) { if (savingRef.current)
|
|
80
86
|
return; const origin = savedContext.current, live = () => savedContext.current.client === origin.client && savedContext.current.conversationId === origin.conversationId; savingRef.current = true; setSaving(true); setSavedError(''); try {
|
|
81
87
|
const key = savedMessageKey(message);
|
|
@@ -91,7 +97,7 @@ export function MessagingProvider({ children, ...props }) {
|
|
|
91
97
|
}
|
|
92
98
|
catch (e) {
|
|
93
99
|
if (live())
|
|
94
|
-
setSavedError(e instanceof Error ? e.message :
|
|
100
|
+
setSavedError(e instanceof Error ? e.message : uiText("This message could not be saved."));
|
|
95
101
|
}
|
|
96
102
|
finally {
|
|
97
103
|
savingRef.current = false;
|
|
@@ -138,8 +144,7 @@ export function MessagingProvider({ children, ...props }) {
|
|
|
138
144
|
setReply,
|
|
139
145
|
editing: state.messageActionsAvailable && editing?.conversationId === state.conversationId && state.settings?.enabled && state.conversation?.canSend !== false && state.messages.some(m => m.id === editing.id && !m.deleted) ? editing : null,
|
|
140
146
|
setEditing, setForwarding, setDeleting, selection: selected, setSelection, saved, saving, toggleSaved, setSavedOpen,
|
|
141
|
-
design: props.appearance ?? state.settings?.appearance ??
|
|
142
|
-
defaultMessagingSettings().appearance,
|
|
147
|
+
design: localizeMessagingAppearance(props.appearance ?? state.settings?.appearance ?? defaultMessagingSettings().appearance, uiText),
|
|
143
148
|
};
|
|
144
149
|
return _jsx(Context.Provider, { value: value, children: _jsxs(MessagingPopupHost, { children: [children, discussion && _jsx(GroupDiscussion, { threadRoot: discussion.threadRoot, onClose: () => setDiscussion(null) }), forwarding.length > 0 && _jsx(MessagingForward, { messages: forwarding, client: props.client, appearance: value.design, participants: props.participants, userId: props.userId, onClose: () => setForwarding([]) }), _jsx(MessageDeletion, { messages: deleting, onClose: () => setDeleting([]) }), savedOpen && _jsx(SavedMessagesPopup, { records: saved, appearance: value.design, onClose: () => setSavedOpen(false), onOpen: async (record) => { setSavedOpen(false); try {
|
|
145
150
|
if (!props.openSavedAttachment)
|
|
@@ -147,10 +152,11 @@ export function MessagingProvider({ children, ...props }) {
|
|
|
147
152
|
await props.openSavedAttachment(record);
|
|
148
153
|
}
|
|
149
154
|
catch (e) {
|
|
150
|
-
setSavedError(e instanceof Error ? e.message :
|
|
151
|
-
} }, onRemove: async (key) => { await props.client.removeSavedMessage(key); setSaved(await props.client.savedMessages()); } }), !!savedError && _jsxs(MessagingPopup, { compact: true, contentWidth: 640, title: "Saved", foreground: value.design.colors.foreground, background: value.design.colors.background, onClose: () => setSavedError(''), children: [_jsx(Text, { accessibilityRole: "alert", style: { color: messagingPopupColors.foreground }, children: savedError }), _jsx(Pressable, { accessibilityRole: "button", onPress: () => setSavedError(''), style: { minHeight: 44, alignSelf: 'flex-end', justifyContent: 'center' }, children: _jsx(Text, { style: { color: value.design.colors.accent }, children: "OK" }) })] })] }) });
|
|
155
|
+
setSavedError(e instanceof Error ? e.message : uiText("This saved attachment could not be opened."));
|
|
156
|
+
} }, onRemove: async (key) => { await props.client.removeSavedMessage(key); setSaved(await props.client.savedMessages()); } }), !!savedError && _jsxs(MessagingPopup, { compact: true, contentWidth: 640, title: uiText("Saved"), foreground: value.design.colors.foreground, background: value.design.colors.background, onClose: () => setSavedError(''), children: [_jsx(Text, { accessibilityRole: "alert", style: { color: messagingPopupColors.foreground }, children: savedError }), _jsx(Pressable, { accessibilityRole: "button", onPress: () => setSavedError(''), style: { minHeight: 44, alignSelf: 'flex-end', justifyContent: 'center' }, children: _jsx(Text, { style: { color: value.design.colors.accent }, children: uiText("OK") }) })] })] }) });
|
|
152
157
|
}
|
|
153
158
|
function GroupDiscussion({ threadRoot, onClose }) {
|
|
159
|
+
const { text: uiText, locale: uiLocale, number: uiNumber, count: uiCount } = useRobonoLocale();
|
|
154
160
|
const context = useMessagingUI(), { client, state, design } = context;
|
|
155
161
|
const [messages, setMessages] = useState([]), [root, setRoot] = useState(), [next, setNext] = useState(null), [busy, setBusy] = useState(false), [error, setError] = useState(''), [visible, setVisible] = useState(true);
|
|
156
162
|
const loaded = useRef([]), loading = useRef(false), refreshPage = useRef(() => { });
|
|
@@ -196,7 +202,7 @@ function GroupDiscussion({ threadRoot, onClose }) {
|
|
|
196
202
|
if (generation === version.current && !control.signal.aborted) {
|
|
197
203
|
setMessages([]);
|
|
198
204
|
setRoot(undefined);
|
|
199
|
-
setError(e instanceof Error ? e.message :
|
|
205
|
+
setError(e instanceof Error ? e.message : uiText("These messages could not load."));
|
|
200
206
|
}
|
|
201
207
|
}
|
|
202
208
|
finally {
|
|
@@ -215,8 +221,8 @@ function GroupDiscussion({ threadRoot, onClose }) {
|
|
|
215
221
|
const open = (message, reply = false) => { pending.current = () => { void client.focusMessage(message.id).then(() => { if (reply && client.getSnapshot().conversationId === message.conversationId)
|
|
216
222
|
context.setReply({ ...message, threadRoot: message.id }); }).catch(() => { }); }; setVisible(false); };
|
|
217
223
|
const rows = root ? [root, ...messages.filter(m => m.id !== root.id)] : messages;
|
|
218
|
-
return _jsxs(MessagingPopup, { visible: visible, onDismiss: dismiss, title: threadRoot ?
|
|
219
|
-
open(root ?? message, true); }, openAttachment: context.openAttachment ? async (message) => { pending.current = () => { void context.openAttachment(message).catch(() => { }); }; setVisible(false); } : undefined }, children: _jsx(FlatList, { data: rows, keyExtractor: m => m.id, style: { flex: 1 }, contentContainerStyle: { paddingVertical: 12, gap: 10 }, ListHeaderComponent: next ? _jsx(Pressable, { accessibilityRole: "button", disabled: busy, onPress: () => void load(true), style: { minHeight: 44, justifyContent: 'center', alignItems: 'center' }, children: _jsx(Text, { style: { color: design.colors.accent }, children: "Earlier replies" }) }) : null, renderItem: ({ item, index }) => _jsxs(View, { children: [root && index === 0 && _jsx(Text, { style: { color: messagingPopupColors.secondary, marginBottom: 8 }, children: "Original message" }), _jsx(MessagingBubble, { message: item }), !threadRoot && _jsx(Pressable, { accessibilityRole: "button", accessibilityLabel: "Go to pinned message", onPress: () => open(item), style: { minHeight: 44, justifyContent: 'center' }, children: _jsx(Text, { style: { color: design.colors.accent }, children: "Go to message" }) })] }), ListEmptyComponent: !busy && !error ? _jsx(Text, { style: { color: messagingPopupColors.secondary, textAlign: 'center', padding: 20 }, children: "No pinned messages." }) : null }) }), busy && _jsx(ActivityIndicator, { color: design.colors.accent }), !!error && _jsxs(View, { children: [_jsx(Text, { accessibilityRole: "alert", style: { color: messagingPopupColors.foreground }, children: error }), _jsx(Pressable, { accessibilityRole: "button", onPress: () => void load(), style: { minHeight: 44, justifyContent: 'center' }, children: _jsx(Text, { style: { color: design.colors.accent }, children: "Try again" }) })] }), root && state.conversation?.canSend !== false && _jsx(Pressable, { accessibilityRole: "button", accessibilityLabel: "Reply in conversation", onPress: () => open(root, true), style: { minHeight: 48, alignItems: 'center', justifyContent: 'center', borderTopWidth: 1, borderTopColor: '#ffffff20' }, children: _jsx(Text, { style: { color: design.colors.accent, fontSize: 16, fontWeight: '600' }, children: "Reply" }) })] });
|
|
224
|
+
return _jsxs(MessagingPopup, { visible: visible, onDismiss: dismiss, title: threadRoot ? uiText("Replies") : uiText("Pinned messages"), onClose: () => setVisible(false), background: messagingPopupColors.background, children: [_jsx(Context.Provider, { value: { ...context, discussionPreview: true, discussionRoot: threadRoot, state: { ...state, messages: rows }, setReply: message => { if (message)
|
|
225
|
+
open(root ?? message, true); }, openAttachment: context.openAttachment ? async (message) => { pending.current = () => { void context.openAttachment(message).catch(() => { }); }; setVisible(false); } : undefined }, children: _jsx(FlatList, { data: rows, keyExtractor: m => m.id, style: { flex: 1 }, contentContainerStyle: { paddingVertical: 12, gap: 10 }, ListHeaderComponent: next ? _jsx(Pressable, { accessibilityRole: "button", disabled: busy, onPress: () => void load(true), style: { minHeight: 44, justifyContent: 'center', alignItems: 'center' }, children: _jsx(Text, { style: { color: design.colors.accent }, children: uiText("Earlier replies") }) }) : null, renderItem: ({ item, index }) => _jsxs(View, { children: [root && index === 0 && _jsx(Text, { style: { color: messagingPopupColors.secondary, marginBottom: 8 }, children: uiText("Original message") }), _jsx(MessagingBubble, { message: item }), !threadRoot && _jsx(Pressable, { accessibilityRole: "button", accessibilityLabel: uiText("Go to pinned message"), onPress: () => open(item), style: { minHeight: 44, justifyContent: 'center' }, children: _jsx(Text, { style: { color: design.colors.accent }, children: uiText("Go to message") }) })] }), ListEmptyComponent: !busy && !error ? _jsx(Text, { style: { color: messagingPopupColors.secondary, textAlign: 'center', padding: 20 }, children: uiText("No pinned messages.") }) : null }) }), busy && _jsx(ActivityIndicator, { color: design.colors.accent }), !!error && _jsxs(View, { children: [_jsx(Text, { accessibilityRole: "alert", style: { color: messagingPopupColors.foreground }, children: error }), _jsx(Pressable, { accessibilityRole: "button", onPress: () => void load(), style: { minHeight: 44, justifyContent: 'center' }, children: _jsx(Text, { style: { color: design.colors.accent }, children: uiText("Try again") }) })] }), root && state.conversation?.canSend !== false && _jsx(Pressable, { accessibilityRole: "button", accessibilityLabel: uiText("Reply in conversation"), onPress: () => open(root, true), style: { minHeight: 48, alignItems: 'center', justifyContent: 'center', borderTopWidth: 1, borderTopColor: '#ffffff20' }, children: _jsx(Text, { style: { color: design.colors.accent, fontSize: 16, fontWeight: '600' }, children: uiText("Reply") }) })] });
|
|
220
226
|
}
|
|
221
227
|
export function messagingHeaderColors(design, group) {
|
|
222
228
|
const background = group ? '#1F5F7A' : design.colors.accent;
|
|
@@ -225,16 +231,19 @@ export function messagingHeaderColors(design, group) {
|
|
|
225
231
|
return { background, foreground };
|
|
226
232
|
}
|
|
227
233
|
function MessagingActivity({ ink }) {
|
|
234
|
+
const { text: uiText, count: uiCount } = useRobonoLocale();
|
|
228
235
|
const { state, design, participants } = useMessagingUI(), [now, setNow] = useState(Date.now());
|
|
229
236
|
useEffect(() => { setNow(Date.now()); if (!state.presence?.peers.length)
|
|
230
237
|
return; const timer = setInterval(() => setNow(Date.now()), 1000); return () => clearInterval(timer); }, [state.presence]);
|
|
231
238
|
const typing = state.settings?.features.typing ? state.presence?.peers.filter(peer => peer.typingUntil > now) ?? [] : [];
|
|
232
239
|
const online = state.settings?.features.online && state.presence?.peers.some(peer => peer.onlineUntil > now);
|
|
233
240
|
const group = state.conversation?.kind === 'group';
|
|
234
|
-
const
|
|
241
|
+
const names = typing.map(peer => participants?.[peer.userId]?.name ?? uiText('Someone'));
|
|
242
|
+
const text = typing.length ? (group ? (typing.length > 2 ? uiText('Several people are typing…') : typing.length === 2 ? uiText('{first} and {second} are typing…', { first: names[0], second: names[1] }) : uiText('{name} is typing…', { name: names[0] })) : uiText('Typing…')) : !group && online ? uiText('Online now') : group ? uiCount(state.conversation?.memberCount ?? state.conversation?.memberIds?.length ?? 0, '{count} member', '{count} members') : '';
|
|
235
243
|
return text ? _jsx(Text, { numberOfLines: 1, style: { color: ink, opacity: .8, fontSize: 14 }, children: text }) : null;
|
|
236
244
|
}
|
|
237
245
|
export function MessagingHeader({ children } = {}) {
|
|
246
|
+
const { text: uiText, locale: uiLocale, number: uiNumber, count: uiCount } = useRobonoLocale();
|
|
238
247
|
const { contact, design, state, client, openDiscussion, userId, participants, onBack, onContactOptions, selection, setSelection, language, components, callActions, attachmentPreview, openAttachment } = useMessagingUI();
|
|
239
248
|
const [scheduledOpen, setScheduledOpen] = useState(false);
|
|
240
249
|
const [safetyOpen, setSafetyOpen] = useState(false), [searchOpen, setSearchOpen] = useState(false), [detailsOpen, setDetailsOpen] = useState(false), [groupOpen, setGroupOpen] = useState(false);
|
|
@@ -250,7 +259,7 @@ export function MessagingHeader({ children } = {}) {
|
|
|
250
259
|
paddingVertical: 4,
|
|
251
260
|
backgroundColor: header.background,
|
|
252
261
|
gap: 4,
|
|
253
|
-
}, children: [(onBack || selection.length > 0) && (_jsx(Pressable, { accessibilityRole: "button", accessibilityLabel: selection.length ?
|
|
262
|
+
}, children: [(onBack || selection.length > 0) && (_jsx(Pressable, { accessibilityRole: "button", accessibilityLabel: selection.length ? uiText("Clear message selection") : uiText("Back"), onPress: () => selection.length ? setSelection([]) : onBack?.(), style: { width: 44, flexShrink: 0, alignSelf: "stretch", marginStart: -12, marginEnd: -4, marginVertical: -4, alignItems: "center", justifyContent: "center" }, children: _jsx(MessageIcon, { name: "back", color: header.foreground, size: 32 }) })), _jsxs(View, { testID: "messaging-chat-identity", style: { flex: 1, minWidth: 0, flexDirection: "row", alignItems: "center", gap: 8 }, children: [design.showAvatars && (contact.avatarUrl
|
|
254
263
|
? (_jsx(Image, { source: { uri: contact.avatarUrl }, style: { width: 38, height: 38, borderRadius: 19, borderWidth: 1, borderColor: "#ffffff" } }))
|
|
255
264
|
: (_jsx(View, { style: {
|
|
256
265
|
width: 38,
|
|
@@ -261,14 +270,15 @@ export function MessagingHeader({ children } = {}) {
|
|
|
261
270
|
backgroundColor: c.incoming,
|
|
262
271
|
alignItems: "center",
|
|
263
272
|
justifyContent: "center",
|
|
264
|
-
}, children: _jsx(Text, { style: { color: c.incomingText }, children: state.conversation?.kind === 'group' ? (state.conversation.name ?? 'Group').slice(0, 1) : contact.name.slice(0, 1) }) }))), _jsxs(View, { style: { flex: 1, minWidth: 0 }, children: [_jsx(Text, { numberOfLines: 1, style: { color: header.foreground, fontSize: 18, fontWeight: "800" }, children: state.conversation?.kind === 'group' ? state.conversation.name : contact.name }), _jsx(MessagingActivity, { ink: header.foreground })] })] }), design.logoUrl && (_jsx(Image, { source: { uri: design.logoUrl }, accessibilityLabel: design.name, style: { width: 32, height: 32 } })), children, state.conversation?.canSend !== false && _jsx(MessagingHeaderCalls, { actions: callActions, name: state.conversation?.kind === 'group' ? (state.conversation.name ?? 'Group') : contact.name, group: state.conversation?.kind === 'group', color: header.foreground }, state.conversationId), _jsx(PrivacyControl, { color: header.foreground }), (onContactOptions || state.conversation?.safety || state.searchAvailable || state.scheduledMessagesAvailable || translationAvailable) && (_jsx(Pressable, { accessibilityRole: "button", accessibilityLabel: "Contact options", ref: menuButton, accessibilityState: { expanded: safetyOpen }, onPress: onContactOptions ?? openContactMenu, style: { width: 42, height: 42, alignItems: "center", justifyContent: "center" }, children: _jsx(MessageIcon, { name: "more", color: header.foreground, size: 25 }) })), safetyOpen && _jsx(MessagingSafety, { onScheduled: state.scheduledMessagesAvailable ? () => { setSafetyOpen(false); setScheduledOpen(true); } : undefined, onDetails: state.settings?.enabled && (state.sharedMediaAvailable || state.conversation?.kind === 'group') ? () => { setSafetyOpen(false); setDetailsOpen(true); } : undefined, onPins: state.discussionAvailable ? () => { setSafetyOpen(false); openDiscussion(); } : undefined, menuAnchor: menuAnchor, client: client, state: state, appearance: design, name: state.conversation?.kind === 'group' ? (state.conversation.name ?? 'Conversation') : contact.name, onSearch: state.searchAvailable && state.settings?.enabled ? () => { setSafetyOpen(false); setSearchOpen(true); } : undefined, onClose: () => setSafetyOpen(false), children: translationAvailable && _jsx(TranslationSettings, { client: client, appearance: design, preference: state.translationPreference, language: language, sourceLanguage: contact.language, voiceAvailable: !!components?.TranslationVoice, onSaved: () => setSafetyOpen(false) }, state.conversationId) })] }), scheduledOpen && _jsx(ScheduledMessages, { client: client, accent: c.accent, onClose: () => setScheduledOpen(false) }, state.conversationId), groupOpen && _jsx(MessagingGroupEditor, { client: client, userId: userId, contacts: participants ?? {}, conversation: state.conversation, groups: state.settings?.groups, accent: c.accent, onClose: () => setGroupOpen(false), onSaved: () => setGroupOpen(false) }), detailsOpen && _jsx(ConversationDetails, { onEditGroup: () => setGroupOpen(true), onLeft: onBack, client: client, state: state, userId: userId, contact: contact, participants: participants, accent: c.accent, onClose: () => setDetailsOpen(false), attachmentPreview: attachmentPreview, openAttachment: openAttachment }), searchOpen && state.searchAvailable && state.settings?.enabled && _jsx(MessagingSearch, { client: client, appearance: design, onClose: () => setSearchOpen(false), resolveName: id => id === userId ? 'You' : participants?.[id]?.name ?? (state.conversation?.kind === 'direct' ? contact.name : 'Chat member') }, state.conversationId)] }));
|
|
273
|
+
}, children: _jsx(Text, { style: { color: c.incomingText }, children: state.conversation?.kind === 'group' ? (state.conversation.name ?? 'Group').slice(0, 1) : contact.name.slice(0, 1) }) }))), _jsxs(View, { style: { flex: 1, minWidth: 0 }, children: [_jsx(Text, { numberOfLines: 1, style: { color: header.foreground, fontSize: 18, fontWeight: "800" }, children: state.conversation?.kind === 'group' ? state.conversation.name : contact.name }), _jsx(MessagingActivity, { ink: header.foreground })] })] }), design.logoUrl && (_jsx(Image, { source: { uri: design.logoUrl }, accessibilityLabel: design.name, style: { width: 32, height: 32 } })), children, state.conversation?.canSend !== false && _jsx(MessagingHeaderCalls, { actions: callActions, name: state.conversation?.kind === 'group' ? (state.conversation.name ?? 'Group') : contact.name, group: state.conversation?.kind === 'group', color: header.foreground }, state.conversationId), _jsx(PrivacyControl, { color: header.foreground }), (onContactOptions || state.conversation?.safety || state.searchAvailable || state.scheduledMessagesAvailable || translationAvailable) && (_jsx(Pressable, { accessibilityRole: "button", testID: "messaging-contact-options", accessibilityLabel: uiText("Contact options"), ref: menuButton, accessibilityState: { expanded: safetyOpen }, onPress: onContactOptions ?? openContactMenu, style: { width: 42, height: 42, alignItems: "center", justifyContent: "center" }, children: _jsx(MessageIcon, { name: "more", color: header.foreground, size: 25 }) })), safetyOpen && _jsx(MessagingSafety, { onScheduled: state.scheduledMessagesAvailable ? () => { setSafetyOpen(false); setScheduledOpen(true); } : undefined, onDetails: state.settings?.enabled && (state.sharedMediaAvailable || state.conversation?.kind === 'group') ? () => { setSafetyOpen(false); setDetailsOpen(true); } : undefined, onPins: state.discussionAvailable ? () => { setSafetyOpen(false); openDiscussion(); } : undefined, menuAnchor: menuAnchor, client: client, state: state, appearance: design, name: state.conversation?.kind === 'group' ? (state.conversation.name ?? 'Conversation') : contact.name, onSearch: state.searchAvailable && state.settings?.enabled ? () => { setSafetyOpen(false); setSearchOpen(true); } : undefined, onClose: () => setSafetyOpen(false), children: translationAvailable && _jsx(TranslationSettings, { client: client, appearance: design, preference: state.translationPreference, language: language, sourceLanguage: contact.language, voiceAvailable: !!components?.TranslationVoice, onSaved: () => setSafetyOpen(false) }, state.conversationId) })] }), scheduledOpen && _jsx(ScheduledMessages, { client: client, accent: c.accent, onClose: () => setScheduledOpen(false) }, state.conversationId), groupOpen && _jsx(MessagingGroupEditor, { client: client, userId: userId, contacts: participants ?? {}, conversation: state.conversation, groups: state.settings?.groups, accent: c.accent, onClose: () => setGroupOpen(false), onSaved: () => setGroupOpen(false) }), detailsOpen && _jsx(ConversationDetails, { onEditGroup: () => setGroupOpen(true), onLeft: onBack, client: client, state: state, userId: userId, contact: contact, participants: participants, accent: c.accent, onClose: () => setDetailsOpen(false), attachmentPreview: attachmentPreview, openAttachment: openAttachment }), searchOpen && state.searchAvailable && state.settings?.enabled && _jsx(MessagingSearch, { client: client, appearance: design, onClose: () => setSearchOpen(false), resolveName: id => id === userId ? 'You' : participants?.[id]?.name ?? (state.conversation?.kind === 'direct' ? contact.name : 'Chat member') }, state.conversationId)] }));
|
|
265
274
|
}
|
|
266
275
|
function PrivacyControl({ color }) {
|
|
276
|
+
const { text: uiText, locale: uiLocale, number: uiNumber, count: uiCount } = useRobonoLocale();
|
|
267
277
|
const { extras, extrasState, design } = useMessagingUI();
|
|
268
278
|
const [error, setError] = useState(''), [busy, setBusy] = useState(false);
|
|
269
279
|
if (!extras || !extrasState || !extrasState.available.privacyLens && !extrasState.privacyLens)
|
|
270
280
|
return null;
|
|
271
|
-
return _jsxs(_Fragment, { children: [_jsx(Pressable, { accessibilityRole: "button", accessibilityLabel: extrasState.privacyLens ?
|
|
281
|
+
return _jsxs(_Fragment, { children: [_jsx(Pressable, { accessibilityRole: "button", accessibilityLabel: extrasState.privacyLens ? uiText("Turn off Privacy Lens") : uiText("Turn on Privacy Lens"), disabled: busy, onPress: () => { setBusy(true); void extras.setPrivacy(!extrasState.privacyLens).catch(() => setError(uiText("Privacy Lens could not change. Try again."))).finally(() => setBusy(false)); }, style: { width: 44, height: 44, alignItems: 'center', justifyContent: 'center' }, children: _jsx(MessageIcon, { name: extrasState.privacyLens ? "eye-off" : "eye", color: color, size: 23 }) }), !!error && _jsx(ExtraPopup, { title: uiText("Privacy Lens"), background: design.colors.background, foreground: design.colors.foreground, onClose: () => setError(''), children: _jsx(Text, { style: { color: design.colors.foreground }, children: error }) })] });
|
|
272
282
|
}
|
|
273
283
|
const mediaNames = {
|
|
274
284
|
voice: "Voice message",
|
|
@@ -276,9 +286,10 @@ const mediaNames = {
|
|
|
276
286
|
video: "Video",
|
|
277
287
|
document: "File",
|
|
278
288
|
};
|
|
279
|
-
function quoteSummary(message) { return message?.poll?.question ?? message?.text ?? (message && message.kind !== "text" && message.kind !== "poll" ? mediaNames[message.kind] : "Earlier message"); }
|
|
289
|
+
function quoteSummary(message, uiText) { return message?.poll?.question ?? message?.text ?? uiText(message && message.kind !== "text" && message.kind !== "poll" ? mediaNames[message.kind] : "Earlier message"); }
|
|
280
290
|
/** The same compact quoted preview in the composer and delivered bubble. */
|
|
281
291
|
function QuotedPreview({ message, ink }) {
|
|
292
|
+
const { text: uiText, locale: uiLocale, number: uiNumber, count: uiCount } = useRobonoLocale();
|
|
282
293
|
const { attachmentPreview, userId, participants, contact, state, design } = useMessagingUI();
|
|
283
294
|
const [thumbnail, setThumbnail] = useState();
|
|
284
295
|
useEffect(() => {
|
|
@@ -291,11 +302,12 @@ function QuotedPreview({ message, ink }) {
|
|
|
291
302
|
}, [message?.id, message?.conversationId, message?.media?.assetId, message?.deleted, attachmentPreview]);
|
|
292
303
|
const name = message && !message.deleted ? (message.senderId === userId ? 'You' : participants?.[message.senderId]?.name ?? (state.conversation?.kind === 'direct' ? contact.name : 'Chat member')) : null;
|
|
293
304
|
const media = message && !message.deleted && message.kind !== 'text' && message.kind !== 'poll';
|
|
294
|
-
return _jsxs(View, { style: { flexDirection: 'row', alignItems: 'center', gap: 7, minWidth: 0 }, children: [media && (thumbnail ? _jsx(Image, { accessibilityLabel: "Quoted attachment thumbnail", source: { uri: thumbnail }, onError: () => setThumbnail(undefined), resizeMode: "cover", style: { width: 28, height: 28, borderRadius: 3 } }) : _jsx(MessageIcon, { name: message.kind === 'voice' ? 'mic' : message.kind, color: design.colors.accent, size: 24 })), _jsxs(Text, { numberOfLines: 2, style: { flexShrink: 1, color: ink, fontSize: 12, lineHeight: 15, opacity: .8 }, children: [name && _jsxs(Text, { style: { fontWeight: '600' }, children: [name, ": "] }), message?.deleted ?
|
|
305
|
+
return _jsxs(View, { style: { flexDirection: 'row', alignItems: 'center', gap: 7, minWidth: 0 }, children: [media && (thumbnail ? _jsx(Image, { accessibilityLabel: uiText("Quoted attachment thumbnail"), source: { uri: thumbnail }, onError: () => setThumbnail(undefined), resizeMode: "cover", style: { width: 28, height: 28, borderRadius: 3 } }) : _jsx(MessageIcon, { name: message.kind === 'voice' ? 'mic' : message.kind, color: design.colors.accent, size: 24 })), _jsxs(Text, { numberOfLines: 2, style: { flexShrink: 1, color: ink, fontSize: 12, lineHeight: 15, opacity: .8 }, children: [name && _jsxs(Text, { style: { fontWeight: '600' }, children: [name, ": "] }), message?.deleted ? uiText("Message deleted") : quoteSummary(message, uiText), message?.kind === 'voice' && message.media?.durationMs ? ` · ${duration(message.media.durationMs)}` : ''] })] });
|
|
295
306
|
}
|
|
296
307
|
/** Thumbnails stay local to the view and are discarded on conversation/message changes. */
|
|
297
308
|
const heardVoiceMessages = new WeakMap();
|
|
298
309
|
function MediaContent({ message, ink, footer, onOpen, onLongPress }) {
|
|
310
|
+
const { text: uiText, locale: uiLocale, number: uiNumber, count: uiCount } = useRobonoLocale();
|
|
299
311
|
const { attachmentPreview, components, design, userId, client } = useMessagingUI();
|
|
300
312
|
const [, updateHeard] = useState(0), voiceKey = message.conversationId + ':' + message.id + ':' + message.media?.assetId;
|
|
301
313
|
const heard = heardVoiceMessages.get(client)?.has(voiceKey) ?? false;
|
|
@@ -319,16 +331,18 @@ function MediaContent({ message, ink, footer, onOpen, onLongPress }) {
|
|
|
319
331
|
return Voice ? _jsx(Voice, { mine: message.senderId === userId, heard: heard, onPlayed: onPlayed, background: message.senderId === userId ? design.colors.outgoing : undefined, message: message, ink: ink, accent: design.colors.accent, waveform: preview.waveform ?? message.media?.waveform, showWaveform: design.showWaveform !== false, footer: footer, onOpen: onOpen, onLongPress: onLongPress }) : _jsx(VoicePlayback, { mine: message.senderId === userId, heard: heard, onPlayed: onPlayed, background: message.senderId === userId ? design.colors.outgoing : undefined, ink: ink, accent: design.colors.accent, waveform: preview.waveform ?? message.media?.waveform, showWaveform: design.showWaveform !== false, durationMs: message.media?.durationMs, footer: footer, onToggle: onOpen, onLongPress: onLongPress });
|
|
320
332
|
}
|
|
321
333
|
if (message.kind === 'image' || message.kind === 'video')
|
|
322
|
-
return _jsx(View, { style: { width: 220, maxWidth: '100%', gap: 0 }, children: _jsxs(View, { style: { height: 155, borderRadius: 10, overflow: 'hidden', alignItems: 'center', justifyContent: 'center', backgroundColor: '#00000012' }, children: [preview.uri ? _jsx(Image, { source: { uri: preview.uri }, accessibilityLabel: message.media?.fileName ?? mediaNames[message.kind], resizeMode: "cover", onError: () => setPreview(value => ({ ...value, uri: undefined })), style: { width: '100%', height: '100%' } }) : _jsx(MessageIcon, { name: message.kind, color: ink, size: 36 }), message.kind === 'video' && _jsxs(_Fragment, { children: [_jsx(View, { style: { position: 'absolute', borderRadius: 24, padding: 10, backgroundColor: '#00000099' }, children: _jsx(MessageIcon, { name: "play", color: "#ffffff" }) }), _jsx(Text, { style: { position: 'absolute', left: 8, bottom: 8, color: '#ffffff', backgroundColor: '#000000aa', borderRadius: 4, paddingHorizontal: 5, fontSize: 12 }, children: duration(message.media?.durationMs) })] })] }) });
|
|
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 ??
|
|
334
|
+
return _jsx(View, { style: { width: 220, maxWidth: '100%', gap: 0 }, children: _jsxs(View, { style: { height: 155, borderRadius: 10, overflow: 'hidden', alignItems: 'center', justifyContent: 'center', backgroundColor: '#00000012' }, children: [preview.uri ? _jsx(Image, { source: { uri: preview.uri }, accessibilityLabel: message.media?.fileName ?? uiText(mediaNames[message.kind]), resizeMode: "cover", onError: () => setPreview(value => ({ ...value, uri: undefined })), style: { width: '100%', height: '100%' } }) : _jsx(MessageIcon, { name: message.kind, color: ink, size: 36 }), message.kind === 'video' && _jsxs(_Fragment, { children: [_jsx(View, { style: { position: 'absolute', borderRadius: 24, padding: 10, backgroundColor: '#00000099' }, children: _jsx(MessageIcon, { name: "play", color: "#ffffff" }) }), _jsx(Text, { style: { position: 'absolute', left: 8, bottom: 8, color: '#ffffff', backgroundColor: '#000000aa', borderRadius: 4, paddingHorizontal: 5, fontSize: 12 }, children: duration(message.media?.durationMs) })] })] }) });
|
|
335
|
+
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 ?? uiText("File") }), _jsxs(Text, { style: { color: ink, fontSize: 12, opacity: 0.75 }, children: [message.media?.fileName?.split('.').pop()?.toUpperCase(), " \u00B7 ", fileSize(message.media?.byteSize)] })] })] });
|
|
324
336
|
}
|
|
325
|
-
function messageTimeLabel(message) { return (message.editedAt && !message.deleted ? 'edited · ' : '') + new Date(message.createdAt).toLocaleTimeString(
|
|
337
|
+
function messageTimeLabel(message, locale, text) { return (message.editedAt && !message.deleted ? text('edited') + ' · ' : '') + new Date(message.createdAt).toLocaleTimeString(locale, { hour: 'numeric', minute: '2-digit' }); }
|
|
326
338
|
function MessageMetadata({ mine, ink, pendingStatus }) {
|
|
339
|
+
const { text: uiText, locale: uiLocale, number: uiNumber, count: uiCount } = useRobonoLocale();
|
|
327
340
|
if (!mine || !pendingStatus)
|
|
328
341
|
return null;
|
|
329
|
-
return _jsx(View, { accessibilityLabel: pendingStatus === 'sending' ?
|
|
342
|
+
return _jsx(View, { accessibilityLabel: pendingStatus === 'queued' ? uiText("Waiting for connection") : pendingStatus === 'sending' ? uiText("Sending message") : uiText("Message not sent"), style: { alignSelf: 'flex-end' }, children: _jsx(MessageIcon, { name: pendingStatus === 'failed' ? 'warning' : 'clock', color: pendingStatus === 'failed' ? '#c94040' : ink, size: 15 }) });
|
|
330
343
|
}
|
|
331
344
|
export function MessagingBubble({ message, pendingStatus, previousMessage, nextMessage }) {
|
|
345
|
+
const { text: uiText, locale: uiLocale, number: uiNumber, count: uiCount } = useRobonoLocale();
|
|
332
346
|
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();
|
|
333
347
|
const extra = !pendingStatus && !message.deleted ? extrasState?.messages[message.id] : undefined;
|
|
334
348
|
const extraRef = useRef(null), socialRef = useRef(null);
|
|
@@ -379,7 +393,7 @@ export function MessagingBubble({ message, pendingStatus, previousMessage, nextM
|
|
|
379
393
|
}
|
|
380
394
|
catch {
|
|
381
395
|
if (generation === transformGeneration.current)
|
|
382
|
-
setError(
|
|
396
|
+
setError(uiText("This message could not be converted. Try again."));
|
|
383
397
|
}
|
|
384
398
|
finally {
|
|
385
399
|
if (generation === transformGeneration.current)
|
|
@@ -390,19 +404,19 @@ export function MessagingBubble({ message, pendingStatus, previousMessage, nextM
|
|
|
390
404
|
const quoteGesture = useQuoteGesture(!pendingStatus && !selection.length, mine, quoting ? () => setReply(message) : undefined, extra ? () => extraRef.current?.open() : undefined, useMessagingUI().onQuoteReady);
|
|
391
405
|
async function open() { if (pendingStatus || quoteGesture.suppressPress())
|
|
392
406
|
return; if (!openAttachment) {
|
|
393
|
-
setError(
|
|
407
|
+
setError(uiText("This attachment cannot be opened."));
|
|
394
408
|
return;
|
|
395
409
|
} setBusy(true); setError(''); try {
|
|
396
410
|
await openAttachment(message);
|
|
397
411
|
}
|
|
398
412
|
catch {
|
|
399
|
-
setError(
|
|
413
|
+
setError(uiText("The attachment could not be opened. Try again."));
|
|
400
414
|
}
|
|
401
415
|
finally {
|
|
402
416
|
setBusy(false);
|
|
403
417
|
} }
|
|
404
418
|
const menu = () => { if (pendingStatus === 'sending' || quoteGesture.suppressPress())
|
|
405
|
-
return; if (!pendingStatus && !message.deleted || canChange || pendingStatus === 'failed' || canTranscript || canTranslate || canInteract && (quoting || state.settings?.features.reactions) || message.text && (copyText || Platform.OS === 'web') || onMessageOptions || !mine && state.conversation?.safety?.canReport)
|
|
419
|
+
return; if (!pendingStatus && !message.deleted || canChange || (pendingStatus === 'failed' || pendingStatus === 'queued') || canTranscript || canTranslate || canInteract && (quoting || state.settings?.features.reactions) || message.text && (copyText || Platform.OS === 'web') || onMessageOptions || !mine && state.conversation?.safety?.canReport)
|
|
406
420
|
openMenu(); };
|
|
407
421
|
async function react(emoji) {
|
|
408
422
|
if (busy)
|
|
@@ -415,54 +429,56 @@ export function MessagingBubble({ message, pendingStatus, previousMessage, nextM
|
|
|
415
429
|
await client.react(message.id, own === emoji ? null : emoji);
|
|
416
430
|
}
|
|
417
431
|
catch {
|
|
418
|
-
setError(
|
|
432
|
+
setError(uiText("Your reaction could not be saved. Try again."));
|
|
419
433
|
}
|
|
420
434
|
finally {
|
|
421
435
|
setBusy(false);
|
|
422
436
|
}
|
|
423
437
|
}
|
|
424
|
-
const renderLinks = (text) => messageLinks(text).map((part, index) => part.url ? _jsx(Text, { accessibilityRole: "link", onLongPress: menu, onPress: event => { event.stopPropagation(); void Linking.openURL(part.url).catch(() => setError(
|
|
425
|
-
const renderText = (text) => { const address = findChatAddress(text); return address ? _jsxs(_Fragment, { children: [renderLinks(text.slice(0, address.start)), _jsx(Text, { accessibilityRole: "link", onLongPress: menu, onPress: event => { event.stopPropagation(); void openChatAddress(address.text).catch(() => setError(
|
|
426
|
-
const extraCard = extra && extras ? _jsx(MessageExtra, { ref: extraRef, addressBubbleHeight: addressBubbleHeight, extra: extra, messageId: message.id, userId: userId, extras: extras, ink: ink, accent: c.accent, background: c.background, foreground: c.foreground, Map: components?.TrackingMap, onLongPress: menu, onCall: onCallHistory, footer: _jsx(MessageMetadata, { message: message, mine: mine, ink: ink }), renderAddress: (action, people) => _jsxs(_Fragment, { children: [_jsxs(Pressable, { onLayout: event => { const h = event.nativeEvent.layout.height; setAddressBubbleHeight(old => Math.abs(old - h) < .5 ? old : h); }, testID: "address-message-bubble", accessibilityRole: "button", accessibilityLabel: "Show live tracking map", hitSlop: { top: 6, bottom: 6, left: 4, right: 4 }, onLongPress: menu, onPress: event => { event.stopPropagation(); if (!quoteGesture.suppressPress())
|
|
438
|
+
const renderLinks = (text) => messageLinks(text).map((part, index) => part.url ? _jsx(Text, { accessibilityRole: "link", onLongPress: menu, onPress: event => { event.stopPropagation(); void Linking.openURL(part.url).catch(() => setError(uiText("This link could not be opened."))); }, style: { textDecorationLine: 'underline' }, children: part.text }, index) : part.text);
|
|
439
|
+
const renderText = (text) => { const address = findChatAddress(text); return address ? _jsxs(_Fragment, { children: [renderLinks(text.slice(0, address.start)), _jsx(Text, { accessibilityRole: "link", onLongPress: menu, onPress: event => { event.stopPropagation(); void openChatAddress(address.text).catch(() => setError(uiText("Maps could not be opened."))); }, style: { color: c.accent, textDecorationLine: 'underline' }, children: address.text }), renderLinks(text.slice(address.end))] }) : renderLinks(text); };
|
|
440
|
+
const extraCard = extra && extras ? _jsx(MessageExtra, { ref: extraRef, addressBubbleHeight: addressBubbleHeight, extra: extra, messageId: message.id, userId: userId, extras: extras, ink: ink, accent: c.accent, background: c.background, foreground: c.foreground, Map: components?.TrackingMap, onLongPress: menu, onCall: onCallHistory, footer: _jsx(MessageMetadata, { message: message, mine: mine, ink: ink }), renderAddress: (action, people) => _jsxs(_Fragment, { children: [_jsxs(Pressable, { onLayout: event => { const h = event.nativeEvent.layout.height; setAddressBubbleHeight(old => Math.abs(old - h) < .5 ? old : h); }, testID: "address-message-bubble", accessibilityRole: "button", accessibilityLabel: uiText("Show live tracking map"), hitSlop: { top: 6, bottom: 6, left: 4, right: 4 }, onLongPress: menu, onPress: event => { event.stopPropagation(); if (!quoteGesture.suppressPress())
|
|
427
441
|
extraRef.current?.open(); }, style: { flexShrink: 1, minWidth: 0, minHeight: 44, justifyContent: 'center', position: 'relative', backgroundColor: mine ? c.outgoing : c.incoming, borderRadius: 8, paddingHorizontal: 6, paddingVertical: 3 }, children: [bubbleBody, people] }), action] }) }) : null;
|
|
428
442
|
const socialCard = socialReady && components?.SocialPlayer ? _jsx(SocialMessagePreview, { ref: socialRef, onLongPress: menu, message: message, client: client, Player: components.SocialPlayer, origin: socialOrigin, accent: c.accent, foreground: c.foreground, background: c.background, renderMessage: action => _jsxs(_Fragment, { children: [_jsx(View, { testID: "social-message-bubble", style: { flexShrink: 1, minWidth: 0, minHeight: 44, justifyContent: 'center', position: 'relative', backgroundColor: mine ? c.outgoing : c.incoming, borderRadius: 8, paddingHorizontal: 6, paddingVertical: 3 }, children: bubbleBody }), action] }) }) : null;
|
|
429
443
|
const canCopy = !!message.text && (!!copyText || Platform.OS === 'web');
|
|
430
|
-
const reactionPicker = !mine && canInteract && state.settings?.features.reactions && _jsx(View, { testID: "message-reaction-picker", accessibilityLabel: "Choose a reaction", style: { height: 50, borderRadius: 25, backgroundColor: '#171717', borderWidth: .5, borderColor: '#ffffff24', justifyContent: 'center', boxShadow: '0 6px 14px #0006' }, children: _jsx(ScrollView, { horizontal: true, showsHorizontalScrollIndicator: false, keyboardShouldPersistTaps: "handled", contentContainerStyle: { alignItems: 'center', paddingHorizontal: 8, gap: 4 }, children: messageReactions.map(emoji => _jsx(Pressable, { accessibilityRole: "button", accessibilityLabel:
|
|
444
|
+
const reactionPicker = !mine && canInteract && state.settings?.features.reactions && _jsx(View, { testID: "message-reaction-picker", accessibilityLabel: uiText("Choose a reaction"), style: { height: 50, borderRadius: 25, backgroundColor: '#171717', borderWidth: .5, borderColor: '#ffffff24', justifyContent: 'center', boxShadow: '0 6px 14px #0006' }, children: _jsx(ScrollView, { horizontal: true, showsHorizontalScrollIndicator: false, keyboardShouldPersistTaps: "handled", contentContainerStyle: { alignItems: 'center', paddingHorizontal: 8, gap: 4 }, children: messageReactions.map(emoji => _jsx(Pressable, { accessibilityRole: "button", accessibilityLabel: uiText("React {value1}", { value1: emoji }), accessibilityState: { selected: !!message.reactions?.some(r => r.emoji === emoji && r.userIds.includes(userId)) }, disabled: busy, onPress: () => void react(emoji), style: { width: 40, height: 38, borderRadius: 19, alignItems: 'center', justifyContent: 'center', backgroundColor: message.reactions?.some(r => r.emoji === emoji && r.userIds.includes(userId)) ? '#f0843433' : 'transparent' }, children: _jsx(Text, { style: { fontSize: 23, lineHeight: 28 }, children: emoji }) }, emoji)) }) });
|
|
431
445
|
const footer = _jsx(MessageMetadata, { message: message, mine: mine, ink: visual ? '#ffffff' : ink, pendingStatus: pendingStatus });
|
|
432
446
|
const bigEmoji = message.kind === 'text' && !message.replyTo && !extra && !canTranslate && !transformed && isBigEmojiText(message.text);
|
|
433
447
|
const showingTransform = !!transformed && !showOriginal && (transformed.kind === 'transcript' || canTranslate);
|
|
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') :
|
|
448
|
+
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') : uiText("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') : uiText("Show original") }) }), canTranscript && !transformed && _jsx(Pressable, { accessibilityRole: "button", accessibilityLabel: uiText("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 ? uiText("Transcribing…") : uiText("Transcript") }) }), canTranslate && transformed?.kind !== 'translation' && _jsx(Pressable, { accessibilityRole: "button", accessibilityLabel: translation.mode === 'voice' ? uiText("Play translation") : uiText("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' ? uiText("Play Translation") : uiText("Translate") }) })] });
|
|
435
449
|
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 });
|
|
436
|
-
const content = showingTransform ? _jsxs(View, { children: [_jsx(Text, { style: { color: ink, fontSize: 11, opacity: .65, marginBottom: 2 }, children: transformed.kind === 'transcript' ?
|
|
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 :
|
|
450
|
+
const content = showingTransform ? _jsxs(View, { children: [_jsx(Text, { style: { color: ink, fontSize: 11, opacity: .65, marginBottom: 2 }, children: transformed.kind === 'transcript' ? uiText("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;
|
|
451
|
+
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: uiText("Go to quoted message"), onLongPress: menu, onPress: () => { void client.focusMessage(message.replyTo).catch(e => setError(e instanceof Error ? e.message : uiText("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 ? uiText("Former member") : participants?.[message.senderId]?.name ?? uiText("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 : uiText('{kind}: {name}', { kind: uiText(mediaNames[message.kind]), name: message.media?.fileName ?? uiText('Open attachment') }), disabled: busy, onLongPress: menu, onPress: message.kind === 'text' ? () => { if (quoteGesture.suppressPress())
|
|
438
452
|
return; if (extra?.address)
|
|
439
453
|
extraRef.current?.open();
|
|
440
454
|
else
|
|
441
|
-
socialRef.current?.open(); } : () => void open(), children: content }), !discussionPreview && state.discussionAvailable && !!message.replyCount && _jsx(Pressable, { accessibilityRole: "button", accessibilityLabel: message.replyCount + ' ' + (message.replyCount === 1 ? 'reply' : 'replies'), onPress: () => openDiscussion(message.id), style: { minHeight: 32, justifyContent: 'center' }, children: _jsxs(Text, { style: { color: c.accent, fontSize: 12, fontWeight: '600' }, children: [message.replyCount, " ", message.replyCount === 1 ?
|
|
442
|
-
return _jsxs(View, { ...(Platform.OS === 'web' ? { onMouseEnter: () => setHovered(true), onMouseLeave: () => setHovered(false), onContextMenu: (event) => { event.preventDefault(); menu(); } } : {}), style: { width: '100%', position: 'relative', marginTop: message.reactions?.length ? 24 : 8, marginBottom: 0 }, children: [quoteGesture.infoLoaded && _jsxs(Animated.View, { pointerEvents: "none", testID: "message-info-reveal", accessible: false, accessibilityElementsHidden: true, importantForAccessibility: "no-hide-descendants", style: { ...quoteGesture.revealWebStyle, position: 'absolute', top: '50%', marginTop: -26, minHeight: 52, width: messageInfoRevealWidth, justifyContent: 'center', ...(mine ? { start: 0, alignItems: 'flex-start' } : { end: 0, alignItems: 'flex-end' }), opacity: quoteGesture.infoOpacity }, children: [_jsxs(View, { style: { flexDirection: 'row', alignItems: 'baseline', gap: 4 }, children: [_jsx(Text, { style: { color: c.secondary, fontSize: 8, lineHeight: 11, fontWeight: '900' }, children: "SENT:" }), _jsx(Text, { style: { color: c.foreground, fontSize: 10, lineHeight: 12, fontWeight: '800' }, children: messageTimeLabel(message) })] }), mine && state.settings?.features.readReceipts && _jsxs(View, { style: { flexDirection: 'row', alignItems: 'baseline', gap: 4 }, children: [_jsx(Text, { style: { color: c.secondary, fontSize: 8, lineHeight: 11, fontWeight: '900' }, children: "READ:" }), _jsx(Text, { style: { color: message.readAt ? c.foreground : c.secondary, fontSize: 10, lineHeight: 12, fontWeight: '800' }, children: message.readAt ? new Date(message.readAt).toLocaleTimeString(
|
|
443
|
-
quoteGesture.webRef.current = node; }, ...quoteGesture.handlers, accessibilityActions: quoting ? [{ name: "quote", label: "Quote message" }] : undefined, onAccessibilityAction: event => { if (quoting && event.nativeEvent.actionName === "quote")
|
|
455
|
+
socialRef.current?.open(); } : () => void open(), children: content }), !discussionPreview && state.discussionAvailable && !!message.replyCount && _jsx(Pressable, { accessibilityRole: "button", accessibilityLabel: message.replyCount + ' ' + (message.replyCount === 1 ? 'reply' : 'replies'), onPress: () => openDiscussion(message.id), style: { minHeight: 32, justifyContent: 'center' }, children: _jsxs(Text, { style: { color: c.accent, fontSize: 12, fontWeight: '600' }, children: [message.replyCount, " ", message.replyCount === 1 ? uiText("reply") : uiText("replies")] }) }), message.kind !== 'text' && !!message.text && _jsx(Text, { selectable: true, onLongPress: menu, style: { color: ink, fontSize: 16, lineHeight: 22, marginTop: 4 }, children: renderText(message.text) }), message.kind !== 'text' && message.kind !== 'poll' && !voice && !visual && _jsx(View, { style: { marginTop: 4, alignSelf: 'flex-end' }, children: footer }), (!voice || showingTransform) && languageActions, transforming && !voice && _jsx(Text, { accessibilityLiveRegion: "polite", style: { color: ink, fontSize: 11 }, children: uiText("Converting…") }), busy && !voice && _jsx(Text, { style: { color: ink, fontSize: 12, marginTop: 4 }, children: uiText("Opening…") }), !!error && _jsxs(MessagingPopup, { compact: true, contentWidth: 640, title: uiText("Message"), foreground: c.foreground, background: chatSurface(c.background), onClose: () => setError(''), children: [_jsx(Text, { accessibilityRole: "alert", style: { color: messagingPopupColors.foreground, fontSize: 15, lineHeight: 21, marginTop: 8 }, children: error }), _jsx(Pressable, { accessibilityRole: "button", accessibilityLabel: uiText("Close message alert"), onPress: () => setError(''), style: { alignSelf: 'flex-end', minHeight: 44, justifyContent: 'center', paddingHorizontal: 8, marginTop: 8 }, children: _jsx(Text, { style: { color: c.accent }, children: uiText("OK") }) })] }), pendingStatus === 'failed' && _jsx(Pressable, { accessibilityRole: "button", accessibilityLabel: uiText("Retry sending message"), onPress: () => void client.retry(message.clientMessageId).catch(() => { }), style: { minHeight: 32, justifyContent: 'center', alignSelf: 'flex-end' }, children: _jsx(Text, { style: { color: c.accent, fontSize: 12 }, children: design.labels.retry }) }), Platform.OS === 'web' && !mine && canInteract && state.settings?.features.reactions && _jsx(Pressable, { accessibilityRole: "button", accessibilityLabel: uiText("React to message"), onFocus: () => setHovered(true), onBlur: () => setHovered(false), onPress: openMenu, style: { opacity: hovered || actions ? 1 : 0, position: 'absolute', top: 6, ...(mine ? { left: -34 } : { right: -34 }), width: 32, height: 32, alignItems: 'center', justifyContent: 'center' }, children: _jsx(MessageIcon, { name: "reaction", color: c.secondary, size: 20 }) }), !!message.reactions?.length && _jsx(View, { testID: "message-reaction-badge", pointerEvents: "none", accessibilityLabel: message.reactions.slice(-3).map(r => r.emoji).join(' '), style: { position: 'absolute', zIndex: 41, top: -16, ...(mine ? { end: -9 } : { start: -9 }), minWidth: 30, height: 20, alignItems: 'center', justifyContent: 'center' }, children: _jsx(Text, { style: { fontSize: 16, lineHeight: 20 }, children: message.reactions.slice(-3).map(r => r.emoji).join(' ') }) }), _jsxs(MessageMenu, { visible: actions, position: menuPosition, screenHeight: viewport.height, mine: mine, reactions: reactionPicker, onClose: () => setActions(false), onDismiss: menuDismissed, children: [quoting && _jsx(MessageMenuRow, { icon: "quote", label: uiText("Quote message"), onPress: () => { setActions(false); setReply(message); } }), !discussionPreview && state.discussionAvailable && !pendingStatus && !message.deleted && _jsx(MessageMenuRow, { icon: "quote", label: uiText("Replies"), onPress: () => leaveMenu(() => openDiscussion(message.threadRoot ?? message.id)) }), state.discussionAvailable && canInteract && state.conversation?.ownerId === userId && _jsx(MessageMenuRow, { icon: "bookmark", label: message.pinned ? uiText("Unpin for group") : uiText("Pin for group"), onPress: () => { setActions(false); void client.pinMessage(message.id, !message.pinned).catch(e => setError(e instanceof Error ? e.message : uiText("The pin could not change."))); } }), canCopy && _jsx(MessageMenuRow, { icon: "copy", label: uiText("Copy"), onPress: () => { setActions(false); void (copyText ? copyText(message.text) : navigator.clipboard.writeText(message.text)).catch(() => setError(uiText("The text could not be copied."))); } }), canInteract && client.canForward(message) && _jsx(MessageMenuRow, { icon: "redo", label: uiText("Forward"), onPress: () => leaveMenu(() => setForwarding([message])) }), !pendingStatus && !message.deleted && message.kind !== 'poll' && client.hasSavedMessages() && _jsx(MessageMenuRow, { icon: "bookmark", label: saved.some(r => r.key === savedMessageKey(message)) ? uiText("Remove from Saved") : uiText("Save"), disabled: saving, onPress: () => { setActions(false); void toggleSaved(message); } }), canChange && message.kind !== 'poll' && state.settings?.features[message.kind] && _jsx(MessageMenuRow, { icon: "edit", label: uiText("Edit"), onPress: () => { setActions(false); setEditing(message); } }), !mine && state.conversation?.safety?.canReport && _jsx(MessageMenuRow, { icon: "flag", label: uiText("Report"), danger: true, onPress: () => leaveMenu(() => setReportOpen(true)) }), canChange && _jsx(MessageMenuRow, { icon: "trash", label: uiText("Delete"), danger: true, onPress: () => leaveMenu(() => setDeleting([message])) }), (pendingStatus === 'failed' || pendingStatus === 'queued') && _jsx(MessageMenuRow, { icon: "trash", label: uiText("Remove unsent message"), danger: true, onPress: () => { setActions(false); void client.discardPending(message.clientMessageId).catch(() => setError(uiText("The unsent message could not be removed."))); } }), !pendingStatus && onMessageOptions && _jsx(MessageMenuRow, { icon: "more", label: uiText("More options"), onPress: () => leaveMenu(() => onMessageOptions(message)) }), !pendingStatus && !message.deleted && _jsx(MessageMenuRow, { icon: "check", label: uiText("Select Multiple"), divider: true, onPress: () => { setActions(false); setSelection([message]); Keyboard.dismiss(); } })] }), reportOpen && _jsx(MessagingSafety, { client: client, state: state, appearance: design, name: participants?.[message.senderId]?.name ?? 'Contact', messageId: message.id, onClose: () => setReportOpen(false) })] });
|
|
456
|
+
return _jsxs(View, { ...(Platform.OS === 'web' ? { onMouseEnter: () => setHovered(true), onMouseLeave: () => setHovered(false), onContextMenu: (event) => { event.preventDefault(); menu(); } } : {}), style: { width: '100%', position: 'relative', marginTop: message.reactions?.length ? 24 : 8, marginBottom: 0 }, children: [quoteGesture.infoLoaded && _jsxs(Animated.View, { pointerEvents: "none", testID: "message-info-reveal", accessible: false, accessibilityElementsHidden: true, importantForAccessibility: "no-hide-descendants", style: { ...quoteGesture.revealWebStyle, position: 'absolute', top: '50%', marginTop: -26, minHeight: 52, width: messageInfoRevealWidth, justifyContent: 'center', ...(mine ? { start: 0, alignItems: 'flex-start' } : { end: 0, alignItems: 'flex-end' }), opacity: quoteGesture.infoOpacity }, children: [_jsxs(View, { style: { flexDirection: 'row', alignItems: 'baseline', gap: 4 }, children: [_jsx(Text, { style: { color: c.secondary, fontSize: 8, lineHeight: 11, fontWeight: '900' }, children: uiText("SENT:") }), _jsx(Text, { style: { color: c.foreground, fontSize: 10, lineHeight: 12, fontWeight: '800' }, children: messageTimeLabel(message, uiLocale, uiText) })] }), mine && state.settings?.features.readReceipts && _jsxs(View, { style: { flexDirection: 'row', alignItems: 'baseline', gap: 4 }, children: [_jsx(Text, { style: { color: c.secondary, fontSize: 8, lineHeight: 11, fontWeight: '900' }, children: uiText("READ:") }), _jsx(Text, { style: { color: message.readAt ? c.foreground : c.secondary, fontSize: 10, lineHeight: 12, fontWeight: '800' }, children: message.readAt ? new Date(message.readAt).toLocaleTimeString(uiLocale, { hour: 'numeric', minute: '2-digit' }) : uiText("Not yet") })] })] }), quoting && _jsxs(Animated.View, { pointerEvents: "none", ...(Platform.OS === 'web' ? { 'aria-hidden': true } : { accessible: false, accessibilityElementsHidden: true, importantForAccessibility: 'no-hide-descendants' }), testID: `quote-reveal-${mine ? 'outgoing' : 'incoming'}`, style: { ...quoteGesture.revealWebStyle, position: 'absolute', top: '50%', marginTop: -26, ...(mine ? { end: 0 } : { start: 0 }), width: messageReplyRevealWidth, minHeight: 52, gap: 3, alignItems: 'center', justifyContent: 'center', opacity: quoteGesture.revealOpacity }, children: [_jsx(View, { style: { width: 40, height: 40, borderRadius: 20, alignItems: 'center', justifyContent: 'center', backgroundColor: chatSurface(c.background), borderWidth: 1.5, borderColor: c.secondary + '40' }, children: _jsx(MessageIcon, { name: "quote", color: c.accent, size: 30 }) }), _jsx(Text, { style: { color: c.secondary, fontSize: 10, fontWeight: '900' }, children: uiText("QUOTE MESSAGE") })] }), _jsx(Animated.View, { ref: node => { anchor.current = node; if (quoteGesture.webRef)
|
|
457
|
+
quoteGesture.webRef.current = node; }, ...quoteGesture.handlers, accessibilityActions: quoting ? [{ name: "quote", label: uiText("Quote message") }] : undefined, onAccessibilityAction: event => { if (quoting && event.nativeEvent.actionName === "quote")
|
|
444
458
|
setReply(message); }, testID: `message-${message.kind}-${mine ? 'outgoing' : 'incoming'}`, style: { ...quoteGesture.webStyle, transform: [{ translateX: quoteGesture.offset }], ...(Platform.OS === "web" && !pendingStatus ? { touchAction: "pan-y", userSelect: "none" } : {}), alignSelf: mine ? 'flex-end' : 'flex-start', maxWidth: '88%', backgroundColor: mine ? c.outgoing : c.incoming, borderRadius: 8, padding: visual ? 3 : 6, paddingBottom: visual || canTranscript || canTranslate ? 3 : 6, boxShadow: bigEmoji ? undefined : '0 1px 1px #00000010', ...(voice && !message.text && !showingTransform ? { backgroundColor: 'transparent', padding: 0, paddingBottom: 0, boxShadow: undefined } : {}), ...(message.kind === 'poll' ? { backgroundColor: chatSurface(c.background), borderWidth: 1, borderColor: c.accent + '60', paddingHorizontal: 8, paddingTop: 4, paddingBottom: 2 } : {}), ...(bigEmoji ? { backgroundColor: 'transparent', paddingHorizontal: 4, paddingTop: 0, paddingBottom: 0 } : {}), ...(extra?.address || socialReady ? { flexDirection: mine ? 'row-reverse' : 'row', alignItems: 'center', gap: 8, padding: 0, paddingBottom: 0, backgroundColor: 'transparent', boxShadow: undefined } : {}) }, children: extra?.address ? extraCard : socialReady ? socialCard : bubbleBody })] });
|
|
445
459
|
}
|
|
446
460
|
function MessageDeletion({ messages, onClose }) {
|
|
461
|
+
const { text: uiText, locale: uiLocale, number: uiNumber, count: uiCount } = useRobonoLocale();
|
|
447
462
|
const { client, design } = useMessagingUI(), [busy, setBusy] = useState(false), [error, setError] = useState('');
|
|
448
463
|
useEffect(() => setError(''), [messages]);
|
|
449
464
|
if (!messages.length)
|
|
450
465
|
return null;
|
|
451
466
|
const c = design.colors;
|
|
452
|
-
return _jsxs(MessagingPopup, { compact: true, contentWidth: 640, title:
|
|
453
|
-
onClose(); }, children: [_jsx(Text, { style: { color: messagingPopupColors.secondary, marginTop: 8 }, children: "Delete for everyone? Only messages that have not been read, heard or opened can be deleted." }), !!error && _jsx(Text, { accessibilityRole: "alert", style: { color: messagingPopupColors.foreground, marginTop: 8 }, children: error }), _jsxs(View, { style: { flexDirection: 'row', gap: 8, marginTop: 12 }, children: [_jsx(Pressable, { accessibilityRole: "button", accessibilityLabel: error ?
|
|
467
|
+
return _jsxs(MessagingPopup, { compact: true, contentWidth: 640, title: uiCount(messages.length, "Delete message?", "Delete {count} messages?"), foreground: c.foreground, closeDisabled: busy, background: chatSurface(c.background), onClose: () => { if (!busy)
|
|
468
|
+
onClose(); }, children: [_jsx(Text, { style: { color: messagingPopupColors.secondary, marginTop: 8 }, children: uiText("Delete for everyone? Only messages that have not been read, heard or opened can be deleted.") }), !!error && _jsx(Text, { accessibilityRole: "alert", style: { color: messagingPopupColors.foreground, marginTop: 8 }, children: error }), _jsxs(View, { style: { flexDirection: 'row', gap: 8, marginTop: 12 }, children: [_jsx(Pressable, { accessibilityRole: "button", accessibilityLabel: error ? uiText("Close message alert") : undefined, disabled: busy, onPress: onClose, 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: error ? uiText("OK") : uiText("Cancel") }) }), !error && _jsx(Pressable, { accessibilityRole: "button", accessibilityLabel: uiText("Delete for everyone"), disabled: busy, onPress: () => { setBusy(true); void (async () => { try {
|
|
454
469
|
for (const message of messages)
|
|
455
470
|
await client.deleteMessage(message.id, message.revision ?? 0);
|
|
456
471
|
onClose();
|
|
457
472
|
}
|
|
458
473
|
catch (e) {
|
|
459
|
-
setError(e instanceof Error ? e.message :
|
|
474
|
+
setError(e instanceof Error ? e.message : uiText("This message could not be deleted."));
|
|
460
475
|
}
|
|
461
476
|
finally {
|
|
462
477
|
setBusy(false);
|
|
463
|
-
} })(); }, 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" }) })] })] });
|
|
478
|
+
} })(); }, style: { flex: 1, minHeight: 44, borderRadius: 12, backgroundColor: '#b72d34', alignItems: 'center', justifyContent: 'center' }, children: _jsx(Text, { style: { color: '#ffffff', fontSize: 16, fontWeight: '600' }, children: uiText("Delete") }) })] })] });
|
|
464
479
|
}
|
|
465
480
|
function MessageSelection() {
|
|
481
|
+
const { text: uiText, locale: uiLocale, number: uiNumber, count: uiCount } = useRobonoLocale();
|
|
466
482
|
const { selection, setSelection, setForwarding, setDeleting, setEditing, state, client, design, userId, copyText, saved, saving, toggleSaved } = useMessagingUI();
|
|
467
483
|
const [error, setError] = useState('');
|
|
468
484
|
if (!selection.length)
|
|
@@ -470,9 +486,10 @@ function MessageSelection() {
|
|
|
470
486
|
const c = design.colors;
|
|
471
487
|
const ordered = orderChatMessages([...selection].sort((a, b) => BigInt(a.id) < BigInt(b.id) ? -1 : BigInt(a.id) > BigInt(b.id) ? 1 : 0)), enabled = !!state.settings?.enabled && state.conversation?.canSend !== false;
|
|
472
488
|
const forward = enabled && ordered.every(m => client.canForward(m)), remove = enabled && state.messageActionsAvailable && ordered.every(m => m.senderId === userId && !m.readAt && !m.deleted);
|
|
473
|
-
return _jsxs(View, { accessibilityLabel: "Selected message actions", style: { flexDirection: 'row', alignItems: 'center', minHeight: 48, paddingHorizontal: 4, gap: 0, backgroundColor: c.background, borderTopWidth: 1, borderTopColor: c.secondary + '25' }, children: [_jsx(Pressable, { accessibilityRole: "button", accessibilityLabel: "Clear message selection", onPress: () => setSelection([]), style: { minWidth: 44, minHeight: 44, alignItems: 'center', justifyContent: 'center' }, children: _jsx(MessageIcon, { name: "close", color: c.foreground }) }), _jsx(Text, { accessibilityLiveRegion: "polite", accessibilityLabel:
|
|
489
|
+
return _jsxs(View, { accessibilityLabel: uiText("Selected message actions"), style: { flexDirection: 'row', alignItems: 'center', minHeight: 48, paddingHorizontal: 4, gap: 0, backgroundColor: c.background, borderTopWidth: 1, borderTopColor: c.secondary + '25' }, children: [_jsx(Pressable, { accessibilityRole: "button", accessibilityLabel: uiText("Clear message selection"), onPress: () => setSelection([]), style: { minWidth: 44, minHeight: 44, alignItems: 'center', justifyContent: 'center' }, children: _jsx(MessageIcon, { name: "close", color: c.foreground }) }), _jsx(Text, { accessibilityLiveRegion: "polite", accessibilityLabel: uiText('{count} selected', { count: selection.length }), style: { flex: 1, color: c.foreground, fontSize: 15 }, children: selection.length }), selection.length === 1 && !!ordered[0].text && (copyText || Platform.OS === 'web') && _jsx(Pressable, { accessibilityRole: "button", accessibilityLabel: uiText("Copy selected message text"), onPress: () => { void (copyText ? copyText(ordered[0].text) : navigator.clipboard.writeText(ordered[0].text)).then(() => setSelection([])).catch(() => setError(uiText("The text could not be copied."))); }, style: { minWidth: 44, minHeight: 44, alignItems: 'center', justifyContent: 'center' }, children: _jsx(MessageIcon, { name: "copy", color: c.accent }) }), selection.length === 1 && client.hasSavedMessages() && ordered[0].kind !== 'poll' && _jsx(Pressable, { accessibilityRole: "button", accessibilityLabel: saved.some(r => r.key === savedMessageKey(ordered[0])) ? uiText("Remove selected message from Saved") : uiText("Save selected message"), disabled: saving, onPress: () => void toggleSaved(ordered[0]), style: { minWidth: 44, minHeight: 44, alignItems: 'center', justifyContent: 'center' }, children: _jsx(MessageIcon, { name: "bookmark", color: c.accent }) }), forward && _jsx(Pressable, { accessibilityRole: "button", accessibilityLabel: uiText("Forward selected messages"), onPress: () => { setForwarding(ordered); setSelection([]); }, style: { minWidth: 44, minHeight: 44, alignItems: 'center', justifyContent: 'center' }, children: _jsx(MessageIcon, { name: "forward", color: c.accent }) }), remove && selection.length === 1 && ordered[0].kind !== 'poll' && _jsx(Pressable, { accessibilityRole: "button", accessibilityLabel: uiText("Edit selected message"), onPress: () => { setEditing(ordered[0]); setSelection([]); }, style: { minWidth: 44, minHeight: 44, alignItems: 'center', justifyContent: 'center' }, children: _jsx(MessageIcon, { name: "edit", color: c.accent }) }), remove && _jsx(Pressable, { accessibilityRole: "button", accessibilityLabel: uiText("Delete selected messages"), onPress: () => { setDeleting(ordered); setSelection([]); }, style: { minWidth: 44, minHeight: 44, alignItems: 'center', justifyContent: 'center' }, children: _jsx(MessageIcon, { name: "trash", color: c.accent }) }), !!error && _jsxs(MessagingPopup, { compact: true, contentWidth: 640, title: uiText("Message"), foreground: c.foreground, background: c.background, onClose: () => setError(''), children: [_jsx(Text, { accessibilityRole: "alert", style: { color: messagingPopupColors.foreground }, children: error }), _jsx(Pressable, { accessibilityRole: "button", onPress: () => setError(''), style: { minHeight: 44, alignSelf: 'flex-end', justifyContent: 'center' }, children: _jsx(Text, { style: { color: c.accent }, children: uiText("OK") }) })] })] });
|
|
474
490
|
}
|
|
475
491
|
export function MessagingList() {
|
|
492
|
+
const { text: uiText, locale: uiLocale, number: uiNumber, count: uiCount } = useRobonoLocale();
|
|
476
493
|
const context = useMessagingUI();
|
|
477
494
|
const { state, client, components, design, userId, selection, setSelection } = context;
|
|
478
495
|
const Privacy = components?.PrivacyLayer ?? MessagingPrivacyLayer;
|
|
@@ -572,12 +589,12 @@ export function MessagingList() {
|
|
|
572
589
|
const date = new Date(item.createdAt), start = !previous || date.toDateString() !== new Date(previous.createdAt).toDateString();
|
|
573
590
|
const checked = selection.some(m => m.id === item.id), selectable = selection.length > 0 && !item.pendingStatus && !item.deleted;
|
|
574
591
|
const toggle = () => setSelection(checked ? selection.filter(m => m.id !== item.id) : [...selection, item]);
|
|
575
|
-
return _jsxs(View, { testID: highlight === item.id ? 'focused-message' : undefined, style: highlight === item.id || checked ? { backgroundColor: design.colors.accent + '20', borderRadius: 8 } : undefined, children: [start && _jsx(Text, { style: { alignSelf: 'center', fontSize: 12, color: design.colors.secondary, backgroundColor: chatSurface(design.colors.background), borderRadius: 7, paddingHorizontal: 12, paddingVertical: 5, marginTop: 4, marginBottom: 10 }, children: date.toDateString() === new Date().toDateString() ?
|
|
592
|
+
return _jsxs(View, { testID: highlight === item.id ? 'focused-message' : undefined, style: highlight === item.id || checked ? { backgroundColor: design.colors.accent + '20', borderRadius: 8 } : undefined, children: [start && _jsx(Text, { style: { alignSelf: 'center', fontSize: 12, color: design.colors.secondary, backgroundColor: chatSurface(design.colors.background), borderRadius: 7, paddingHorizontal: 12, paddingVertical: 5, marginTop: 4, marginBottom: 10 }, children: date.toDateString() === new Date().toDateString() ? uiText("Today") : date.toLocaleDateString(uiLocale, { month: 'short', day: 'numeric' }) }), _jsx(View, { style: { paddingLeft: selection.length ? 32 : 0 }, pointerEvents: selection.length ? 'none' : 'auto', accessibilityElementsHidden: selection.length > 0, importantForAccessibility: selection.length ? 'no-hide-descendants' : 'auto', ...(Platform.OS === 'web' && selection.length ? { 'aria-hidden': true } : {}), children: components?.Message ? _jsx(components.Message, { message: item }) : _jsx(MessagingBubble, { message: item, pendingStatus: item.pendingStatus, previousMessage: previous, nextMessage: items[index - 1] }) }), selectable && _jsx(Pressable, { accessibilityRole: "checkbox", accessibilityLabel: uiText('Select message: {message}', { message: item.text || item.media?.fileName || uiText(mediaNames[item.kind] || 'Message') }), accessibilityState: { checked }, onPress: toggle, style: { position: 'absolute', inset: 0, justifyContent: 'center' }, children: _jsx(View, { style: { width: 24, height: 24, borderRadius: 12, borderWidth: 1, borderColor: design.colors.accent, backgroundColor: checked ? design.colors.accent : design.colors.background, alignItems: 'center', justifyContent: 'center' }, children: checked && _jsx(MessageIcon, { name: "check", size: 16, color: "#ffffff" }) }) })] });
|
|
576
593
|
}, onEndReached: () => {
|
|
577
594
|
if (state.hasMore && !state.loading && !state.historyError) {
|
|
578
595
|
void client.loadOlder().catch(() => { });
|
|
579
596
|
}
|
|
580
|
-
}, onEndReachedThreshold: 0.2, ListHeaderComponent: state.historyWindow && state.hasNewer ? _jsx(Pressable, { accessibilityRole: "button", accessibilityLabel: "Load newer messages", disabled: state.loading, onPress: () => void client.loadNewer().catch(() => { }), style: { padding: 10, minHeight: 44, alignItems: 'center' }, children: _jsx(Text, { style: { color: design.colors.accent }, children: "Load newer messages" }) }) : null, ListFooterComponent: _jsxs(_Fragment, { children: [state.messages.length > 0 && (state.loading || state.historyError || state.hasMore) ? _jsx(View, { style: { padding: 10, alignItems: 'center' }, children: state.loading ? _jsx(ActivityIndicator, { accessibilityLabel: "Loading earlier messages", color: design.colors.accent }) : _jsx(Pressable, { accessibilityRole: "button", accessibilityLabel: state.historyError ?
|
|
597
|
+
}, onEndReachedThreshold: 0.2, ListHeaderComponent: state.historyWindow && state.hasNewer ? _jsx(Pressable, { accessibilityRole: "button", accessibilityLabel: uiText("Load newer messages"), disabled: state.loading, onPress: () => void client.loadNewer().catch(() => { }), style: { padding: 10, minHeight: 44, alignItems: 'center' }, children: _jsx(Text, { style: { color: design.colors.accent }, children: uiText("Load newer messages") }) }) : null, ListFooterComponent: _jsxs(_Fragment, { children: [state.messages.length > 0 && (state.loading || state.historyError || state.hasMore) ? _jsx(View, { style: { padding: 10, alignItems: 'center' }, children: state.loading ? _jsx(ActivityIndicator, { accessibilityLabel: uiText("Loading earlier messages"), color: design.colors.accent }) : _jsx(Pressable, { accessibilityRole: "button", accessibilityLabel: state.historyError ? uiText("Retry earlier messages") : uiText("Load earlier messages"), onPress: () => void client.loadOlder().catch(() => { }), style: { minHeight: 44, justifyContent: 'center' }, children: _jsx(Text, { style: { color: design.colors.accent }, children: state.historyError ? uiText("Earlier messages could not load. Retry") : uiText("Load earlier messages") }) }) }) : null, context.extrasState?.privacyLens && _jsx(View, { testID: "privacy-history-spacer", style: { height: Math.max(0, historyHeight - privacyReadingStripHeight) } })] }), ListEmptyComponent: _jsx(Text, { style: { color: design.colors.secondary }, children: state.loading ? uiText("Loading messages…") : design.labels.empty }) }, context.extrasState?.privacyLens ? "privacy-reading" : "regular-history") }), (away || state.historyWindow) && _jsx(Animated.View, { testID: "latest-message-control", pointerEvents: showLatest ? 'auto' : 'none', accessibilityElementsHidden: !showLatest, importantForAccessibility: showLatest ? 'auto' : 'no-hide-descendants', ...(Platform.OS === 'web' ? { 'aria-hidden': !showLatest } : {}), style: { position: 'absolute', left: '50%', marginLeft: -22, bottom: 8, opacity: Platform.OS === 'web' ? (showLatest ? 1 : 0) : latestOpacity, ...(Platform.OS === 'web' ? { transitionProperty: 'opacity', transitionDuration: '200ms' } : {}) }, children: _jsx(Pressable, { accessibilityRole: "button", accessibilityLabel: newMessages ? uiText("New messages. Go to latest") : uiText("Go to latest message"), disabled: !showLatest, onPress: () => jumpToLatest(), style: { width: 44, height: 44, borderRadius: 22, borderWidth: 1.5, borderColor: design.colors.accent, backgroundColor: chatSurface(design.colors.background), justifyContent: 'center', alignItems: 'center' }, children: _jsx(MessageIcon, { name: "arrow-down", color: design.colors.accent, size: 26 }) }) }), _jsx(MessageSelection, {})] }));
|
|
581
598
|
}
|
|
582
599
|
/** One horizontal media row on phones and in the browser preview. */
|
|
583
600
|
function AttachmentStrip({ children, label, height, recent = false }) {
|
|
@@ -604,6 +621,7 @@ function AttachmentStrip({ children, label, height, recent = false }) {
|
|
|
604
621
|
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 });
|
|
605
622
|
}
|
|
606
623
|
export function MessagingComposer() {
|
|
624
|
+
const { text: uiText, locale: uiLocale, number: uiNumber, count: uiCount, direction: uiDirection } = useRobonoLocale();
|
|
607
625
|
const weatherAllowed = !!useMessagingUI().extrasState?.available.weather;
|
|
608
626
|
const { state, client, design, extras, pickAttachment, attachmentChoices, attachmentPreview, releaseAttachment, prepareAttachment, retainDraftAttachments, reply, setReply, editing, setEditing, components, openAttachment, userId, participants } = useMessagingUI();
|
|
609
627
|
const VoiceComposer = components?.VoiceComposer ?? VoiceMessageComposer;
|
|
@@ -627,7 +645,7 @@ export function MessagingComposer() {
|
|
|
627
645
|
setMentionMembers(page.memberIds ?? page.members.map(m => m.userId));
|
|
628
646
|
setMentionError('');
|
|
629
647
|
} }).catch(() => { if (live)
|
|
630
|
-
setMentionError(
|
|
648
|
+
setMentionError(uiText("Group members could not load.")); });
|
|
631
649
|
return () => { live = false; };
|
|
632
650
|
}, [client, state.conversationId, mentionQuery !== undefined]);
|
|
633
651
|
const mentionSuggestions = mentionQuery === undefined ? [] : mentionMembers.filter(id => id !== userId && participants?.[id]?.name.toLocaleLowerCase().includes(mentionQuery.toLocaleLowerCase())).slice(0, 6);
|
|
@@ -736,7 +754,7 @@ export function MessagingComposer() {
|
|
|
736
754
|
}
|
|
737
755
|
catch (e) {
|
|
738
756
|
if (client.getSnapshot().conversationId === current.conversationId)
|
|
739
|
-
setError(e instanceof Error ? e.message :
|
|
757
|
+
setError(e instanceof Error ? e.message : uiText("This message could not be changed."));
|
|
740
758
|
}
|
|
741
759
|
finally {
|
|
742
760
|
inFlight.current = false;
|
|
@@ -768,7 +786,7 @@ export function MessagingComposer() {
|
|
|
768
786
|
void releaseAttachment?.(content.media);
|
|
769
787
|
}));
|
|
770
788
|
if (client.getSnapshot().conversationId === conversationId && results.some(result => result.status === 'rejected') && !client.getSnapshot().pending.some(item => item.status === 'failed'))
|
|
771
|
-
setError(
|
|
789
|
+
setError(uiText("The message could not be prepared. Please try again."));
|
|
772
790
|
}
|
|
773
791
|
async function schedule(time) {
|
|
774
792
|
if (inFlight.current)
|
|
@@ -817,7 +835,7 @@ export function MessagingComposer() {
|
|
|
817
835
|
// and original time, even after that time passes, to avoid a second delivery.
|
|
818
836
|
if (!requested || e instanceof MessagingError && e.status >= 400 && e.status < 500 && ![408, 409, 429].includes(e.status))
|
|
819
837
|
scheduleAttempt.current = null;
|
|
820
|
-
setScheduleError(e instanceof Error ? e.message :
|
|
838
|
+
setScheduleError(e instanceof Error ? e.message : uiText("This message could not be scheduled."));
|
|
821
839
|
}
|
|
822
840
|
}
|
|
823
841
|
finally {
|
|
@@ -827,7 +845,7 @@ export function MessagingComposer() {
|
|
|
827
845
|
}
|
|
828
846
|
async function pick(kind, source, attachmentId, intent = "send", mixedLibrary = false) {
|
|
829
847
|
if (!editing && selectedRef.current.length >= 10) {
|
|
830
|
-
setError(
|
|
848
|
+
setError(uiText("Choose up to 10 attachments per message."));
|
|
831
849
|
return;
|
|
832
850
|
}
|
|
833
851
|
if (intent === 'send')
|
|
@@ -835,7 +853,7 @@ export function MessagingComposer() {
|
|
|
835
853
|
if (inFlight.current)
|
|
836
854
|
return;
|
|
837
855
|
if (!pickAttachment) {
|
|
838
|
-
setError("Attachments are unavailable.");
|
|
856
|
+
setError(uiText("Attachments are unavailable."));
|
|
839
857
|
return;
|
|
840
858
|
}
|
|
841
859
|
const conversationId = state.conversationId;
|
|
@@ -888,7 +906,7 @@ export function MessagingComposer() {
|
|
|
888
906
|
}
|
|
889
907
|
}
|
|
890
908
|
catch (e) {
|
|
891
|
-
setError(e instanceof Error ? e.message : "The attachment could not be sent.");
|
|
909
|
+
setError(e instanceof Error ? e.message : uiText("The attachment could not be sent."));
|
|
892
910
|
}
|
|
893
911
|
finally {
|
|
894
912
|
if (recording) {
|
|
@@ -904,11 +922,11 @@ export function MessagingComposer() {
|
|
|
904
922
|
function removeSelected(id) { const item = selectedRef.current.find(item => item.id === id); setSelected(items => items.filter(item => item.id !== id)); if (item && (!editing || item.media.assetId !== editing.media?.assetId))
|
|
905
923
|
void releaseAttachment?.(item.media); }
|
|
906
924
|
function renderAttachments(voiceReview, blocked = busy) {
|
|
907
|
-
return selected.length > 0 || voiceReview ? _jsxs(AttachmentStrip, { label: "Pending attachments", height: selected.some(item => item.kind === 'document') ? 104 : 86, children: [selected.map(item => item.kind === 'voice' && item.media.assetId !== editing?.media?.assetId && components?.VoiceAttachment ? _jsx(components.VoiceAttachment, { media: item.media, appearance: design, disabled: blocked, onRemove: () => removeSelected(item.id) }, item.id) : _jsxs(View, { style: { width: 88, paddingTop: 8, paddingRight: 10 }, children: [_jsxs(Pressable, { accessibilityRole: "button", accessibilityLabel: 'Preview '
|
|
925
|
+
return selected.length > 0 || voiceReview ? _jsxs(AttachmentStrip, { label: uiText("Pending attachments"), height: selected.some(item => item.kind === 'document') ? 104 : 86, children: [selected.map(item => item.kind === 'voice' && item.media.assetId !== editing?.media?.assetId && components?.VoiceAttachment ? _jsx(components.VoiceAttachment, { media: item.media, appearance: design, disabled: blocked, onRemove: () => removeSelected(item.id) }, item.id) : _jsxs(View, { style: { width: 88, paddingTop: 8, paddingRight: 10 }, children: [_jsxs(Pressable, { accessibilityRole: "button", accessibilityLabel: uiText('Preview {name}', { name: item.media.fileName ?? uiText(mediaNames[item.kind]) }), disabled: blocked || !openAttachment, onPress: () => void openAttachment?.({ ...item, id: item.id, clientMessageId: item.id, conversationId: state.conversationId, senderId: userId, createdAt: new Date().toISOString(), readAt: null }).catch(() => setError(uiText("This attachment could not be opened."))), style: { width: 76, height: 76, borderRadius: 9, overflow: 'hidden', backgroundColor: c.incoming, borderWidth: 1, borderColor: c.secondary + '40', alignItems: 'center', justifyContent: 'center' }, children: [item.thumbnail ? _jsx(Image, { source: { uri: item.thumbnail }, accessibilityLabel: item.media.fileName ?? uiText(mediaNames[item.kind]), resizeMode: "cover", style: { width: '100%', height: '100%' } }) : _jsx(MessageIcon, { name: item.kind === 'voice' ? 'mic' : item.kind, color: c.accent, size: 28 }), item.kind === 'video' && _jsx(View, { style: { position: 'absolute', bottom: 0, left: 0, right: 0, padding: 4, backgroundColor: '#0009' }, children: _jsx(Text, { style: { color: '#fff', fontSize: 10 }, children: duration(item.media.durationMs) }) })] }), item.kind === 'document' && _jsx(Text, { numberOfLines: 1, style: { fontSize: 10, color: c.foreground, marginTop: 3 }, children: item.media.fileName ?? uiText(mediaNames[item.kind]) }), _jsx(Pressable, { accessibilityRole: "button", accessibilityLabel: uiText('Remove {name}', { name: item.media.fileName ?? uiText(mediaNames[item.kind]) }), disabled: blocked, onPress: () => removeSelected(item.id), style: { position: 'absolute', right: 0, top: 0, width: 30, height: 30, alignItems: 'center', justifyContent: 'center' }, children: _jsx(View, { style: { backgroundColor: c.foreground, borderRadius: 11, padding: 3 }, children: _jsx(MessageIcon, { name: "close", size: 16, color: c.background }) }) })] }, item.id)), voiceReview] }) : null;
|
|
908
926
|
}
|
|
909
927
|
function renderTextComposer(voice = { hasVoice: false, blocked: false }) {
|
|
910
928
|
const blocked = voice.blocked || busy;
|
|
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) {
|
|
929
|
+
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: uiText("Attach"), accessibilityState: { expanded: attachmentsOpen }, disabled: busy || blocked, onPress: () => { if (attachmentsOpen) {
|
|
912
930
|
setAttachmentsOpen(false);
|
|
913
931
|
input.current?.focus();
|
|
914
932
|
}
|
|
@@ -918,29 +936,30 @@ export function MessagingComposer() {
|
|
|
918
936
|
event.preventDefault();
|
|
919
937
|
if (!blocked)
|
|
920
938
|
void send();
|
|
921
|
-
} } } : {}), onFocus: () => setAttachmentsOpen(false), style: { ...(Platform.OS === 'web' ? { outlineStyle: 'none' } : {}), flex: 1, minWidth: 0, minHeight: 44, height: inputHeight, maxHeight: 120, color: c.foreground, fontSize: 16, lineHeight: 22, paddingHorizontal: 4, paddingVertical: 8, backgroundColor: 'transparent' } }), !features?.text && _jsx(View, { style: { flex: 1 } }), features?.voice && _jsx(Pressable, { accessibilityRole: "button", accessibilityLabel: "Voice message", disabled: voicePending || busy || blocked, onPress: () => void pick('voice'), style: { width: 42, height: 44, flexShrink: 0, alignItems: 'center', justifyContent: 'center', opacity: voicePending || busy || blocked ? 0.5 : 1 }, children: _jsx(MessageIcon, { name: "mic", color: c.accent, size: 23 }) })] }), (features?.text || selected.length > 0) && _jsx(QuickSendButton, { choice: state.quickMessage, empty: !draft.trim() && !selected.length && !voice.hasVoice && !voice.sendVoice, enabled: !!features?.text, disabled: blocked, editing: !!editing, sendAvailable: !!draft.trim() || !!voice.sendVoice || selected.length > 0, appearance: design, onSchedule: state.scheduledMessagesAvailable && !editing && (!!draft.trim() || selected.length > 0) && !voice.hasVoice && !voice.sendVoice ? () => { setScheduleError(''); setScheduleOpen(true); } : undefined, onChoose: emoji => { void client.setQuickMessage(emoji).catch(() => setError(
|
|
939
|
+
} } } : {}), onFocus: () => setAttachmentsOpen(false), style: { ...(Platform.OS === 'web' ? { outlineStyle: 'none' } : {}), writingDirection: draft ? 'auto' : uiDirection, textAlign: uiDirection === 'rtl' ? 'right' : 'left', flex: 1, minWidth: 0, minHeight: 44, height: inputHeight, maxHeight: 120, color: c.foreground, fontSize: 16, lineHeight: 22, paddingHorizontal: 4, paddingVertical: 8, backgroundColor: 'transparent' } }), !features?.text && _jsx(View, { style: { flex: 1 } }), features?.voice && _jsx(Pressable, { accessibilityRole: "button", accessibilityLabel: uiText("Voice message"), disabled: voicePending || busy || blocked, onPress: () => void pick('voice'), style: { width: 42, height: 44, flexShrink: 0, alignItems: 'center', justifyContent: 'center', opacity: voicePending || busy || blocked ? 0.5 : 1 }, children: _jsx(MessageIcon, { name: "mic", color: c.accent, size: 23 }) })] }), (features?.text || selected.length > 0) && _jsx(QuickSendButton, { choice: state.quickMessage, empty: !draft.trim() && !selected.length && !voice.hasVoice && !voice.sendVoice, enabled: !!features?.text, disabled: blocked, editing: !!editing, sendAvailable: !!draft.trim() || !!voice.sendVoice || selected.length > 0, appearance: design, onSchedule: state.scheduledMessagesAvailable && !editing && (!!draft.trim() || selected.length > 0) && !voice.hasVoice && !voice.sendVoice ? () => { setScheduleError(''); setScheduleOpen(true); } : undefined, onChoose: emoji => { void client.setQuickMessage(emoji).catch(() => setError(uiText("This quick message could not be saved on this device."))); }, onSend: () => { if (voice.sendVoice)
|
|
922
940
|
voice.sendVoice();
|
|
923
941
|
else
|
|
924
942
|
void send(); }, onQuickSend: emoji => { void submit({ kind: 'text', text: emoji }).catch(() => { }); } }, state.conversationId)] });
|
|
925
943
|
}
|
|
926
944
|
const attachmentActions = [];
|
|
927
945
|
if (library.image || library.video)
|
|
928
|
-
attachmentActions.push({ key: 'library', label: library.image && library.video ?
|
|
946
|
+
attachmentActions.push({ key: 'library', label: library.image && library.video ? uiText("Photos & Videos") : library.image ? uiText("Photos") : uiText("Videos"), icon: 'image', run: () => { void pick(library.image ? 'image' : 'video', undefined, undefined, 'send', true); } });
|
|
929
947
|
if (camera)
|
|
930
|
-
attachmentActions.push({ key: 'camera', label:
|
|
948
|
+
attachmentActions.push({ key: 'camera', label: uiText("Camera"), icon: 'camera', run: () => { void pick(features?.image ? 'image' : 'video', 'camera'); } });
|
|
931
949
|
if (library.document)
|
|
932
|
-
attachmentActions.push({ key: 'files', label:
|
|
950
|
+
attachmentActions.push({ key: 'files', label: uiText("Files"), icon: 'document', run: () => { void pick('document'); } });
|
|
933
951
|
if (!editing && features?.poll)
|
|
934
|
-
attachmentActions.push({ key: 'poll', label: state.conversation?.kind === 'group' ?
|
|
952
|
+
attachmentActions.push({ key: 'poll', label: state.conversation?.kind === 'group' ? uiText("Poll") : uiText("Yes/No question"), icon: 'poll', run: () => setPollOpen(true) });
|
|
935
953
|
if (!editing && weatherAllowed)
|
|
936
|
-
attachmentActions.push({ key: 'weather', label:
|
|
954
|
+
attachmentActions.push({ key: 'weather', label: uiText("Weather"), icon: 'weather', run: () => { setBusy(true); setError(''); void extras?.sendWeather().then(() => client.refresh()).catch(e => setError(e instanceof Error ? e.message : uiText("Weather could not be sent."))).finally(() => setBusy(false)); } });
|
|
937
955
|
if (!enabled || state.conversation?.canSend === false) {
|
|
938
|
-
return (_jsx(Text, { style: { padding: 16, color: c.secondary }, children: state.offline ?
|
|
956
|
+
return (_jsx(Text, { style: { padding: 16, color: c.secondary }, children: state.offline ? uiText("Offline · Recent messages only. Connect to send and load more.") : state.conversation?.safety?.blockedByMe ? uiText("You blocked this contact. Use contact options to unblock.") : state.conversation?.canSend === false ? (state.conversation.kind === 'group' ? uiText('Only members allowed to send can post in this group.') : uiText('Messaging is unavailable for this conversation.')) : design.labels.unavailable }));
|
|
939
957
|
}
|
|
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 '
|
|
958
|
+
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: uiText('Mention {name}', { name: 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: [uiText("Editing:"), editing.text || editing.media?.fileName] }), _jsx(Pressable, { accessibilityRole: "button", accessibilityLabel: uiText("Cancel editing"), disabled: busy, onPress: () => setEditing(null), style: { minHeight: 36, justifyContent: 'center' }, children: _jsx(Text, { style: { color: c.accent }, children: uiText("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: uiText("Cancel quote"), onPress: () => setReply(null), children: _jsx(Text, { style: { color: c.foreground }, children: uiText("Cancel") }) })] })), state.settings?.history?.mode === 'last_two' && _jsx(Text, { style: { color: c.secondary, fontSize: 11, textAlign: 'center' }, children: uiText("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: uiText("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 }))] }));
|
|
941
959
|
}
|
|
942
960
|
/** Account navigation. Mount in the inbox/app toolbar, never in a contact menu. */
|
|
943
961
|
export function MessagingAccountMenu({ onNewGroup, onMaps } = {}) {
|
|
962
|
+
const { text: uiText, locale: uiLocale, number: uiNumber, count: uiCount } = useRobonoLocale();
|
|
944
963
|
const { client, design, setSavedOpen, account, companyName } = useMessagingUI();
|
|
945
964
|
const [open, setOpen] = useState(false), [accountOpen, setAccountOpen] = useState(false), pending = useRef(null);
|
|
946
965
|
const button = useRef(null), viewport = useWindowDimensions(), [anchor, setAnchor] = useState({ x: 12, y: 12 });
|
|
@@ -955,12 +974,23 @@ export function MessagingAccountMenu({ onNewGroup, onMaps } = {}) {
|
|
|
955
974
|
event.stopPropagation();
|
|
956
975
|
setOpen(false);
|
|
957
976
|
} }; document.addEventListener('keydown', key, true); return () => document.removeEventListener('keydown', key, true); }, [open]);
|
|
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) })] });
|
|
977
|
+
return _jsxs(_Fragment, { children: [_jsx(Pressable, { ref: button, accessibilityRole: "button", accessibilityLabel: uiText("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: uiText("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: uiText("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: uiText("Account") })] }), onMaps && _jsxs(Pressable, { accessibilityRole: "button", accessibilityLabel: uiText("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: uiText("Maps") })] }), onNewGroup && _jsxs(Pressable, { accessibilityRole: "button", accessibilityLabel: uiText("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: uiText("New group") })] }), client.hasSavedMessages() && _jsxs(Pressable, { accessibilityRole: "button", accessibilityLabel: uiText("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: uiText("Saved") })] })] }) })] }) }), accountOpen && _jsx(MessagingAccountScreen, { account: account, companyName: companyName ?? design.name, onClose: () => setAccountOpen(false) })] });
|
|
978
|
+
}
|
|
979
|
+
function MessagingWaitingChanges() {
|
|
980
|
+
const { state, client, design } = useMessagingUI(), { text: uiText, count } = useRobonoLocale();
|
|
981
|
+
const [open, setOpen] = useState(false), [busy, setBusy] = useState(false), [error, setError] = useState('');
|
|
982
|
+
useEffect(() => { setOpen(false); setError(''); }, [state.conversationId]);
|
|
983
|
+
const actions = state.pendingActions ?? [];
|
|
984
|
+
if (!actions.length)
|
|
985
|
+
return null;
|
|
986
|
+
const work = (id, retry) => { setBusy(true); setError(''); void (retry ? client.retryMessageAction(id) : client.discardMessageAction(id)).catch(e => setError(e instanceof Error ? e.message : uiText("This change could not be saved."))).finally(() => setBusy(false)); };
|
|
987
|
+
return _jsxs(_Fragment, { children: [_jsx(Pressable, { accessibilityRole: "button", onPress: () => setOpen(true), style: { minHeight: 44, justifyContent: 'center', paddingHorizontal: 12 }, children: _jsx(Text, { style: { color: design.colors.accent }, children: actions.some(a => !a.retry) ? uiText("A message change needs your attention") : count(actions.length, "{count} change waiting for connection", "{count} changes waiting for connection") }) }), open && _jsxs(MessagingPopup, { title: uiText("Waiting changes"), onClose: () => setOpen(false), foreground: design.colors.foreground, background: chatSurface(design.colors.background), children: [_jsx(ScrollView, { children: actions.map(action => _jsxs(View, { style: { paddingVertical: 12, gap: 8, borderBottomWidth: 1, borderBottomColor: messagingPopupColors.secondary }, children: [_jsx(Text, { style: { color: messagingPopupColors.foreground, fontWeight: '600' }, children: action.operation === 'react' ? uiText("Reaction") : action.operation === 'edit' ? uiText("Edit message") : uiText("Delete message") }), _jsx(Text, { numberOfLines: 3, style: { color: messagingPopupColors.foreground }, children: typeof action.input.text === 'string' ? action.input.text : state.messages.find(m => m.id === action.messageId)?.text ?? uiText("Message") }), _jsx(Text, { style: { color: messagingPopupColors.secondary }, children: action.retry ? uiText("Waiting for connection. This change has not been confirmed.") : uiText(action.error ?? "This change could not be saved.") }), _jsxs(View, { style: { flexDirection: 'row', gap: 20 }, children: [!action.retry && _jsx(Pressable, { accessibilityRole: "button", disabled: busy, onPress: () => work(action.id, true), style: { minHeight: 44, justifyContent: 'center' }, children: _jsx(Text, { style: { color: design.colors.accent }, children: uiText("Retry") }) }), _jsx(Pressable, { accessibilityRole: "button", disabled: busy, onPress: () => work(action.id, false), style: { minHeight: 44, justifyContent: 'center' }, children: _jsx(Text, { style: { color: design.colors.accent }, children: uiText("Discard change") }) })] })] }, action.id)) }), !!error && _jsx(Text, { accessibilityRole: "alert", style: { color: messagingPopupColors.foreground }, children: error })] })] });
|
|
959
988
|
}
|
|
960
989
|
export function MessagingConversation() {
|
|
990
|
+
const { text: uiText } = useRobonoLocale();
|
|
961
991
|
const { state, client, components, design, style } = useMessagingUI();
|
|
962
992
|
const Header = components?.Header ?? MessagingHeader, List = components?.List ?? MessagingList, Composer = components?.Composer ?? MessagingComposer;
|
|
963
|
-
return (_jsxs(KeyboardAvoidingView, { behavior: Platform.OS === "ios" ? "padding" : undefined, style: [{ flex: 1, backgroundColor: design.colors.background }, style], children: [_jsx(MessagingPopupHeaderBoundary, { children: _jsx(Header, {}) }), _jsx(List, {}), state.error && (_jsx(Text, { accessibilityRole: "alert", style: { padding: 12, color: design.colors.foreground }, children: state.error })), _jsx(Composer, {}, state.conversationId)] }));
|
|
993
|
+
return (_jsxs(KeyboardAvoidingView, { behavior: Platform.OS === "ios" ? "padding" : undefined, style: [{ flex: 1, backgroundColor: design.colors.background }, style], children: [_jsx(MessagingPopupHeaderBoundary, { children: _jsx(Header, {}) }), _jsx(List, {}), state.error && (_jsx(Text, { accessibilityRole: "alert", style: { padding: 12, color: design.colors.foreground }, children: uiText(state.error) })), _jsx(MessagingWaitingChanges, {}), _jsx(Composer, {}, state.conversationId)] }));
|
|
964
994
|
}
|
|
965
995
|
/** Embed inside your app's safe area and navigation. No engine, push or global audio side effects. */
|
|
966
996
|
export function MessagingScreen(props) {
|