@robono/messaging-react-native 0.11.1 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +10 -0
- package/README.md +66 -7
- package/SBOM.spdx.json +242 -98
- package/dist/account.d.ts +1 -0
- package/dist/account.js +1 -1
- package/dist/expo-media.js +18 -4
- package/dist/expo-messenger.d.ts +5 -0
- package/dist/expo-messenger.js +3 -3
- package/dist/expo-screen.d.ts +10 -2
- package/dist/expo-screen.js +15 -7
- package/dist/expo-thumbnails.d.ts +0 -1
- package/dist/expo-thumbnails.js +17 -26
- package/dist/expo.d.ts +2 -0
- package/dist/expo.js +2 -0
- package/dist/group-editor.d.ts +1 -0
- package/dist/group-editor.js +1 -1
- package/dist/inbox.d.ts +1 -0
- package/dist/inbox.js +2 -2
- package/dist/index.d.ts +11 -1
- package/dist/index.js +15 -12
- package/dist/media-controller.d.ts +9 -2
- package/dist/media-controller.js +15 -10
- package/dist/media-screening.js +9 -3
- package/dist/preview-cache.d.ts +24 -0
- package/dist/preview-cache.js +59 -0
- package/dist/recording-permission.d.ts +12 -0
- package/dist/recording-permission.js +34 -0
- package/dist/sign-in.d.ts +76 -0
- package/dist/sign-in.js +46 -0
- package/package.json +4 -4
package/dist/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
import { MessagingRoomModeration, MessagingRoomRemoval } from './room-moderation.js';
|
|
3
|
+
export { MessagingConnectionState, MessagingSignIn, MessagingPhoneInput, MessagingChoicePicker } from './sign-in.js';
|
|
3
4
|
import { CustomMessage } from './custom-message.js';
|
|
4
5
|
import { MessagingTypography } from './typography.js';
|
|
5
6
|
import { Text, TextInput } from './typography.js';
|
|
@@ -349,11 +350,13 @@ function MessageMetadata({ mine, ink, pendingStatus }) {
|
|
|
349
350
|
}
|
|
350
351
|
export function MessagingBubble({ message, pendingStatus, previousMessage, nextMessage }) {
|
|
351
352
|
const { text: uiText, locale: uiLocale, number: uiNumber, count: uiCount } = useRobonoLocale();
|
|
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();
|
|
353
|
-
const
|
|
353
|
+
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, linkHandling } = useMessagingUI();
|
|
354
|
+
const deliveryStatus = pendingStatus ?? (message.senderId === userId && message.delivery?.state === 'pending' ? 'sending' : message.senderId === userId && message.delivery?.state === 'failed' ? 'failed' : undefined);
|
|
355
|
+
const candidateExtra = !message.custom && !pendingStatus && !message.deleted ? extrasState?.messages[message.id] : undefined;
|
|
356
|
+
const extra = candidateExtra?.address && (linkHandling?.addresses === false || linkHandling?.openAddress) ? undefined : candidateExtra;
|
|
354
357
|
const extraRef = useRef(null), socialRef = useRef(null);
|
|
355
358
|
const socialOrigin = useMessagingUI().socialOrigin ?? 'https://robono.com';
|
|
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));
|
|
359
|
+
const socialReady = linkHandling?.webLinks !== false && !linkHandling?.openUrl && !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));
|
|
357
360
|
const contextContactName = contact.name;
|
|
358
361
|
const mine = message.senderId === userId, c = design.colors, ink = message.kind === 'poll' ? c.foreground : mine ? c.outgoingText : c.incomingText;
|
|
359
362
|
const bubbleRadius = design.presentation?.bubbleShape === 'square' ? 2 : design.presentation?.bubbleShape === 'rounded' ? 16 : 8;
|
|
@@ -443,14 +446,14 @@ export function MessagingBubble({ message, pendingStatus, previousMessage, nextM
|
|
|
443
446
|
setBusy(false);
|
|
444
447
|
}
|
|
445
448
|
}
|
|
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);
|
|
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); };
|
|
449
|
+
const renderLinks = (text) => messageLinks(text).map((part, index) => part.url && linkHandling?.webLinks !== false ? _jsx(Text, { accessibilityRole: "link", onLongPress: menu, onPress: event => { event.stopPropagation(); void Promise.resolve().then(() => linkHandling?.openUrl ? linkHandling.openUrl(part.url, message) : Linking.openURL(part.url)).catch(() => setError(uiText("This link could not be opened."))); }, style: { textDecorationLine: 'underline' }, children: part.text }, index) : part.text);
|
|
450
|
+
const renderText = (text) => { const address = linkHandling?.addresses === false ? null : findChatAddress(text); return address ? _jsxs(_Fragment, { children: [renderLinks(text.slice(0, address.start)), _jsx(Text, { accessibilityRole: "link", onLongPress: menu, onPress: event => { event.stopPropagation(); void Promise.resolve().then(() => linkHandling?.openAddress ? linkHandling.openAddress(address.text, message) : 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); };
|
|
448
451
|
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())
|
|
449
452
|
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
453
|
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;
|
|
451
454
|
const canCopy = !!message.text && (!!copyText || Platform.OS === 'web');
|
|
452
455
|
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)) }) });
|
|
453
|
-
const footer = _jsx(MessageMetadata, { message: message, mine: mine, ink: visual ? '#ffffff' : ink, pendingStatus:
|
|
456
|
+
const footer = _jsx(MessageMetadata, { message: message, mine: mine, ink: visual ? '#ffffff' : ink, pendingStatus: deliveryStatus });
|
|
454
457
|
const bigEmoji = message.kind === 'text' && !message.replyTo && !extra && !canTranslate && !transformed && isBigEmojiText(message.text);
|
|
455
458
|
const showingTransform = !!transformed && !showOriginal && (transformed.kind === 'transcript' || canTranslate);
|
|
456
459
|
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") }) })] });
|
|
@@ -459,11 +462,11 @@ export function MessagingBubble({ message, pendingStatus, previousMessage, nextM
|
|
|
459
462
|
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;
|
|
460
463
|
const regionProps = { message, mine, appearance: design, pendingStatus };
|
|
461
464
|
const bodyContent = components?.MessageBody && !message.deleted ? _jsx(components.MessageBody, { ...regionProps, children: content }) : content;
|
|
462
|
-
const bubbleBody = _jsxs(_Fragment, { children: [!bigEmoji && (!mine || !
|
|
465
|
+
const bubbleBody = _jsxs(_Fragment, { children: [!bigEmoji && (!mine || !deliveryStatus) && _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())
|
|
463
466
|
return; if (extra?.address)
|
|
464
467
|
extraRef.current?.open();
|
|
465
468
|
else
|
|
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") }) })] }),
|
|
469
|
+
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") }) })] }), deliveryStatus === '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
470
|
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)
|
|
468
471
|
quoteGesture.webRef.current = node; }, ...quoteGesture.handlers, accessibilityActions: quoting ? [{ name: "quote", label: uiText("Quote message") }] : undefined, onAccessibilityAction: event => { if (quoting && event.nativeEvent.actionName === "quote")
|
|
469
472
|
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 })] });
|
|
@@ -969,15 +972,15 @@ export function MessagingComposer() {
|
|
|
969
972
|
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 }))] }));
|
|
970
973
|
}
|
|
971
974
|
/** Account navigation. Mount in the inbox/app toolbar, never in a contact menu. */
|
|
972
|
-
export function MessagingAccountMenu({ onNewGroup, onMaps, onPublicRooms } = {}) {
|
|
975
|
+
export function MessagingAccountMenu({ onNewConversation, onNewGroup, onMaps, onPublicRooms } = {}) {
|
|
973
976
|
const { text: uiText, locale: uiLocale, number: uiNumber, count: uiCount } = useRobonoLocale();
|
|
974
977
|
const { client, design, setSavedOpen, account, companyName } = useMessagingUI();
|
|
975
978
|
const [open, setOpen] = useState(false), [accountOpen, setAccountOpen] = useState(false), pending = useRef(null);
|
|
976
979
|
const button = useRef(null), viewport = useWindowDimensions(), [anchor, setAnchor] = useState({ x: 12, y: 12 });
|
|
977
980
|
const width = Math.min(218, viewport.width - 24);
|
|
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); });
|
|
981
|
+
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) - (onNewConversation ? 44 : 0) - (onNewGroup ? 44 : 0) - (onMaps ? 44 : 0) - (onPublicRooms ? 44 : 0))) }); setOpen(true); });
|
|
979
982
|
const dismissed = () => { const next = pending.current; if (!next)
|
|
980
|
-
return; pending.current = null; requestAnimationFrame(() => next === 'saved' ? setSavedOpen(true) : next === 'group' ? onNewGroup?.() : next === 'maps' ? onMaps?.() : next === 'rooms' ? onPublicRooms?.() : setAccountOpen(true)); };
|
|
983
|
+
return; pending.current = null; requestAnimationFrame(() => next === 'new-chat' ? onNewConversation?.() : next === 'saved' ? setSavedOpen(true) : next === 'group' ? onNewGroup?.() : next === 'maps' ? onMaps?.() : next === 'rooms' ? onPublicRooms?.() : setAccountOpen(true)); };
|
|
981
984
|
useEffect(() => { if (!open && Platform.OS !== 'ios')
|
|
982
985
|
dismissed(); }, [open]);
|
|
983
986
|
useEffect(() => { if (!open || Platform.OS !== 'web')
|
|
@@ -985,7 +988,7 @@ export function MessagingAccountMenu({ onNewGroup, onMaps, onPublicRooms } = {})
|
|
|
985
988
|
event.stopPropagation();
|
|
986
989
|
setOpen(false);
|
|
987
990
|
} }; document.addEventListener('keydown', key, true); return () => document.removeEventListener('keydown', key, true); }, [open]);
|
|
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) })] });
|
|
991
|
+
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) - (onNewConversation ? 44 : 0) - (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") })] }), onNewConversation && _jsxs(Pressable, { accessibilityRole: "button", accessibilityLabel: uiText("New chat"), onPress: () => { pending.current = 'new-chat'; setOpen(false); }, style: ({ pressed }) => ({ minHeight: 44, paddingHorizontal: 12, flexDirection: 'row', alignItems: 'center', gap: 10, backgroundColor: pressed ? '#ffffff12' : 'transparent' }), children: [_jsx(MessageIcon, { name: "edit", color: "#f5f5f5", size: 22 }), _jsx(Text, { style: { color: '#f5f5f5', fontSize: 15, fontWeight: '600' }, children: uiText("New chat") })] }), 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) })] });
|
|
989
992
|
}
|
|
990
993
|
function MessagingWaitingChanges() {
|
|
991
994
|
const { state, client, design } = useMessagingUI(), { text: uiText, count } = useRobonoLocale();
|
|
@@ -33,6 +33,8 @@ export interface MessagingMediaDevice {
|
|
|
33
33
|
/** Ordered local selections. No upload or send occurs in the picker. */
|
|
34
34
|
pickMany?(kind: Exclude<MediaKind, 'voice'>, settings: MessagingSettings, signal: AbortSignal, maximum: number, mixedLibrary?: boolean): Promise<LocalMessagingMedia[]>;
|
|
35
35
|
startRecording(maxDurationMs: number, signal: AbortSignal): Promise<void>;
|
|
36
|
+
/** True only during the operating system's microphone permission handoff. */
|
|
37
|
+
isRequestingRecordingPermission?(): boolean;
|
|
36
38
|
recordingStatus(): {
|
|
37
39
|
durationMs: number;
|
|
38
40
|
recording: boolean;
|
|
@@ -106,10 +108,15 @@ export declare class MessagingMediaController {
|
|
|
106
108
|
finishRecording(interrupted?: boolean): Promise<void>;
|
|
107
109
|
send(): Promise<void>;
|
|
108
110
|
/** View an owned cache copy of a draft. Closing never removes the original draft. */
|
|
109
|
-
previewLocal(file: LocalMessagingMedia, kind: MediaKind): Promise<void>;
|
|
111
|
+
previewLocal(file: LocalMessagingMedia, kind: MediaKind, message?: MessagingMessage): Promise<void>;
|
|
110
112
|
open(message: MessagingMessage): Promise<void>;
|
|
111
113
|
/** Save/share a local copy of a playing stream without replacing its playback source. */
|
|
112
|
-
exportFile<T>(save: (file: LocalMessagingMedia
|
|
114
|
+
exportFile<T>(save: (file: LocalMessagingMedia, context: {
|
|
115
|
+
message: MessagingMessage | null;
|
|
116
|
+
signal: AbortSignal;
|
|
117
|
+
}) => Promise<T>, options?: {
|
|
118
|
+
reauthorize?: boolean;
|
|
119
|
+
}): Promise<T>;
|
|
113
120
|
/** Recover a failed/expired native stream once through fresh authorized download. */
|
|
114
121
|
fallbackStream(uri: string, positionMs: number, autoPlay: boolean): Promise<void>;
|
|
115
122
|
private openMedia;
|
package/dist/media-controller.js
CHANGED
|
@@ -299,7 +299,7 @@ export class MessagingMediaController {
|
|
|
299
299
|
}
|
|
300
300
|
}
|
|
301
301
|
/** View an owned cache copy of a draft. Closing never removes the original draft. */
|
|
302
|
-
async previewLocal(file, kind) {
|
|
302
|
+
async previewLocal(file, kind, message) {
|
|
303
303
|
const intent = ++this.intent;
|
|
304
304
|
await this.closeCurrent();
|
|
305
305
|
if (intent !== this.intent || this.disposed) {
|
|
@@ -307,25 +307,30 @@ export class MessagingMediaController {
|
|
|
307
307
|
return;
|
|
308
308
|
}
|
|
309
309
|
this.conversationId = this.options.getConversationId();
|
|
310
|
+
this.openedMessage = message ?? null;
|
|
311
|
+
this.abort = new AbortController();
|
|
310
312
|
this.update({ phase: 'viewing', kind, file, error: null, progress: 0 });
|
|
311
313
|
}
|
|
312
314
|
open(message) { return this.openMedia(message); }
|
|
313
315
|
/** Save/share a local copy of a playing stream without replacing its playback source. */
|
|
314
|
-
async exportFile(save) {
|
|
315
|
-
const file = this.state.file, generation = this.generation, message = this.openedMessage;
|
|
316
|
+
async exportFile(save, options = {}) {
|
|
317
|
+
const file = this.state.file, generation = this.generation, message = this.openedMessage, conversationId = this.conversationId;
|
|
316
318
|
if (this.disposed || !file || this.state.phase !== 'viewing')
|
|
317
319
|
throw Error('The media viewer is closed.');
|
|
318
|
-
|
|
319
|
-
|
|
320
|
+
const signal = this.abort?.signal;
|
|
321
|
+
if (!signal)
|
|
322
|
+
throw Error('The attachment is unavailable.');
|
|
323
|
+
if (!file.stream && !options.reauthorize)
|
|
324
|
+
return save(file, { message, signal });
|
|
320
325
|
if (!message || !this.abort)
|
|
321
326
|
throw Error('The attachment is unavailable.');
|
|
322
|
-
const copy = await this.options.storage.download(message, { signal: this.abort.signal, onProgress: () => { } });
|
|
327
|
+
const copy = await this.options.storage.download(options.reauthorize ? { ...message, attachmentDownload: undefined } : message, { signal: this.abort.signal, onProgress: () => { } });
|
|
323
328
|
try {
|
|
324
|
-
if (!this.isCurrent(generation) || this.options.getConversationId() !==
|
|
329
|
+
if (!this.isCurrent(generation) || this.options.getConversationId() !== conversationId)
|
|
325
330
|
throw Error('The media viewer is closed.');
|
|
326
331
|
if (copy.stream)
|
|
327
332
|
throw Error('A local file is required to save this attachment.');
|
|
328
|
-
return await save(copy);
|
|
333
|
+
return await save(copy, { message, signal });
|
|
329
334
|
}
|
|
330
335
|
finally {
|
|
331
336
|
await this.options.device.remove(copy).catch(() => { });
|
|
@@ -335,7 +340,7 @@ export class MessagingMediaController {
|
|
|
335
340
|
fallbackStream(uri, positionMs, autoPlay) {
|
|
336
341
|
if (!this.state.file?.stream || this.state.file.uri !== uri || !this.openedMessage)
|
|
337
342
|
return Promise.resolve();
|
|
338
|
-
return this.openMedia(this.openedMessage, { download: true, positionMs, autoPlay });
|
|
343
|
+
return this.openMedia({ ...this.openedMessage, attachmentDownload: undefined }, { download: true, positionMs, autoPlay });
|
|
339
344
|
}
|
|
340
345
|
async openMedia(message, recovery) {
|
|
341
346
|
const intent = ++this.intent;
|
|
@@ -404,7 +409,7 @@ export class MessagingMediaController {
|
|
|
404
409
|
}
|
|
405
410
|
async interrupt() { if (this.state.phase === 'recording')
|
|
406
411
|
await this.finishRecording(true);
|
|
407
|
-
else if (this.state.phase === 'preparing')
|
|
412
|
+
else if (this.state.phase === 'preparing' && !this.options.device.isRequestingRecordingPermission?.())
|
|
408
413
|
await this.cancel(); }
|
|
409
414
|
async dispose() { this.disposed = true; for (const abort of this.preparedAborts.values())
|
|
410
415
|
abort.abort(); this.forwardUploads.clear(); await this.cancel(); await Promise.all([...this.prepared.keys()].map(id => this.releasePrepared(id))); this.listeners.clear(); }
|
package/dist/media-screening.js
CHANGED
|
@@ -1,10 +1,16 @@
|
|
|
1
|
+
// React Native AbortSignal implementations need not expose throwIfAborted/reason.
|
|
2
|
+
function checkCancelled(signal) {
|
|
3
|
+
if (signal.aborted)
|
|
4
|
+
throw Object.assign(Error('Cancelled'), { name: 'AbortError' });
|
|
5
|
+
}
|
|
1
6
|
/** Wait between short authenticated requests; never occupy a backend request for a video job. */
|
|
2
7
|
export async function awaitMediaScreening(request, signal, onPending, options = {}) {
|
|
3
8
|
const deadline = Date.now() + (options.timeoutMs ?? 600000);
|
|
4
9
|
while (true) {
|
|
5
|
-
signal
|
|
10
|
+
checkCancelled(signal);
|
|
6
11
|
try {
|
|
7
12
|
const value = await request();
|
|
13
|
+
checkCancelled(signal);
|
|
8
14
|
const error = value?.error;
|
|
9
15
|
if (error?.code === 'screening_pending')
|
|
10
16
|
throw Object.assign(Error(error.message), { code: error.code });
|
|
@@ -15,12 +21,12 @@ export async function awaitMediaScreening(request, signal, onPending, options =
|
|
|
15
21
|
catch (e) {
|
|
16
22
|
if (e.code !== 'screening_pending')
|
|
17
23
|
throw e;
|
|
18
|
-
signal
|
|
24
|
+
checkCancelled(signal);
|
|
19
25
|
onPending?.();
|
|
20
26
|
if (Date.now() >= deadline)
|
|
21
27
|
throw Object.assign(Error('This attachment is still being checked. Retry to check its status; it has not been sent.'), { code: 'screening_pending', retryable: true });
|
|
22
28
|
await new Promise((resolve, reject) => {
|
|
23
|
-
const cancel = () => { clearTimeout(timer); signal.removeEventListener('abort', cancel); reject(Error('Cancelled')); };
|
|
29
|
+
const cancel = () => { clearTimeout(timer); signal.removeEventListener('abort', cancel); reject(Object.assign(Error('Cancelled'), { name: 'AbortError' })); };
|
|
24
30
|
const timer = setTimeout(() => { signal.removeEventListener('abort', cancel); resolve(); }, options.pollMs ?? 3000);
|
|
25
31
|
signal.addEventListener('abort', cancel, { once: true });
|
|
26
32
|
if (signal.aborted)
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
type Preview = {
|
|
2
|
+
uri?: string;
|
|
3
|
+
waveform?: number[];
|
|
4
|
+
};
|
|
5
|
+
/** Memory-only, bounded previews. Call clear on identity/permission boundaries.
|
|
6
|
+
* Different images can progress independently without unbounded native work. */
|
|
7
|
+
export declare class MessagingPreviewCache {
|
|
8
|
+
private options;
|
|
9
|
+
private now;
|
|
10
|
+
private entries;
|
|
11
|
+
private queue;
|
|
12
|
+
private running;
|
|
13
|
+
constructor(options?: {
|
|
14
|
+
concurrency: number;
|
|
15
|
+
maximum: number;
|
|
16
|
+
ttlMs: number;
|
|
17
|
+
timeoutMs: number;
|
|
18
|
+
}, now?: () => number);
|
|
19
|
+
get(key: string, load: (signal: AbortSignal) => Promise<Preview>): Promise<Preview>;
|
|
20
|
+
clear(): void;
|
|
21
|
+
private drop;
|
|
22
|
+
private pump;
|
|
23
|
+
}
|
|
24
|
+
export {};
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/** Memory-only, bounded previews. Call clear on identity/permission boundaries.
|
|
2
|
+
* Different images can progress independently without unbounded native work. */
|
|
3
|
+
export class MessagingPreviewCache {
|
|
4
|
+
options;
|
|
5
|
+
now;
|
|
6
|
+
entries = new Map();
|
|
7
|
+
queue = [];
|
|
8
|
+
running = 0;
|
|
9
|
+
constructor(options = { concurrency: 2, maximum: 60, ttlMs: 60000, timeoutMs: 15000 }, now = Date.now) {
|
|
10
|
+
this.options = options;
|
|
11
|
+
this.now = now;
|
|
12
|
+
}
|
|
13
|
+
get(key, load) {
|
|
14
|
+
const previous = this.entries.get(key);
|
|
15
|
+
if (previous && previous.expires > this.now()) {
|
|
16
|
+
this.entries.delete(key);
|
|
17
|
+
this.entries.set(key, previous);
|
|
18
|
+
return previous.promise;
|
|
19
|
+
}
|
|
20
|
+
if (previous)
|
|
21
|
+
this.drop(previous);
|
|
22
|
+
let resolve;
|
|
23
|
+
const promise = new Promise(r => resolve = r);
|
|
24
|
+
const entry = { key, abort: new AbortController(), load, promise, resolve, expires: Infinity, started: false };
|
|
25
|
+
this.entries.set(key, entry);
|
|
26
|
+
this.queue.push(entry);
|
|
27
|
+
while (this.entries.size > this.options.maximum)
|
|
28
|
+
this.drop(this.entries.values().next().value);
|
|
29
|
+
this.pump();
|
|
30
|
+
return promise;
|
|
31
|
+
}
|
|
32
|
+
clear() { for (const entry of this.entries.values())
|
|
33
|
+
this.drop(entry); this.queue = []; }
|
|
34
|
+
drop(entry) { if (this.entries.get(entry.key) === entry)
|
|
35
|
+
this.entries.delete(entry.key); entry.abort.abort(); entry.resolve({}); }
|
|
36
|
+
pump() {
|
|
37
|
+
while (this.running < this.options.concurrency && this.queue.length) {
|
|
38
|
+
const entry = this.queue.shift();
|
|
39
|
+
if (entry.abort.signal.aborted)
|
|
40
|
+
continue;
|
|
41
|
+
this.running++;
|
|
42
|
+
entry.started = true;
|
|
43
|
+
// Some native thumbnail operations cannot be cancelled once started. Release
|
|
44
|
+
// the queue slot on cancellation/deadline and ignore their eventual result.
|
|
45
|
+
// Otherwise two stalled decoders would stop the entire conversation forever.
|
|
46
|
+
let onAbort;
|
|
47
|
+
const cancelled = new Promise(resolve => { onAbort = () => resolve({}); entry.abort.signal.addEventListener('abort', onAbort, { once: true }); });
|
|
48
|
+
const timer = setTimeout(() => this.drop(entry), this.options.timeoutMs);
|
|
49
|
+
void Promise.race([Promise.resolve().then(() => entry.abort.signal.aborted ? {} : entry.load(entry.abort.signal)), cancelled]).then(value => {
|
|
50
|
+
if (entry.abort.signal.aborted || this.entries.get(entry.key) !== entry)
|
|
51
|
+
return;
|
|
52
|
+
entry.expires = this.now() + this.options.ttlMs;
|
|
53
|
+
entry.resolve(value);
|
|
54
|
+
if (!value.uri && !value.waveform)
|
|
55
|
+
this.entries.delete(entry.key);
|
|
56
|
+
}, () => this.drop(entry)).finally(() => { clearTimeout(timer); entry.abort.signal.removeEventListener('abort', onAbort); this.running--; this.pump(); });
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
type Permission = {
|
|
2
|
+
granted: boolean;
|
|
3
|
+
};
|
|
4
|
+
/** Permission UI may temporarily background the app. Capture still requires a
|
|
5
|
+
* foreground app after the prompt closes; it never starts while suspended. */
|
|
6
|
+
export declare function prepareRecordingPermission(options: {
|
|
7
|
+
read: () => Promise<Permission>;
|
|
8
|
+
request: () => Promise<Permission>;
|
|
9
|
+
state: () => string | null;
|
|
10
|
+
subscribe: (changed: () => void) => () => void;
|
|
11
|
+
}, signal: AbortSignal): Promise<void>;
|
|
12
|
+
export {};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/** Permission UI may temporarily background the app. Capture still requires a
|
|
2
|
+
* foreground app after the prompt closes; it never starts while suspended. */
|
|
3
|
+
export async function prepareRecordingPermission(options, signal) {
|
|
4
|
+
const check = () => { if (signal.aborted)
|
|
5
|
+
throw Object.assign(Error('Cancelled.'), { name: 'AbortError' }); };
|
|
6
|
+
check();
|
|
7
|
+
if (options.state() !== 'active')
|
|
8
|
+
throw Error('Return to the conversation to record a voice message.');
|
|
9
|
+
let permission = await options.read();
|
|
10
|
+
check();
|
|
11
|
+
if (!permission.granted) {
|
|
12
|
+
permission = await options.request();
|
|
13
|
+
check();
|
|
14
|
+
}
|
|
15
|
+
if (!permission.granted)
|
|
16
|
+
throw Error('Microphone permission is needed to record a voice message.');
|
|
17
|
+
if (options.state() === 'active')
|
|
18
|
+
return;
|
|
19
|
+
await new Promise((resolve, reject) => {
|
|
20
|
+
let remove = () => { };
|
|
21
|
+
const finish = (error) => { clearTimeout(timer); remove(); signal.removeEventListener('abort', cancel); error ? reject(error) : resolve(); };
|
|
22
|
+
const cancel = () => finish(Object.assign(Error('Cancelled.'), { name: 'AbortError' }));
|
|
23
|
+
const changed = () => { if (options.state() === 'active')
|
|
24
|
+
finish(); };
|
|
25
|
+
const timer = setTimeout(() => finish(Error('Return to the conversation and try recording again.')), 2000);
|
|
26
|
+
remove = options.subscribe(changed);
|
|
27
|
+
signal.addEventListener('abort', cancel, { once: true });
|
|
28
|
+
if (signal.aborted)
|
|
29
|
+
cancel();
|
|
30
|
+
else
|
|
31
|
+
changed();
|
|
32
|
+
});
|
|
33
|
+
check();
|
|
34
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import React, { type ReactNode } from 'react';
|
|
2
|
+
import { type MessagingAppearance } from '@robono/messaging';
|
|
3
|
+
/** Optional presentation only. The customer's existing identity provider owns all verification. */
|
|
4
|
+
export interface MessagingSignInProps {
|
|
5
|
+
appearance?: MessagingAppearance;
|
|
6
|
+
logo?: ReactNode;
|
|
7
|
+
stage: 'phone' | 'code' | 'profile';
|
|
8
|
+
/** Country selector and phone input supplied by the host, using MessagingPhoneInput if desired. */
|
|
9
|
+
phoneField: ReactNode;
|
|
10
|
+
code: string;
|
|
11
|
+
onCodeChange: (code: string) => void;
|
|
12
|
+
name: string;
|
|
13
|
+
onNameChange: (name: string) => void;
|
|
14
|
+
profileFields?: ReactNode;
|
|
15
|
+
consent: {
|
|
16
|
+
checked: boolean;
|
|
17
|
+
onChange: (checked: boolean) => void;
|
|
18
|
+
title: string;
|
|
19
|
+
details: ReactNode;
|
|
20
|
+
};
|
|
21
|
+
busy?: boolean;
|
|
22
|
+
disabled?: boolean;
|
|
23
|
+
error?: string;
|
|
24
|
+
/** Remaining lifetime reported by the identity provider. Never invent a verification deadline. */
|
|
25
|
+
secondsRemaining?: number;
|
|
26
|
+
onSubmit: () => void;
|
|
27
|
+
onChangePhone: () => void;
|
|
28
|
+
labels?: Partial<Record<'phoneSubtitle' | 'codeSubtitle' | 'profileSubtitle' | 'phone' | 'name' | 'namePlaceholder' | 'continue' | 'verify' | 'sendAgain' | 'busy' | 'changePhone' | 'code' | 'expired', string>>;
|
|
29
|
+
}
|
|
30
|
+
/** Original Messenger's card, typography, consent row and code cells, shared by web and native. */
|
|
31
|
+
export declare function MessagingSignIn(p: MessagingSignInProps): React.JSX.Element;
|
|
32
|
+
export interface MessagingPhoneInputProps {
|
|
33
|
+
value: string;
|
|
34
|
+
onChange: (value: string) => void;
|
|
35
|
+
countryName: string;
|
|
36
|
+
callingCode: string;
|
|
37
|
+
flag: string;
|
|
38
|
+
onChooseCountry: () => void;
|
|
39
|
+
disabled?: boolean;
|
|
40
|
+
placeholder?: string;
|
|
41
|
+
label?: string;
|
|
42
|
+
appearance?: MessagingAppearance;
|
|
43
|
+
}
|
|
44
|
+
/** Same field proportions and divider as Messenger, with host-owned country data. */
|
|
45
|
+
export declare function MessagingPhoneInput(p: MessagingPhoneInputProps): React.JSX.Element;
|
|
46
|
+
export interface MessagingChoice {
|
|
47
|
+
value: string;
|
|
48
|
+
label: string;
|
|
49
|
+
detail?: string;
|
|
50
|
+
symbol?: string;
|
|
51
|
+
}
|
|
52
|
+
export interface MessagingChoicePickerProps {
|
|
53
|
+
visible: boolean;
|
|
54
|
+
title: string;
|
|
55
|
+
closeLabel?: string;
|
|
56
|
+
choices: ReadonlyArray<MessagingChoice>;
|
|
57
|
+
selected: string;
|
|
58
|
+
onSelect: (value: string) => void;
|
|
59
|
+
onClose: () => void;
|
|
60
|
+
appearance?: MessagingAppearance;
|
|
61
|
+
}
|
|
62
|
+
/** The same compact choice list for customer sign-in, language and country fields. */
|
|
63
|
+
export declare function MessagingChoicePicker(p: MessagingChoicePickerProps): React.JSX.Element;
|
|
64
|
+
/** The same connection/recovery screen for customer websites and phone apps. */
|
|
65
|
+
export interface MessagingConnectionStateProps {
|
|
66
|
+
appearance?: MessagingAppearance;
|
|
67
|
+
error?: string;
|
|
68
|
+
onRetry?: () => void;
|
|
69
|
+
onSignOut?: () => void;
|
|
70
|
+
labels?: {
|
|
71
|
+
loading?: string;
|
|
72
|
+
retry?: string;
|
|
73
|
+
signOut?: string;
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
export declare function MessagingConnectionState({ appearance, error, onRetry, onSignOut, labels }: MessagingConnectionStateProps): React.JSX.Element;
|
package/dist/sign-in.js
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { useRef, useState } from 'react';
|
|
3
|
+
import { ActivityIndicator, FlatList, Image, Keyboard, KeyboardAvoidingView, Platform, Pressable, ScrollView, StyleSheet, Text, TextInput, View } from 'react-native';
|
|
4
|
+
import { defaultMessagingSettings } from '@robono/messaging';
|
|
5
|
+
import { RobonoIcon } from './visual/RobonoIcon.js';
|
|
6
|
+
import { MessagingMenuSurface, MessagingPopupHost } from './popup.js';
|
|
7
|
+
function surfaces(background) {
|
|
8
|
+
const hex = /^#[\da-f]{6}$/i.test(background) ? background : '#050505';
|
|
9
|
+
const rgb = [1, 3, 5].map(i => parseInt(hex.slice(i, i + 2), 16));
|
|
10
|
+
const sign = rgb.reduce((a, b) => a + b, 0) > 384 ? -1 : 1;
|
|
11
|
+
const offset = (n) => '#' + rgb.map(v => Math.max(0, Math.min(255, v + sign * n)).toString(16).padStart(2, '0')).join('');
|
|
12
|
+
return { card: offset(12), field: offset(24), border: offset(37) };
|
|
13
|
+
}
|
|
14
|
+
/** Original Messenger's card, typography, consent row and code cells, shared by web and native. */
|
|
15
|
+
export function MessagingSignIn(p) {
|
|
16
|
+
const a = p.appearance ?? defaultMessagingSettings().appearance, c = a.colors, surface = surfaces(c.background);
|
|
17
|
+
const labels = { phoneSubtitle: 'Enter your mobile number to continue.', codeSubtitle: 'Enter the 6-digit code.', profileSubtitle: 'Finish setting up your account.', phone: 'Mobile number', name: 'Name', namePlaceholder: 'Your name', continue: 'Continue', verify: 'Verify', sendAgain: 'Send a new code', busy: 'Checking...', changePhone: 'Use a different number', code: 'Verification code', expired: 'Code expired', ...p.labels };
|
|
18
|
+
const input = useRef(null), [focused, setFocused] = useState(false);
|
|
19
|
+
const expired = p.secondsRemaining !== undefined && p.secondsRemaining <= 0;
|
|
20
|
+
const disabled = !!p.busy || !!p.disabled || (p.stage === 'phone' && !p.consent.checked) || (p.stage === 'code' && !expired && p.code.length !== 6) || (p.stage === 'profile' && !p.name.trim());
|
|
21
|
+
const label = p.busy ? labels.busy : p.stage === 'code' ? (expired ? labels.sendAgain : labels.verify) : labels.continue;
|
|
22
|
+
return _jsx(MessagingPopupHost, { children: _jsx(KeyboardAvoidingView, { behavior: Platform.OS === 'ios' ? 'padding' : undefined, style: { flex: 1, minHeight: 0, backgroundColor: c.background }, children: _jsx(ScrollView, { keyboardShouldPersistTaps: "handled", contentContainerStyle: s.page, children: _jsxs(Pressable, { accessible: false, onPress: Keyboard.dismiss, style: [s.card, { backgroundColor: surface.card, borderColor: surface.border }], children: [p.logo ? _jsx(View, { style: s.logo, children: p.logo }) : a.logoUrl ? _jsx(Image, { accessibilityLabel: a.name, source: { uri: a.logoUrl }, style: s.logo, resizeMode: "contain" }) : _jsx(Text, { style: [s.brand, { color: c.foreground }], children: a.name }), _jsx(Text, { style: { color: c.secondary, fontSize: 16 }, children: p.stage === 'phone' ? labels.phoneSubtitle : p.stage === 'code' ? labels.codeSubtitle : labels.profileSubtitle }), p.stage === 'phone' ? _jsxs(_Fragment, { children: [_jsx(Text, { style: [s.label, { color: c.foreground }], children: labels.phone }), p.phoneField, _jsxs(View, { style: s.consentRow, children: [_jsx(Pressable, { accessibilityRole: "checkbox", accessibilityLabel: p.consent.title, accessibilityState: { checked: p.consent.checked, disabled: !!p.busy }, disabled: p.busy, hitSlop: 8, onPress: () => { Keyboard.dismiss(); p.consent.onChange(!p.consent.checked); }, style: [s.checkbox, { borderColor: c.accent, backgroundColor: p.consent.checked ? c.accent : 'transparent' }], children: p.consent.checked && _jsx(RobonoIcon, { name: "checkmark", size: 17, color: c.background }) }), _jsxs(View, { style: { flex: 1, minWidth: 0, gap: 4 }, children: [_jsx(Text, { style: [s.consentTitle, { color: c.foreground }], children: p.consent.title }), _jsx(Text, { style: [s.consentDetails, { color: c.secondary }], children: p.consent.details })] })] })] }) : p.stage === 'profile' ? _jsxs(_Fragment, { children: [_jsx(Text, { style: [s.label, { color: c.foreground }], children: labels.name }), _jsx(TextInput, { accessibilityLabel: labels.name, value: p.name, onChangeText: p.onNameChange, editable: !p.busy, maxLength: 80, autoCapitalize: "words", textContentType: "name", placeholder: labels.namePlaceholder, placeholderTextColor: c.secondary, style: [s.input, { color: c.foreground, backgroundColor: surface.field, borderColor: surface.border }] }), p.profileFields] }) : _jsxs(_Fragment, { children: [_jsxs(Pressable, { accessible: false, onPress: () => input.current?.focus(), style: { minHeight: 58, position: 'relative' }, children: [_jsx(View, { accessible: false, style: s.codeRow, children: Array.from({ length: 6 }, (_, i) => _jsx(View, { style: [s.codeCell, { backgroundColor: surface.field, borderColor: focused && i === Math.min(p.code.length, 5) ? c.accent : p.code[i] ? c.secondary : surface.border, borderWidth: focused && i === Math.min(p.code.length, 5) ? 2 : 1 }], children: _jsx(Text, { style: { fontSize: 28, fontWeight: '700', color: c.foreground }, children: p.code[i] ?? '' }) }, i)) }), _jsx(TextInput, { ref: input, accessibilityLabel: labels.code, value: p.code, onChangeText: v => p.onCodeChange(v.replace(/\D/g, '').slice(0, 6)), onFocus: () => setFocused(true), onBlur: () => setFocused(false), editable: !p.busy && !expired, keyboardType: "number-pad", autoComplete: "sms-otp", textContentType: "oneTimeCode", maxLength: 6, caretHidden: true, style: s.hiddenInput })] }), p.secondsRemaining !== undefined && _jsxs(View, { style: s.countdown, children: [_jsx(RobonoIcon, { name: "time-outline", size: 17, color: c.secondary }), _jsx(Text, { accessibilityLiveRegion: expired ? 'polite' : 'none', style: { color: expired ? '#EF4444' : c.secondary, fontSize: 15, fontWeight: '700', fontVariant: ['tabular-nums'] }, children: expired ? labels.expired : Math.floor(p.secondsRemaining / 60) + ':' + String(p.secondsRemaining % 60).padStart(2, '0') })] })] }), !!p.error && _jsx(Text, { accessibilityRole: "alert", style: { color: '#EF4444', fontSize: 14, lineHeight: 19 }, children: p.error }), _jsx(Pressable, { accessibilityRole: "button", accessibilityLabel: label, accessibilityState: { disabled, busy: !!p.busy }, disabled: disabled, onPress: p.onSubmit, style: [s.button, { backgroundColor: c.accent, opacity: disabled ? .45 : 1 }], children: _jsx(Text, { style: { color: c.background, fontWeight: '800' }, children: label }) }), p.stage !== 'phone' && _jsx(Pressable, { disabled: p.busy, accessibilityRole: "button", onPress: p.onChangePhone, children: _jsx(Text, { style: { color: c.accent, fontWeight: '600', textAlign: 'center', paddingVertical: 12 }, children: labels.changePhone }) })] }) }) }) });
|
|
23
|
+
}
|
|
24
|
+
/** Same field proportions and divider as Messenger, with host-owned country data. */
|
|
25
|
+
export function MessagingPhoneInput(p) { const c = (p.appearance ?? defaultMessagingSettings().appearance).colors, surface = surfaces(c.background); return _jsxs(View, { style: [s.phoneRow, { backgroundColor: surface.field, borderColor: surface.border }], children: [_jsxs(Pressable, { accessibilityRole: "button", accessibilityLabel: 'Country, ' + p.countryName, disabled: p.disabled, hitSlop: 4, onPress: () => { Keyboard.dismiss(); p.onChooseCountry(); }, style: s.country, children: [_jsx(Text, { style: { fontSize: 19 }, children: p.flag }), _jsx(Text, { style: { fontSize: 17, fontWeight: '700', color: c.foreground }, children: p.callingCode }), _jsx(RobonoIcon, { name: "chevron-down", size: 16, color: c.secondary })] }), _jsx(View, { style: [s.divider, { backgroundColor: surface.border }] }), _jsx(TextInput, { accessibilityLabel: p.label ?? 'Mobile number', value: p.value, onChangeText: p.onChange, editable: !p.disabled, keyboardType: "phone-pad", autoComplete: "tel-national", textContentType: "telephoneNumber", maxLength: 20, placeholder: p.placeholder, placeholderTextColor: c.secondary, style: { flex: 1, minHeight: 52, fontSize: 17, color: c.foreground } })] }); }
|
|
26
|
+
/** The same compact choice list for customer sign-in, language and country fields. */
|
|
27
|
+
export function MessagingChoicePicker(p) {
|
|
28
|
+
const c = (p.appearance ?? defaultMessagingSettings().appearance).colors, surface = surfaces(c.background);
|
|
29
|
+
return _jsx(MessagingMenuSurface, { visible: p.visible, onClose: p.onClose, children: () => _jsx(View, { style: { flex: 1, justifyContent: 'center', padding: 16, backgroundColor: 'rgba(0,0,0,.72)' }, children: _jsxs(View, { accessibilityViewIsModal: true, style: { maxHeight: '82%', width: '100%', maxWidth: 480, alignSelf: 'center', borderRadius: 8, borderWidth: 1, borderColor: surface.border, backgroundColor: c.background, overflow: 'hidden' }, children: [_jsxs(View, { style: { minHeight: 58, paddingStart: 16, paddingEnd: 58, justifyContent: 'center', borderBottomWidth: StyleSheet.hairlineWidth, borderBottomColor: surface.border, backgroundColor: surface.card }, children: [_jsx(Text, { accessibilityRole: "header", style: { color: c.foreground, fontSize: 18, fontWeight: '800' }, children: p.title }), _jsx(Pressable, { accessibilityRole: "button", accessibilityLabel: p.closeLabel ?? 'Close ' + p.title, onPress: p.onClose, style: { position: 'absolute', top: 0, end: 0, width: 58, height: 58, alignItems: 'center', justifyContent: 'center' }, children: _jsx(RobonoIcon, { name: "close", size: 22, color: c.foreground }) })] }), _jsx(FlatList, { data: p.choices, keyExtractor: row => row.value, keyboardShouldPersistTaps: "handled", contentContainerStyle: { padding: 8 }, renderItem: ({ item }) => _jsxs(Pressable, { accessibilityRole: "radio", accessibilityLabel: item.label + (item.detail ? ', ' + item.detail : ''), accessibilityState: { selected: p.selected === item.value }, onPress: () => p.onSelect(item.value), style: { minHeight: 58, borderRadius: 8, flexDirection: 'row', alignItems: 'center', gap: 12, paddingHorizontal: 12, paddingVertical: 8, backgroundColor: p.selected === item.value ? surface.field : 'transparent' }, children: [item.symbol && _jsx(Text, { style: { width: 31, fontSize: 21 }, children: item.symbol }), _jsxs(View, { style: { flex: 1, minWidth: 0 }, children: [_jsx(Text, { style: { color: c.foreground, fontSize: 16, fontWeight: '800' }, children: item.label }), item.detail && _jsx(Text, { style: { color: c.secondary, fontSize: 13, marginTop: 2 }, children: item.detail })] }), p.selected === item.value && _jsx(RobonoIcon, { name: "checkmark", size: 22, color: c.accent })] }) })] }) }) });
|
|
30
|
+
}
|
|
31
|
+
// Measurements copied from src/AppRoot.tsx authCard/phoneInput/codeCell/smsConsent styles.
|
|
32
|
+
// Keep behavior and presentation here: the consumer app must not fork these screens.
|
|
33
|
+
const s = StyleSheet.create({
|
|
34
|
+
page: { flexGrow: 1, justifyContent: 'center', padding: 24 }, card: { width: '100%', maxWidth: 480, alignSelf: 'center', borderRadius: 8, padding: 24, gap: 12, borderWidth: 1 },
|
|
35
|
+
logo: { width: 168, height: 48, alignSelf: 'center', marginBottom: 4 }, brand: { fontSize: 24, fontWeight: '800', textAlign: 'center', marginBottom: 4 },
|
|
36
|
+
label: { fontSize: 14, fontWeight: '700', marginBottom: -8 }, input: { minHeight: 52, borderWidth: 1, borderColor: '#2A2A2A', borderRadius: 8, paddingHorizontal: 16, fontSize: 17, backgroundColor: '#1D1D1D' },
|
|
37
|
+
phoneRow: { minHeight: 52, borderWidth: 1, borderColor: '#2A2A2A', borderRadius: 8, paddingHorizontal: 12, backgroundColor: '#1D1D1D', flexDirection: 'row', alignItems: 'center', direction: 'ltr' },
|
|
38
|
+
country: { minHeight: 50, flexDirection: 'row', alignItems: 'center', gap: 5, paddingRight: 8 }, divider: { width: StyleSheet.hairlineWidth, height: 28, marginRight: 12, backgroundColor: '#2A2A2A' },
|
|
39
|
+
consentRow: { flexDirection: 'row', alignItems: 'flex-start', gap: 8 }, checkbox: { width: 28, height: 28, borderWidth: 2, borderRadius: 6, alignItems: 'center', justifyContent: 'center', marginTop: 2 }, consentTitle: { fontSize: 14, fontWeight: '800', lineHeight: 19 }, consentDetails: { fontSize: 12, lineHeight: 17 },
|
|
40
|
+
codeRow: { minHeight: 58, flexDirection: 'row', justifyContent: 'center', gap: 8, direction: 'ltr' }, codeCell: { flex: 1, maxWidth: 52, minHeight: 58, borderRadius: 8, alignItems: 'center', justifyContent: 'center', backgroundColor: '#1D1D1D' }, hiddenInput: { position: 'absolute', top: 0, right: 0, bottom: 0, left: 0, zIndex: 2, color: 'transparent', backgroundColor: 'transparent', opacity: .01, fontSize: 1 },
|
|
41
|
+
countdown: { alignSelf: 'center', flexDirection: 'row', alignItems: 'center', gap: 6, direction: 'ltr' }, button: { minHeight: 48, borderRadius: 8, alignItems: 'center', justifyContent: 'center', paddingHorizontal: 16 },
|
|
42
|
+
});
|
|
43
|
+
export function MessagingConnectionState({ appearance, error, onRetry, onSignOut, labels }) {
|
|
44
|
+
const colors = (appearance ?? defaultMessagingSettings().appearance).colors;
|
|
45
|
+
return _jsxs(View, { style: { flex: 1, minHeight: 160, backgroundColor: colors.background, alignItems: 'center', justifyContent: 'center', padding: 24, gap: 16 }, children: [!error && _jsx(ActivityIndicator, { color: colors.accent }), _jsx(Text, { accessibilityRole: error ? 'alert' : undefined, accessibilityLiveRegion: "polite", style: { color: colors.foreground, fontSize: 17, lineHeight: 25, textAlign: 'center', maxWidth: 400 }, children: error || labels?.loading || 'Opening Messenger…' }), error && onRetry && _jsx(Pressable, { accessibilityRole: "button", onPress: onRetry, style: { minHeight: 48, minWidth: 180, paddingHorizontal: 24, alignItems: 'center', justifyContent: 'center', backgroundColor: colors.accent, borderRadius: 8 }, children: _jsx(Text, { style: { color: colors.background, fontSize: 16, fontWeight: '700' }, children: labels?.retry || 'Try again' }) }), error && onSignOut && _jsx(Pressable, { accessibilityRole: "button", onPress: onSignOut, style: { minHeight: 44, justifyContent: 'center', paddingHorizontal: 16 }, children: _jsx(Text, { style: { color: colors.accent, fontSize: 16 }, children: labels?.signOut || 'Sign out' }) })] });
|
|
46
|
+
}
|