@robono/messaging-react-native 0.9.0 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/README.md +52 -0
  3. package/SBOM.spdx.json +494 -206
  4. package/dist/account.js +2 -1
  5. package/dist/attachment-menu.js +2 -1
  6. package/dist/call-message.js +2 -1
  7. package/dist/calling.js +4 -2
  8. package/dist/conversation-details.d.ts +1 -0
  9. package/dist/conversation-details.js +3 -2
  10. package/dist/custom-message.d.ts +7 -0
  11. package/dist/custom-message.js +53 -0
  12. package/dist/customization.d.ts +49 -0
  13. package/dist/customization.js +1 -0
  14. package/dist/expo-connected-calling.js +4 -2
  15. package/dist/expo-messenger.d.ts +4 -1
  16. package/dist/expo-messenger.js +34 -8
  17. package/dist/expo-screen.js +20 -18
  18. package/dist/expo-social.js +2 -1
  19. package/dist/extras.js +2 -1
  20. package/dist/forward.js +2 -1
  21. package/dist/group-editor.js +2 -1
  22. package/dist/header-calls.js +2 -1
  23. package/dist/icons.js +2 -1
  24. package/dist/inbox-search.d.ts +3 -1
  25. package/dist/inbox-search.js +30 -9
  26. package/dist/inbox-view.d.ts +21 -0
  27. package/dist/inbox-view.js +32 -0
  28. package/dist/inbox.d.ts +6 -1
  29. package/dist/inbox.js +24 -17
  30. package/dist/index.d.ts +10 -3
  31. package/dist/index.js +40 -29
  32. package/dist/maps-connection.js +2 -1
  33. package/dist/media-ui.js +2 -1
  34. package/dist/message-menu.js +2 -1
  35. package/dist/native-outbox.d.ts +1 -1
  36. package/dist/polls.js +4 -3
  37. package/dist/popup.js +2 -1
  38. package/dist/public-rooms.d.ts +10 -0
  39. package/dist/public-rooms.js +78 -0
  40. package/dist/quick-send.js +2 -1
  41. package/dist/room-moderation.d.ts +22 -0
  42. package/dist/room-moderation.js +87 -0
  43. package/dist/safety.js +2 -1
  44. package/dist/saved.js +2 -1
  45. package/dist/scheduled.js +2 -1
  46. package/dist/search.js +2 -1
  47. package/dist/social.js +2 -1
  48. package/dist/translation-settings.js +2 -1
  49. package/dist/typography.d.ts +12 -0
  50. package/dist/typography.js +31 -0
  51. package/dist/voice-composer.js +2 -1
  52. package/package.json +6 -6
package/dist/index.js CHANGED
@@ -1,4 +1,8 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { MessagingRoomModeration, MessagingRoomRemoval } from './room-moderation.js';
3
+ import { CustomMessage } from './custom-message.js';
4
+ import { MessagingTypography } from './typography.js';
5
+ import { Text, TextInput } from './typography.js';
2
6
  import { useRobonoLocale, RobonoLocaleProvider, localizeMessagingAppearance } from './localization.js';
3
7
  export { RobonoLocaleProvider, useRobonoLocale } from './localization.js';
4
8
  import { MessagingMenuSurface } from './popup.js';
@@ -28,7 +32,7 @@ export { MessagingSafety } from './safety.js';
28
32
  import { PollMessage, PollComposer } from "./polls.js";
29
33
  export { PollMessage, PollComposer } from "./polls.js";
30
34
  import { createContext, useContext, useEffect, useRef, useState, useSyncExternalStore, } from "react";
31
- import { Animated, ActivityIndicator, AppState, BackHandler, FlatList, Image, KeyboardAvoidingView, Keyboard, Linking, Platform, Pressable, ScrollView, Text, TextInput, View, useWindowDimensions, } from "react-native";
35
+ import { Animated, ActivityIndicator, AppState, BackHandler, FlatList, Image, KeyboardAvoidingView, Keyboard, Linking, Platform, Pressable, ScrollView, View, useWindowDimensions, } from "react-native";
32
36
  import { useQuoteGesture, messageInfoRevealWidth, messageReplyRevealWidth } from "./use-quote-gesture.js";
33
37
  import { MessageIcon, duration, fileSize, chatSurface, composerSurface } from "./icons.js";
34
38
  import { VoiceMessageComposer } from "./voice-composer.js";
@@ -146,14 +150,14 @@ function MessagingProviderContent({ children, ...props }) {
146
150
  setEditing, setForwarding, setDeleting, selection: selected, setSelection, saved, saving, toggleSaved, setSavedOpen,
147
151
  design: localizeMessagingAppearance(props.appearance ?? state.settings?.appearance ?? defaultMessagingSettings().appearance, uiText),
148
152
  };
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 {
150
- if (!props.openSavedAttachment)
151
- throw Error('Saved attachments cannot be opened here.');
152
- await props.openSavedAttachment(record);
153
- }
154
- catch (e) {
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") }) })] })] }) });
153
+ return _jsx(Context.Provider, { value: value, children: _jsx(MessagingTypography, { appearance: value.design, 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 {
154
+ if (!props.openSavedAttachment)
155
+ throw Error('Saved attachments cannot be opened here.');
156
+ await props.openSavedAttachment(record);
157
+ }
158
+ catch (e) {
159
+ setSavedError(e instanceof Error ? e.message : uiText("This saved attachment could not be opened."));
160
+ } }, 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") }) })] })] }) }) });
157
161
  }
158
162
  function GroupDiscussion({ threadRoot, onClose }) {
159
163
  const { text: uiText, locale: uiLocale, number: uiNumber, count: uiCount } = useRobonoLocale();
@@ -231,23 +235,25 @@ export function messagingHeaderColors(design, group) {
231
235
  return { background, foreground };
232
236
  }
233
237
  function MessagingActivity({ ink }) {
234
- const { text: uiText, count: uiCount } = useRobonoLocale();
238
+ const { text: uiText, count: uiCount, locale: uiLocale } = useRobonoLocale();
235
239
  const { state, design, participants } = useMessagingUI(), [now, setNow] = useState(Date.now());
236
240
  useEffect(() => { setNow(Date.now()); if (!state.presence?.peers.length)
237
241
  return; const timer = setInterval(() => setNow(Date.now()), 1000); return () => clearInterval(timer); }, [state.presence]);
238
242
  const typing = state.settings?.features.typing ? state.presence?.peers.filter(peer => peer.typingUntil > now) ?? [] : [];
239
243
  const online = state.settings?.features.online && state.presence?.peers.some(peer => peer.onlineUntil > now);
240
244
  const group = state.conversation?.kind === 'group';
245
+ const lastSeen = state.settings?.features.lastSeen ? state.presence?.peers.find(peer => Number.isFinite(peer.lastSeenAt) && peer.lastSeenAt > 0)?.lastSeenAt : null;
246
+ const lastSeenLabel = lastSeen ? new Date(lastSeen).toLocaleString(uiLocale, { month: 'short', day: 'numeric', hour: 'numeric', minute: '2-digit' }) : '';
241
247
  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') : '';
248
+ 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 && lastSeenLabel ? uiText('Last seen {time}', { time: lastSeenLabel }) : group ? uiCount(state.conversation?.memberCount ?? state.conversation?.memberIds?.length ?? 0, '{count} member', '{count} members') : '';
243
249
  return text ? _jsx(Text, { numberOfLines: 1, style: { color: ink, opacity: .8, fontSize: 14 }, children: text }) : null;
244
250
  }
245
251
  export function MessagingHeader({ children } = {}) {
246
252
  const { text: uiText, locale: uiLocale, number: uiNumber, count: uiCount } = useRobonoLocale();
247
253
  const { contact, design, state, client, openDiscussion, userId, participants, onBack, onContactOptions, selection, setSelection, language, components, callActions, attachmentPreview, openAttachment } = useMessagingUI();
248
254
  const [scheduledOpen, setScheduledOpen] = useState(false);
249
- const [safetyOpen, setSafetyOpen] = useState(false), [searchOpen, setSearchOpen] = useState(false), [detailsOpen, setDetailsOpen] = useState(false), [groupOpen, setGroupOpen] = useState(false);
250
- useEffect(() => { setDetailsOpen(false); setGroupOpen(false); setScheduledOpen(false); }, [state.conversationId]);
255
+ const [safetyOpen, setSafetyOpen] = useState(false), [searchOpen, setSearchOpen] = useState(false), [detailsOpen, setDetailsOpen] = useState(false), [groupOpen, setGroupOpen] = useState(false), [moderationOpen, setModerationOpen] = useState(false);
256
+ useEffect(() => { setDetailsOpen(false); setGroupOpen(false); setModerationOpen(false); setScheduledOpen(false); }, [state.conversationId]);
251
257
  const menuButton = useRef(null), viewport = useWindowDimensions(), [menuAnchor, setMenuAnchor] = useState({ x: 12, y: 12 });
252
258
  const openContactMenu = () => menuButton.current?.measureInWindow((x, y, width, height) => { setMenuAnchor({ x: Math.max(12, Math.min(x + width - 240, viewport.width - 252)), y: y + height + 4 }); setSafetyOpen(true); });
253
259
  const translationAvailable = !!state.settings?.enabled && !!state.settings.features.translation && client.hasLanguageTools();
@@ -270,7 +276,7 @@ export function MessagingHeader({ children } = {}) {
270
276
  backgroundColor: c.incoming,
271
277
  alignItems: "center",
272
278
  justifyContent: "center",
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)] }));
279
+ }, 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, components?.HeaderActions && _jsx(components.HeaderActions, { conversationId: state.conversationId, contact: contact, appearance: design }), 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) }), moderationOpen && _jsx(MessagingRoomModeration, { client: client, userId: userId, participants: participants, accent: c.accent, onClose: () => setModerationOpen(false) }), detailsOpen && _jsx(ConversationDetails, { onModerate: () => setModerationOpen(true), 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)] }));
274
280
  }
275
281
  function PrivacyControl({ color }) {
276
282
  const { text: uiText, locale: uiLocale, number: uiNumber, count: uiCount } = useRobonoLocale();
@@ -344,17 +350,19 @@ function MessageMetadata({ mine, ink, pendingStatus }) {
344
350
  export function MessagingBubble({ message, pendingStatus, previousMessage, nextMessage }) {
345
351
  const { text: uiText, locale: uiLocale, number: uiNumber, count: uiCount } = useRobonoLocale();
346
352
  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();
347
- const extra = !pendingStatus && !message.deleted ? extrasState?.messages[message.id] : undefined;
353
+ const extra = !message.custom && !pendingStatus && !message.deleted ? extrasState?.messages[message.id] : undefined;
348
354
  const extraRef = useRef(null), socialRef = useRef(null);
349
355
  const socialOrigin = useMessagingUI().socialOrigin ?? 'https://robono.com';
350
- const socialReady = !extra?.address && !pendingStatus && !message.deleted && message.kind === 'text' && state.settings?.enabled && state.socialPreviewAvailable && components?.SocialPlayer && messageLinks(message.text ?? '').some(link => !!link.url && !!socialShare(link.url, socialOrigin));
356
+ const socialReady = !message.custom && !extra?.address && !pendingStatus && !message.deleted && message.kind === 'text' && state.settings?.enabled && state.socialPreviewAvailable && components?.SocialPlayer && messageLinks(message.text ?? '').some(link => !!link.url && !!socialShare(link.url, socialOrigin));
351
357
  const contextContactName = contact.name;
352
358
  const mine = message.senderId === userId, c = design.colors, ink = message.kind === 'poll' ? c.foreground : mine ? c.outgoingText : c.incomingText;
359
+ const bubbleRadius = design.presentation?.bubbleShape === 'square' ? 2 : design.presentation?.bubbleShape === 'rounded' ? 16 : 8;
360
+ const messageGap = design.presentation?.messageSpacing === 'compact' ? 4 : design.presentation?.messageSpacing === 'relaxed' ? 12 : 8;
353
361
  const visual = message.kind === 'image' || message.kind === 'video', voice = message.kind === 'voice';
354
362
  const previous = previousMessage ?? state.messages[state.messages.findIndex(m => m.id === message.id) - 1];
355
363
  const joinsBefore = continuesAttachmentSend(previousMessage, message), joinsAfter = continuesAttachmentSend(message, nextMessage);
356
364
  const grouped = previous?.senderId === message.senderId && new Date(previous.createdAt).toDateString() === new Date(message.createdAt).toDateString();
357
- const [error, setError] = useState(''), [busy, setBusy] = useState(false), [actions, setActions] = useState(false), [reportOpen, setReportOpen] = useState(false);
365
+ const [error, setError] = useState(''), [busy, setBusy] = useState(false), [actions, setActions] = useState(false), [reportOpen, setReportOpen] = useState(false), [roomRemoval, setRoomRemoval] = useState(false);
358
366
  const afterMenu = useRef(null);
359
367
  const menuDismissed = () => { const next = afterMenu.current; afterMenu.current = null; next?.(); };
360
368
  const leaveMenu = (next) => { afterMenu.current = next; setActions(false); if (Platform.OS !== 'ios')
@@ -438,24 +446,27 @@ export function MessagingBubble({ message, pendingStatus, previousMessage, nextM
438
446
  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
447
  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
448
  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())
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;
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;
449
+ extraRef.current?.open(); }, style: { flexShrink: 1, minWidth: 0, minHeight: 44, justifyContent: 'center', position: 'relative', backgroundColor: mine ? c.outgoing : c.incoming, borderRadius: bubbleRadius, paddingHorizontal: 6, paddingVertical: 3 }, children: [bubbleBody, people] }), action] }) }) : null;
450
+ 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: bubbleRadius, paddingHorizontal: 6, paddingVertical: 3 }, children: bubbleBody }), action] }) }) : null;
443
451
  const canCopy = !!message.text && (!!copyText || Platform.OS === 'web');
444
452
  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)) }) });
445
453
  const footer = _jsx(MessageMetadata, { message: message, mine: mine, ink: visual ? '#ffffff' : ink, pendingStatus: pendingStatus });
446
454
  const bigEmoji = message.kind === 'text' && !message.replyTo && !extra && !canTranslate && !transformed && isBigEmojiText(message.text);
447
455
  const showingTransform = !!transformed && !showOriginal && (transformed.kind === 'transcript' || canTranslate);
448
456
  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") }) })] });
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 });
457
+ const fallbackContent = _jsx(View, { testID: "message-original-text", children: _jsx(Text, { selectable: true, style: { color: ink, fontSize: 16, lineHeight: 22 }, children: renderText(message.text ?? '') }) });
458
+ const originalContent = message.custom && !message.deleted ? _jsxs(Pressable, { onLongPress: menu, children: [_jsx(CustomMessage, { message: message, mine: mine, appearance: design, pendingStatus: pendingStatus, components: components, enabled: canInteract, children: fallbackContent }), pendingStatus && footer] }) : 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 });
450
459
  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())
460
+ const regionProps = { message, mine, appearance: design, pendingStatus };
461
+ const bodyContent = components?.MessageBody && !message.deleted ? _jsx(components.MessageBody, { ...regionProps, children: content }) : content;
462
+ 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 ? -(messageGap + 2) / 2 : 0, bottom: joinsAfter ? -(messageGap + 2) / 2 : 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 : bubbleRadius, borderBottomEndRadius: joinsAfter ? 0 : voice && !message.text && !showingTransform ? 24 : bubbleRadius } : { borderStartWidth: 2, borderStartColor: '#2B2B2B', borderTopStartRadius: joinsBefore ? 0 : voice && !message.text && !showingTransform ? 24 : bubbleRadius, borderBottomStartRadius: joinsAfter ? 0 : voice && !message.text && !showingTransform ? 24 : bubbleRadius }) } }), 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") }), message.custom || extra?.address || extra?.weather || extra?.call || voice || message.kind === 'poll' ? bodyContent : _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())
452
463
  return; if (extra?.address)
453
464
  extraRef.current?.open();
454
465
  else
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)
466
+ socialRef.current?.open(); } : () => void open(), children: bodyContent }), !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, components?.MessageFooter && !message.deleted && _jsx(components.MessageFooter, { ...regionProps }), 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.custom && 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)) }), !pendingStatus && !message.deleted && ['owner', 'moderator'].includes(state.conversation?.roomRole ?? '') && _jsx(MessageMenuRow, { icon: "trash", label: uiText('Remove from group'), danger: true, onPress: () => leaveMenu(() => setRoomRemoval(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(); } })] }), roomRemoval && _jsx(MessagingRoomRemoval, { client: client, message: message, accent: c.accent, onClose: () => setRoomRemoval(false) }), reportOpen && _jsx(MessagingSafety, { client: client, state: state, appearance: design, name: participants?.[message.senderId]?.name ?? 'Contact', messageId: message.id, onClose: () => setReportOpen(false) })] });
467
+ return _jsxs(View, { ...(Platform.OS === 'web' ? { onMouseEnter: () => setHovered(true), onMouseLeave: () => setHovered(false), onContextMenu: (event) => { event.preventDefault(); menu(); } } : {}), style: { width: '100%', position: 'relative', marginTop: messageGap + (message.reactions?.length ? 16 : 0), 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
468
  quoteGesture.webRef.current = node; }, ...quoteGesture.handlers, accessibilityActions: quoting ? [{ name: "quote", label: uiText("Quote message") }] : undefined, onAccessibilityAction: event => { if (quoting && event.nativeEvent.actionName === "quote")
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 })] });
469
+ 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: bubbleRadius, 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 })] });
459
470
  }
460
471
  function MessageDeletion({ messages, onClose }) {
461
472
  const { text: uiText, locale: uiLocale, number: uiNumber, count: uiCount } = useRobonoLocale();
@@ -958,15 +969,15 @@ export function MessagingComposer() {
958
969
  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 }))] }));
959
970
  }
960
971
  /** Account navigation. Mount in the inbox/app toolbar, never in a contact menu. */
961
- export function MessagingAccountMenu({ onNewGroup, onMaps } = {}) {
972
+ export function MessagingAccountMenu({ onNewGroup, onMaps, onPublicRooms } = {}) {
962
973
  const { text: uiText, locale: uiLocale, number: uiNumber, count: uiCount } = useRobonoLocale();
963
974
  const { client, design, setSavedOpen, account, companyName } = useMessagingUI();
964
975
  const [open, setOpen] = useState(false), [accountOpen, setAccountOpen] = useState(false), pending = useRef(null);
965
976
  const button = useRef(null), viewport = useWindowDimensions(), [anchor, setAnchor] = useState({ x: 12, y: 12 });
966
977
  const width = Math.min(218, viewport.width - 24);
967
- const show = () => button.current?.measureInWindow((x, y, w, h) => { setAnchor({ x: Math.max(12, Math.min(x + w - width, viewport.width - width - 12)), y: Math.max(12, Math.min(y + h + 4, viewport.height - (client.hasSavedMessages() ? 116 : 72) - (onNewGroup ? 44 : 0) - (onMaps ? 44 : 0))) }); setOpen(true); });
978
+ const show = () => button.current?.measureInWindow((x, y, w, h) => { setAnchor({ x: Math.max(12, Math.min(x + w - width, viewport.width - width - 12)), y: Math.max(12, Math.min(y + h + 4, viewport.height - (client.hasSavedMessages() ? 116 : 72) - (onNewGroup ? 44 : 0) - (onMaps ? 44 : 0) - (onPublicRooms ? 44 : 0))) }); setOpen(true); });
968
979
  const dismissed = () => { const next = pending.current; if (!next)
969
- return; pending.current = null; requestAnimationFrame(() => next === 'saved' ? setSavedOpen(true) : next === 'group' ? onNewGroup?.() : next === 'maps' ? onMaps?.() : setAccountOpen(true)); };
980
+ return; pending.current = null; requestAnimationFrame(() => next === 'saved' ? setSavedOpen(true) : next === 'group' ? onNewGroup?.() : next === 'maps' ? onMaps?.() : next === 'rooms' ? onPublicRooms?.() : setAccountOpen(true)); };
970
981
  useEffect(() => { if (!open && Platform.OS !== 'ios')
971
982
  dismissed(); }, [open]);
972
983
  useEffect(() => { if (!open || Platform.OS !== 'web')
@@ -974,7 +985,7 @@ export function MessagingAccountMenu({ onNewGroup, onMaps } = {}) {
974
985
  event.stopPropagation();
975
986
  setOpen(false);
976
987
  } }; document.addEventListener('keydown', key, true); return () => document.removeEventListener('keydown', key, true); }, [open]);
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) })] });
988
+ 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) - (onPublicRooms ? 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") })] }), onPublicRooms && _jsxs(Pressable, { accessibilityRole: "button", accessibilityLabel: uiText("Public groups"), onPress: () => { pending.current = 'rooms'; 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("Public groups") })] }), 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
989
  }
979
990
  function MessagingWaitingChanges() {
980
991
  const { state, client, design } = useMessagingUI(), { text: uiText, count } = useRobonoLocale();
@@ -994,6 +1005,6 @@ export function MessagingConversation() {
994
1005
  }
995
1006
  /** Embed inside your app's safe area and navigation. No engine, push or global audio side effects. */
996
1007
  export function MessagingScreen(props) {
997
- return (_jsx(MessagingProvider, { ...props, children: _jsx(MessagingConversation, {}) }));
1008
+ return (_jsx(MessagingProvider, { ...props, children: props.renderLayout ? props.renderLayout(_jsx(MessagingConversation, {})) : _jsx(MessagingConversation, {}) }));
998
1009
  }
999
1010
  export { MessagingInbox } from './inbox.js';
@@ -1,7 +1,8 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Text } from './typography.js';
2
3
  import { useRobonoLocale } from './localization.js';
3
4
  import { useCallback, useEffect, useRef, useState } from 'react';
4
- import { AppState, Platform, Pressable, Text, View } from 'react-native';
5
+ import { AppState, Platform, Pressable, View } from 'react-native';
5
6
  /** Recover settings after reconnect/foreground without capturing GPS or loading a map. */
6
7
  export function useMessengerMaps(workspace) {
7
8
  const [available, setAvailable] = useState(false), [error, setError] = useState('');
package/dist/media-ui.js CHANGED
@@ -1,9 +1,10 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Text } from './typography.js';
2
3
  import { useRobonoLocale } from './localization.js';
3
4
  import { messengerVoiceWaveform } from './voice-waveform.js';
4
5
  import { createPhotoGesture } from './photo-gesture.js';
5
6
  import { useRef, useContext, useState, useEffect } from 'react';
6
- import { ActivityIndicator, Image, PanResponder, Platform, Pressable, Text, View } from 'react-native';
7
+ import { ActivityIndicator, Image, PanResponder, Platform, Pressable, View } from 'react-native';
7
8
  import { MediaReadiness } from './media-readiness.js';
8
9
  import { MessageIcon, duration, fileSize } from './icons.js';
9
10
  /** Same chat controls on web, iOS and Android. Audio engines supply state, not a different UI. */
@@ -1,8 +1,9 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { Text } from './typography.js';
2
3
  import { useRobonoLocale } from './localization.js';
3
4
  import { messagingPopupBackdrop, MessagingMenuSurface } from './popup.js';
4
5
  import { useEffect } from 'react';
5
- import { Platform, Pressable, ScrollView, Text, View } from 'react-native';
6
+ import { Platform, Pressable, ScrollView, View } from 'react-native';
6
7
  import { MessageIcon } from './icons.js';
7
8
  /** Anchor outside the selected bubble, never at its text's center. */
8
9
  export function messageMenuPosition(x, y, width, height, screenWidth, screenHeight, mine) {
@@ -45,7 +45,7 @@ export declare class NativeMessagingOutbox implements MessagingOutbox {
45
45
  needsPreparation?: boolean;
46
46
  }[];
47
47
  attachments: {
48
- kind: "text" | "video" | "image" | "document" | "poll" | "voice";
48
+ kind: "text" | "image" | "document" | "video" | "poll" | "voice";
49
49
  media: MessageMedia;
50
50
  }[];
51
51
  quickMessage?: string | undefined;
package/dist/polls.js CHANGED
@@ -1,11 +1,12 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { Text, TextInput } from './typography.js';
2
3
  import { useRobonoLocale } from './localization.js';
3
4
  import { messagingPopupColors } from './popup.js';
4
5
  import { ExtraPopup } from './extras.js';
5
6
  import { MessageIcon } from './icons.js';
6
7
  import { useRef, useState } from 'react';
7
8
  import { createPollVoteQueue } from './pollVoteQueue';
8
- import { ActivityIndicator, Pressable, ScrollView, Switch, Text, TextInput, View } from 'react-native';
9
+ import { ActivityIndicator, Pressable, ScrollView, Switch, View } from 'react-native';
9
10
  import { validatePoll } from '@robono/messaging';
10
11
  const PollArea = View;
11
12
  import { MessagingPopup as PollModal } from './popup.js';
@@ -43,14 +44,14 @@ export function PollMessage({ message, userId, ink, accent, enabled, onVote, onC
43
44
  actionBusy.current = false;
44
45
  setBusy(false);
45
46
  } }
46
- const details = _jsxs(View, { style: { gap: 14 }, children: [_jsxs(View, { style: { gap: 4 }, children: [_jsx(Text, { style: { fontSize: 17, lineHeight: 23, fontWeight: '600', color: messagingPopupColors.foreground }, children: poll.question }), _jsxs(Text, { style: { fontSize: 12, lineHeight: 17, color: messagingPopupColors.foreground, opacity: .65 }, children: [votes.length, " ", votes.length === 1 ? uiText("person voted") : uiText("people voted"), " \u00B7 ", closed ? uiText("Closed") : poll.multiple ? uiText("Multiple answers") : uiText("One answer per person")] })] }), _jsx(View, { style: { gap: 14 }, children: poll.options.map((option, index) => {
47
+ const details = _jsxs(View, { style: { gap: 14 }, children: [_jsxs(View, { style: { gap: 4 }, children: [_jsx(Text, { style: { fontSize: 17, lineHeight: 23, fontWeight: '600', color: messagingPopupColors.foreground }, children: poll.question }), _jsxs(Text, { style: { fontSize: 12, lineHeight: 17, color: messagingPopupColors.foreground, opacity: .65 }, children: [uiCount(votes.length, '{count} person voted', '{count} people voted'), " \u00B7 ", closed ? uiText("Closed") : poll.multiple ? uiText("Multiple answers") : uiText("One answer per person")] })] }), _jsx(View, { style: { gap: 14 }, children: poll.options.map((option, index) => {
47
48
  const people = votes.filter(v => v.options.includes(index));
48
49
  const names = people.map(v => v.userId === userId ? 'You' : resolveVoterName?.(v.userId) || 'Chat member').join(', ');
49
50
  return _jsxs(View, { style: { gap: 5 }, children: [_jsxs(View, { style: { flexDirection: 'row', alignItems: 'baseline', gap: 12 }, children: [_jsx(Text, { style: { flex: 1, color: messagingPopupColors.foreground, fontSize: 15, lineHeight: 21, fontWeight: '600' }, children: option }), _jsx(Text, { style: { color: messagingPopupColors.foreground, opacity: .65, fontSize: 13 }, children: people.length })] }), _jsxs(View, { style: { height: 4, borderRadius: 2, overflow: 'hidden' }, children: [_jsx(View, { style: { position: 'absolute', top: 0, bottom: 0, left: 0, right: 0, backgroundColor: foreground, opacity: .12 } }), _jsx(View, { style: { height: 4, width: `${votes.length ? people.length / votes.length * 100 : 0}%`, backgroundColor: accent } })] }), !!names && _jsx(Text, { style: { color: messagingPopupColors.foreground, opacity: .7, fontSize: 13, lineHeight: 18 }, children: names })] }, index);
50
51
  }) }), message.senderId === userId && !closed && enabled && _jsx(Pressable, { accessibilityRole: "button", disabled: busy || pending !== null, onPress: () => void act(onClose), style: { minHeight: 36, alignSelf: 'flex-end', justifyContent: 'center' }, children: _jsx(Text, { style: { color: accent, fontSize: 14 }, children: uiText("End poll") }) }), !!error && _jsx(Text, { accessibilityRole: "alert", style: { color: messagingPopupColors.foreground, fontSize: 13, lineHeight: 18 }, children: error })] });
51
52
  return _jsxs(_Fragment, { children: [_jsxs(Pressable, { accessible: false, onLongPress: onLongPress, onPress: event => { event.stopPropagation(); setShowVotes(true); }, accessibilityLabel: uiText("Poll: {value1}. Open votes", { value1: poll.question }), style: { width: 270, maxWidth: '100%', gap: 0, padding: 0 }, children: [_jsxs(Text, { accessibilityRole: "button", onLongPress: onLongPress, onPress: () => setShowVotes(true), style: { fontSize: 17, lineHeight: 23, fontWeight: '600', color: ink }, accessibilityHint: closed ? uiText("Poll closed") : poll.multiple ? uiText("Choose one or more answers") : uiText("Choose one answer"), children: [poll.question, closed ? uiText(" · Closed") : ''] }), poll.options.map((label, index) => {
52
53
  const count = votes.filter(v => v.options.includes(index)).length, checked = selected.includes(index);
53
- return _jsxs(View, { style: { minHeight: 36, paddingVertical: 3, gap: 2 }, children: [_jsxs(View, { style: { flexDirection: 'row', gap: 10, alignItems: 'center' }, children: [_jsx(Pressable, { accessibilityRole: poll.multiple ? 'checkbox' : 'radio', accessibilityLabel: `${label}, ${count} ${count === 1 ? 'vote' : 'votes'}`, "aria-checked": checked, accessibilityState: { checked, disabled: !enabled || closed || busy, busy: pending !== null }, hitSlop: 4, disabled: !enabled || closed || busy, onPress: event => { event.stopPropagation(); if (!enabled || closed || busy || actionBusy.current)
54
+ return _jsxs(View, { style: { minHeight: 36, paddingVertical: 3, gap: 2 }, children: [_jsxs(View, { style: { flexDirection: 'row', gap: 10, alignItems: 'center' }, children: [_jsx(Pressable, { accessibilityRole: poll.multiple ? 'checkbox' : 'radio', accessibilityLabel: uiText('{option}, {votes}', { option: label, votes: uiCount(count, '{count} vote', '{count} votes') }), "aria-checked": checked, accessibilityState: { checked, disabled: !enabled || closed || busy, busy: pending !== null }, hitSlop: 4, disabled: !enabled || closed || busy, onPress: event => { event.stopPropagation(); if (!enabled || closed || busy || actionBusy.current)
54
55
  return; const previous = selection.current; setError(''); void queue.current(previous.includes(index) ? previous.filter(x => x !== index) : poll.multiple ? [...previous, index] : [index]); }, style: { width: 20, height: 20, borderRadius: poll.multiple ? 4 : 10, borderWidth: 2, borderColor: checked ? accent : ink, alignItems: 'center', justifyContent: 'center' }, children: checked && _jsx(View, { style: { width: 10, height: 10, borderRadius: poll.multiple ? 2 : 5, backgroundColor: accent } }) }), _jsx(Text, { style: { flex: 1, color: ink, fontSize: 16 }, children: label }), _jsx(Text, { style: { color: ink, fontSize: 13 }, children: count })] }), _jsx(View, { pointerEvents: "none", style: { height: 3, marginLeft: 30, backgroundColor: ink + '20', borderRadius: 2, overflow: 'hidden' }, children: _jsx(View, { style: { height: 3, width: `${votes.length ? count / votes.length * 100 : 0}%`, backgroundColor: accent } }) })] }, index);
55
56
  }), !!error && _jsx(Text, { accessibilityRole: "alert", style: { color: ink }, children: error })] }), showVotes && (renderDetails ? renderDetails(details, () => setShowVotes(false)) : _jsx(ExtraPopup, { title: uiText("Votes"), background: background, foreground: foreground, onClose: () => setShowVotes(false), children: _jsx(ScrollView, { style: { flexShrink: 1 }, children: details }) }))] });
56
57
  }
package/dist/popup.js CHANGED
@@ -1,7 +1,8 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { Text } from './typography.js';
2
3
  import { useRobonoLocale } from './localization.js';
3
4
  import { createContext, useContext, useEffect, useLayoutEffect, useRef, useState, useCallback, useMemo } from 'react';
4
- import { Keyboard, KeyboardAvoidingView, Modal, Platform, Pressable, SafeAreaView, Text, View, useWindowDimensions } from 'react-native';
5
+ import { Keyboard, KeyboardAvoidingView, Modal, Platform, Pressable, SafeAreaView, View, useWindowDimensions } from 'react-native';
5
6
  import { MessageIcon } from './icons.js';
6
7
  /** Browser hosts can keep settings dialogs inside their embedded chat. */
7
8
  export const MessagingPopupSurface = createContext(null);
@@ -0,0 +1,10 @@
1
+ import React from 'react';
2
+ import type { MessagingInboxClient } from '@robono/messaging';
3
+ /** Shared, pane-bounded public group discovery. No user directory is required. */
4
+ export declare function MessagingPublicRooms({ inbox, accent, createRequestId, onClose, onJoined }: {
5
+ inbox: MessagingInboxClient;
6
+ accent: string;
7
+ createRequestId: () => string;
8
+ onClose: () => void;
9
+ onJoined: (id: string, name: string) => void;
10
+ }): React.JSX.Element;
@@ -0,0 +1,78 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useEffect, useRef, useState } from 'react';
3
+ import { ActivityIndicator, FlatList, Platform, Pressable, View } from 'react-native';
4
+ import { Text, TextInput } from './typography.js';
5
+ import { useRobonoLocale } from './localization.js';
6
+ import { MessagingPopup, messagingPopupColors as c } from './popup.js';
7
+ /** Shared, pane-bounded public group discovery. No user directory is required. */
8
+ export function MessagingPublicRooms({ inbox, accent, createRequestId, onClose, onJoined }) {
9
+ const { text, count } = useRobonoLocale();
10
+ const [query, setQuery] = useState(''), [rows, setRows] = useState([]), [next, setNext] = useState(null), [loading, setLoading] = useState(false), [joining, setJoining] = useState(null), [error, setError] = useState('');
11
+ const generation = useRef(0), controller = useRef(null), attempts = useRef(new Map()), busy = useRef(false), mounted = useRef(true);
12
+ const [visible, setVisible] = useState(true), joined = useRef(null);
13
+ const finish = () => { const result = joined.current; joined.current = null; if (result)
14
+ requestAnimationFrame(() => { if (mounted.current)
15
+ onJoined(result.id, result.name); }); };
16
+ const openJoined = (id, name) => { joined.current = { id, name }; setVisible(false); };
17
+ useEffect(() => { if (!visible && Platform.OS !== 'ios')
18
+ finish(); }, [visible]);
19
+ async function load(more = false) {
20
+ controller.current?.abort();
21
+ const abort = new AbortController();
22
+ controller.current = abort;
23
+ const version = ++generation.current;
24
+ setLoading(true);
25
+ setError('');
26
+ try {
27
+ const page = await inbox.publicRooms({ query, ...(more && next ? { after: next } : {}), signal: abort.signal });
28
+ if (!mounted.current || version !== generation.current)
29
+ return;
30
+ setRows(current => [...new Map([...(more ? current : []), ...page.rooms].map(room => [room.id, room])).values()]);
31
+ setNext(page.hasMore ? page.nextCursor : null);
32
+ }
33
+ catch (e) {
34
+ if (mounted.current && version === generation.current && !abort.signal.aborted)
35
+ setError(e instanceof Error ? e.message : text('Public groups could not be loaded.'));
36
+ }
37
+ finally {
38
+ if (mounted.current && version === generation.current)
39
+ setLoading(false);
40
+ }
41
+ }
42
+ useEffect(() => { mounted.current = true; return () => { mounted.current = false; ++generation.current; controller.current?.abort(); }; }, [inbox]);
43
+ useEffect(() => { ++generation.current; controller.current?.abort(); setRows([]); setNext(null); setLoading(true); const timer = setTimeout(() => void load(), 200); return () => { clearTimeout(timer); controller.current?.abort(); }; }, [inbox, query]);
44
+ async function join(room) {
45
+ if (busy.current)
46
+ return;
47
+ if (room.joined) {
48
+ openJoined(room.id, room.name);
49
+ return;
50
+ }
51
+ busy.current = true;
52
+ setJoining(room.id);
53
+ setError('');
54
+ let requestId = attempts.current.get(room.id);
55
+ if (!requestId) {
56
+ requestId = createRequestId();
57
+ attempts.current.set(room.id, requestId);
58
+ }
59
+ try {
60
+ const result = await inbox.joinPublicRoom(room.id, requestId);
61
+ if (mounted.current)
62
+ openJoined(result.conversationId, result.name);
63
+ }
64
+ catch (e) {
65
+ if (mounted.current) {
66
+ setError(e instanceof Error ? e.message : text('This group could not be joined.'));
67
+ if (e?.code === 'room_join_expired')
68
+ attempts.current.delete(room.id);
69
+ }
70
+ }
71
+ finally {
72
+ busy.current = false;
73
+ if (mounted.current)
74
+ setJoining(null);
75
+ }
76
+ }
77
+ return _jsxs(MessagingPopup, { visible: visible, onDismiss: finish, title: text('Public groups'), background: c.background, onClose: onClose, children: [_jsx(TextInput, { accessibilityLabel: text('Search public groups'), placeholder: text('Search public groups'), placeholderTextColor: c.secondary, value: query, onChangeText: setQuery, maxLength: 200, style: { color: c.foreground, fontSize: 17, minHeight: 48, paddingHorizontal: 12, borderBottomWidth: 1, borderBottomColor: '#ffffff30' } }), !!error && _jsxs(View, { style: { padding: 12, gap: 4 }, children: [_jsx(Text, { accessibilityRole: "alert", style: { color: c.foreground }, children: error }), _jsx(Pressable, { accessibilityRole: "button", disabled: loading, onPress: () => void load(), style: { minHeight: 44, justifyContent: 'center' }, children: _jsx(Text, { style: { color: accent }, children: text('Retry') }) })] }), _jsx(FlatList, { data: rows, keyExtractor: r => r.id, style: { flex: 1 }, keyboardShouldPersistTaps: "handled", contentContainerStyle: { paddingHorizontal: 12, paddingBottom: 12 }, initialNumToRender: 15, renderItem: ({ item: room }) => _jsxs(View, { style: { flexDirection: 'row', gap: 12, alignItems: 'center', paddingVertical: 14, borderBottomWidth: 1, borderBottomColor: '#ffffff18' }, children: [_jsxs(View, { style: { flex: 1, gap: 4 }, children: [_jsx(Text, { style: { color: c.foreground, fontSize: 18, fontWeight: '600' }, children: room.name }), !!room.description && _jsx(Text, { numberOfLines: 3, style: { color: c.secondary, fontSize: 15, lineHeight: 21 }, children: room.description }), _jsx(Text, { style: { color: c.secondary, fontSize: 13 }, children: count(room.memberCount, '{count} member', '{count} members') })] }), _jsx(Pressable, { accessibilityRole: "button", accessibilityLabel: text(room.joined ? 'Open {name}' : 'Join {name}', { name: room.name }), accessibilityState: { disabled: !!joining || !room.joined && !room.joinable }, disabled: !!joining || !room.joined && !room.joinable, onPress: () => void join(room), style: { minHeight: 44, minWidth: 70, paddingHorizontal: 12, alignItems: 'center', justifyContent: 'center', borderRadius: 12, borderWidth: 1, borderColor: accent, opacity: !room.joined && !room.joinable ? .4 : 1 }, children: joining === room.id ? _jsx(ActivityIndicator, { color: accent }) : _jsx(Text, { style: { color: accent, fontWeight: '600' }, children: text(room.joined ? 'Open' : room.joinable ? 'Join' : 'Closed') }) })] }), ListEmptyComponent: !loading && !error ? _jsx(Text, { style: { color: c.secondary, paddingVertical: 24, textAlign: 'center' }, children: text(query.trim() ? 'No matching public groups.' : 'No public groups yet.') }) : null, ListFooterComponent: _jsxs(View, { children: [loading && _jsx(ActivityIndicator, { accessibilityLabel: text('Loading public groups'), color: accent, style: { padding: 16 } }), next && !loading && _jsx(Pressable, { accessibilityRole: "button", onPress: () => void load(true), style: { minHeight: 48, justifyContent: 'center', alignItems: 'center' }, children: _jsx(Text, { style: { color: accent }, children: text('Load more') }) })] }) })] });
78
+ }
@@ -1,8 +1,9 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { Text } from './typography.js';
2
3
  import { useRobonoLocale } from './localization.js';
3
4
  import { MessagingPopupHeader, messagingPopupBackdrop, MessagingMenuSurface } from './popup.js';
4
5
  import { useEffect, useRef, useState } from 'react';
5
- import { Platform, Pressable, ScrollView, Text, View, useWindowDimensions } from 'react-native';
6
+ import { Platform, Pressable, ScrollView, View, useWindowDimensions } from 'react-native';
6
7
  import { quickMessageOptions, validQuickMessage } from '@robono/messaging';
7
8
  import { MessageIcon, chatSurface } from './icons.js';
8
9
  /** Messenger's empty-composer shortcut and anchored picker. Selecting never sends. */
@@ -0,0 +1,22 @@
1
+ import React from 'react';
2
+ import type { MessagingClient } from '@robono/messaging';
3
+ /** Owners/moderators see current roles; the service decides whether each change is allowed. */
4
+ export declare function MessagingRoomModeration({ client, userId, participants, accent, onClose }: {
5
+ client: MessagingClient;
6
+ userId: string;
7
+ participants?: Record<string, {
8
+ name: string;
9
+ avatarUrl?: string;
10
+ }>;
11
+ accent: string;
12
+ onClose: () => void;
13
+ }): React.JSX.Element;
14
+ export declare function MessagingRoomRemoval({ client, message, accent, onClose }: {
15
+ client: MessagingClient;
16
+ message: {
17
+ id: string;
18
+ revision?: number;
19
+ };
20
+ accent: string;
21
+ onClose: () => void;
22
+ }): React.JSX.Element;