@robono/messaging-react-native 0.7.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +9 -1
- package/README.md +40 -37
- package/SBOM.spdx.json +235 -163
- package/dist/account.js +5 -3
- package/dist/attachment-menu.js +3 -1
- package/dist/call-message.js +6 -4
- package/dist/calling.js +3 -1
- package/dist/conversation-details.js +9 -6
- package/dist/expo-connected-calling.d.ts +4 -2
- package/dist/expo-connected-calling.js +19 -4
- package/dist/expo-extras.js +4 -1
- package/dist/expo-media.js +27 -5
- package/dist/expo-messenger.d.ts +42 -4
- package/dist/expo-messenger.js +57 -7
- package/dist/expo-notifications.d.ts +2 -0
- package/dist/expo-notifications.js +8 -3
- package/dist/expo-outbox.js +22 -1
- package/dist/expo-screen.d.ts +1 -1
- package/dist/expo-screen.js +23 -14
- package/dist/expo-social.js +3 -1
- package/dist/expo.d.ts +1 -1
- package/dist/expo.js +1 -1
- package/dist/extras.js +9 -6
- package/dist/forward.js +6 -4
- package/dist/group-editor.js +7 -5
- package/dist/header-calls.js +7 -5
- package/dist/inbox-search.js +4 -2
- package/dist/inbox.js +9 -7
- package/dist/index.d.ts +6 -3
- package/dist/index.js +91 -61
- package/dist/localization.d.ts +24 -0
- package/dist/localization.js +12 -0
- package/dist/maps-connection.js +3 -1
- package/dist/media-ui.js +13 -7
- package/dist/message-menu.js +3 -1
- package/dist/native-outbox.d.ts +9 -1
- package/dist/native-outbox.js +7 -0
- package/dist/polls.js +18 -14
- package/dist/popup.js +6 -3
- package/dist/quick-send.js +4 -2
- package/dist/safety.js +9 -7
- package/dist/saved.js +5 -3
- package/dist/scheduled.js +17 -13
- package/dist/search.js +5 -3
- package/dist/social.js +7 -4
- package/dist/translation-settings.js +5 -3
- package/dist/voice-composer.js +9 -5
- package/package.json +9 -7
- package/setup.plugin.cjs +20 -0
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { type RobonoLocale } from '@robono/messaging';
|
|
3
|
+
/** Wrap multiple individual components once, or pass interfaceLocale to a supplied screen. */
|
|
4
|
+
export declare function RobonoLocaleProvider({ value, children }: {
|
|
5
|
+
value?: RobonoLocale;
|
|
6
|
+
children: React.ReactNode;
|
|
7
|
+
}): React.JSX.Element;
|
|
8
|
+
export declare function useRobonoLocale(): {
|
|
9
|
+
locale: string;
|
|
10
|
+
direction: "ltr" | "rtl";
|
|
11
|
+
configuration: {
|
|
12
|
+
locale: string;
|
|
13
|
+
direction: "ltr" | "rtl";
|
|
14
|
+
messages?: Readonly<Record<string, string>>;
|
|
15
|
+
plurals?: Readonly<Record<string, Partial<Record<Intl.LDMLPluralRule, string>>>>;
|
|
16
|
+
};
|
|
17
|
+
text: (source: string, values?: import("@robono/messaging").RobonoTextValues) => string;
|
|
18
|
+
count: (value: number, singular: string, plural: string, values?: import("@robono/messaging").RobonoTextValues) => string;
|
|
19
|
+
number: (value: number, options?: Intl.NumberFormatOptions) => string;
|
|
20
|
+
date: (value: Date | number, options?: Intl.DateTimeFormatOptions) => string;
|
|
21
|
+
};
|
|
22
|
+
export declare function localizeMessagingAppearance<T extends {
|
|
23
|
+
labels: Record<string, string>;
|
|
24
|
+
}>(appearance: T, text: (source: string) => string): T;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { createContext, useContext, useMemo } from 'react';
|
|
3
|
+
import { createRobonoLocalizer } from '@robono/messaging';
|
|
4
|
+
const defaultLocale = createRobonoLocalizer();
|
|
5
|
+
const LocaleContext = createContext(defaultLocale);
|
|
6
|
+
/** Wrap multiple individual components once, or pass interfaceLocale to a supplied screen. */
|
|
7
|
+
export function RobonoLocaleProvider({ value, children }) {
|
|
8
|
+
const parent = useContext(LocaleContext), locale = useMemo(() => value ? createRobonoLocalizer(value) : parent, [value, parent]);
|
|
9
|
+
return _jsx(LocaleContext.Provider, { value: locale, children: children });
|
|
10
|
+
}
|
|
11
|
+
export function useRobonoLocale() { return useContext(LocaleContext); }
|
|
12
|
+
export function localizeMessagingAppearance(appearance, text) { return { ...appearance, labels: Object.fromEntries(Object.entries(appearance.labels).map(([key, value]) => [key, text(value)])) }; }
|
package/dist/maps-connection.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useRobonoLocale } from './localization.js';
|
|
2
3
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
3
4
|
import { AppState, Platform, Pressable, Text, View } from 'react-native';
|
|
4
5
|
/** Recover settings after reconnect/foreground without capturing GPS or loading a map. */
|
|
@@ -38,5 +39,6 @@ export function useMessengerMaps(workspace) {
|
|
|
38
39
|
return { available, error, retry: refresh };
|
|
39
40
|
}
|
|
40
41
|
export function MessengerMapsNotice({ error, retry }) {
|
|
41
|
-
|
|
42
|
+
const { text: uiText, locale: uiLocale, number: uiNumber, count: uiCount } = useRobonoLocale();
|
|
43
|
+
return error ? _jsxs(View, { style: { backgroundColor: '#171717', paddingHorizontal: 16, paddingVertical: 8, flexDirection: 'row', gap: 12, alignItems: 'center' }, children: [_jsx(Text, { accessibilityRole: "alert", style: { color: '#FAFAFA', flex: 1 }, children: error }), _jsx(Pressable, { accessibilityRole: "button", accessibilityLabel: uiText("Retry Maps"), onPress: () => void retry(), style: { padding: 8 }, children: _jsx(Text, { style: { color: '#FF8A2C' }, children: uiText("Retry Maps") }) })] }) : null;
|
|
42
44
|
}
|
package/dist/media-ui.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useRobonoLocale } from './localization.js';
|
|
2
3
|
import { messengerVoiceWaveform } from './voice-waveform.js';
|
|
3
4
|
import { createPhotoGesture } from './photo-gesture.js';
|
|
4
5
|
import { useRef, useContext, useState, useEffect } from 'react';
|
|
@@ -7,6 +8,7 @@ import { MediaReadiness } from './media-readiness.js';
|
|
|
7
8
|
import { MessageIcon, duration, fileSize } from './icons.js';
|
|
8
9
|
/** Same chat controls on web, iOS and Android. Audio engines supply state, not a different UI. */
|
|
9
10
|
export function VoicePlayback({ ink, accent, waveform, showWaveform = true, footer, onLongPress, mine = false, heard = false, onPlayed, background, playing = false, loading = false, positionMs = 0, durationMs = 0, error, onToggle, onSeek }) {
|
|
11
|
+
const { text: uiText, locale: uiLocale, number: uiNumber, count: uiCount } = useRobonoLocale();
|
|
10
12
|
const width = useRef(1), held = useRef(false), [played, setPlayed] = useState(false), progress = durationMs ? Math.max(0, Math.min(1, positionMs / durationMs)) : 0;
|
|
11
13
|
useEffect(() => { if (playing && positionMs > 0) {
|
|
12
14
|
setPlayed(true);
|
|
@@ -14,18 +16,21 @@ export function VoicePlayback({ ink, accent, waveform, showWaveform = true, foot
|
|
|
14
16
|
} }, [playing, positionMs, onPlayed]);
|
|
15
17
|
const bars = showWaveform ? messengerVoiceWaveform(waveform, durationMs) : null, fill = mine ? ink : accent;
|
|
16
18
|
const hold = () => { held.current = true; onLongPress?.(); };
|
|
17
|
-
return _jsxs(View, { testID: "voice-message-player", style: { width: 220, maxWidth: '100%', minHeight: 44, borderRadius: 24, borderWidth: 1, borderColor: mine ? '#ffffff24' : '#2A2A2A', backgroundColor: background ?? (mine ? '#1F5F7A' : '#1D1D1D'), paddingLeft: 5, paddingRight: 8, paddingVertical: 3 }, children: [_jsxs(View, { style: { flexDirection: 'row', direction: 'ltr', alignItems: 'center', gap: 8 }, children: [_jsx(Pressable, { testID: "voice-play-control", accessibilityRole: "button", accessibilityLabel: playing ?
|
|
18
|
-
onToggle(); }, onLongPress: hold, style: { width: 36, height: 36, borderRadius: 18, paddingLeft: playing ? 0 : 2, backgroundColor: mine ? '#ffffff29' : heard || played ? accent : '#2e9d63', alignItems: 'center', justifyContent: 'center' }, children: loading ? _jsx(ActivityIndicator, { size: "small", color: ink }) : _jsx(MessageIcon, { name: playing ? 'pause' : 'play', color: ink, size: 20 }) }), _jsxs(View, { style: { flex: 1, minWidth: 0, gap: 7 }, children: [_jsx(Pressable, { onLongPress: hold, accessibilityRole: "adjustable", accessibilityLabel: showWaveform ?
|
|
19
|
+
return _jsxs(View, { testID: "voice-message-player", style: { width: 220, maxWidth: '100%', minHeight: 44, borderRadius: 24, borderWidth: 1, borderColor: mine ? '#ffffff24' : '#2A2A2A', backgroundColor: background ?? (mine ? '#1F5F7A' : '#1D1D1D'), paddingLeft: 5, paddingRight: 8, paddingVertical: 3 }, children: [_jsxs(View, { style: { flexDirection: 'row', direction: 'ltr', alignItems: 'center', gap: 8 }, children: [_jsx(Pressable, { testID: "voice-play-control", accessibilityRole: "button", accessibilityLabel: playing ? uiText("Pause voice message") : uiText("Play voice message"), accessibilityState: { busy: loading }, disabled: loading, hitSlop: 6, delayLongPress: 360, onPressIn: () => { held.current = false; }, onPress: () => { if (!held.current)
|
|
20
|
+
onToggle(); }, onLongPress: hold, style: { width: 36, height: 36, borderRadius: 18, paddingLeft: playing ? 0 : 2, backgroundColor: mine ? '#ffffff29' : heard || played ? accent : '#2e9d63', alignItems: 'center', justifyContent: 'center' }, children: loading ? _jsx(ActivityIndicator, { size: "small", color: ink }) : _jsx(MessageIcon, { name: playing ? 'pause' : 'play', color: ink, size: 20 }) }), _jsxs(View, { style: { flex: 1, minWidth: 0, gap: 7 }, children: [_jsx(Pressable, { onLongPress: hold, accessibilityRole: "adjustable", accessibilityLabel: showWaveform ? uiText("Voice waveform") : uiText("Voice position"), accessibilityValue: { min: 0, max: Math.ceil(durationMs / 1000), now: Math.floor(positionMs / 1000) }, accessibilityActions: [{ name: 'increment', label: uiText("Forward five seconds") }, { name: 'decrement', label: uiText("Back five seconds") }], onAccessibilityAction: e => onSeek?.(Math.max(0, Math.min(durationMs, positionMs + (e.nativeEvent.actionName === 'increment' ? 5000 : -5000)))), onLayout: e => { width.current = e.nativeEvent.layout.width; }, onPressIn: () => { held.current = false; }, onPress: e => { if (held.current)
|
|
19
21
|
return; const bounds = Platform.OS === 'web' ? e.currentTarget.getBoundingClientRect() : null; const x = bounds ? e.nativeEvent.clientX - bounds.left : e.nativeEvent.locationX; const total = bounds?.width ?? width.current; if (total > 0 && Number.isFinite(x))
|
|
20
22
|
onSeek?.(Math.max(0, Math.min(1, x / total)) * durationMs); }, style: { height: bars ? 24 : 9, transform: [{ translateY: 4 }], flexDirection: 'row', alignItems: 'center', gap: 1 }, children: bars ? bars.map((sample, i) => _jsx(View, { pointerEvents: "none", style: { flex: 1, flexBasis: 0, borderRadius: 2, overflow: 'hidden', backgroundColor: ink + (mine ? '33' : '38'), height: 2 + sample * 18 }, children: _jsx(View, { style: { height: '100%', backgroundColor: fill, width: `${Math.max(0, Math.min(1, progress * bars.length - i)) * 100}%` } }) }, i)) : _jsx(View, { pointerEvents: "none", style: { flex: 1, height: 9, borderRadius: 5, overflow: 'hidden', backgroundColor: ink + '33' }, children: _jsx(View, { style: { height: 9, borderRadius: 5, backgroundColor: fill, width: `${progress * 100}%` } }) }) }), _jsxs(View, { style: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', gap: 4, paddingRight: footer ? 6 : 0 }, children: [_jsx(Text, { testID: "voice-duration", style: { color: mine ? ink : '#A3A3A3', fontSize: 11, fontWeight: '800', fontVariant: ['tabular-nums'] }, children: duration(playing || positionMs > 0 ? Math.max(0, durationMs - positionMs) : durationMs) }), footer && _jsx(View, { testID: "voice-message-actions", style: { flexShrink: 1, maxWidth: 112, alignItems: 'flex-end' }, children: footer })] })] })] }), !!error && _jsx(Text, { accessibilityRole: "alert", style: { color: ink, fontSize: 12, marginTop: 6 }, children: error })] });
|
|
21
23
|
}
|
|
22
24
|
export function MediaPlaybackControls({ playing = false, loading = false, positionMs = 0, durationMs = 0, error, onToggle, onSeek, onFullscreen, fullscreen = false }) {
|
|
25
|
+
const { text: uiText, locale: uiLocale, number: uiNumber, count: uiCount } = useRobonoLocale();
|
|
23
26
|
const width = useRef(1);
|
|
24
|
-
return _jsxs(View, { style: { paddingHorizontal: 18, paddingVertical: 10, gap: 6, backgroundColor: '#111316' }, children: [_jsxs(View, { style: { flexDirection: 'row', alignItems: 'center', gap: 12 }, children: [_jsx(Pressable, { accessibilityRole: "button", accessibilityLabel: playing ?
|
|
25
|
-
onSeek?.(Math.max(0, Math.min(1, x / total)) * durationMs); }, style: { flex: 1, height: 44, justifyContent: 'center' }, children: _jsx(View, { style: { height: 3, borderRadius: 2, backgroundColor: '#ffffff44' }, children: _jsx(View, { style: { height: 3, borderRadius: 2, backgroundColor: '#fff', width: `${durationMs ? Math.min(100, positionMs / durationMs * 100) : 0}%` } }) }) }), _jsx(Text, { style: { color: '#bbb', fontSize: 12, fontVariant: ['tabular-nums'] }, children: duration(durationMs) }), onFullscreen && _jsx(Pressable, { accessibilityRole: "button", accessibilityLabel: fullscreen ? "Exit full screen" : "Full screen video", disabled: loading, onPress: onFullscreen, style: { width: 44, height: 44, alignItems: "center", justifyContent: "center" }, children: _jsx(MessageIcon, { name: fullscreen ? "close" : "open-in-new", color: "#fff", size: 22 }) })] }), !!error && _jsx(Text, { accessibilityRole: "alert", style: { color: '#fff', fontSize: 13 }, children: error })] });
|
|
27
|
+
return _jsxs(View, { style: { paddingHorizontal: 18, paddingVertical: 10, gap: 6, backgroundColor: '#111316' }, children: [_jsxs(View, { style: { flexDirection: 'row', alignItems: 'center', gap: 12 }, children: [_jsx(Pressable, { accessibilityRole: "button", accessibilityLabel: playing ? uiText("Pause video") : uiText("Play video"), disabled: loading, onPress: onToggle, style: { width: 44, height: 44, alignItems: 'center', justifyContent: 'center' }, children: loading ? _jsx(ActivityIndicator, { color: "#fff" }) : _jsx(MessageIcon, { name: playing ? 'pause' : 'play', color: "#fff" }) }), _jsx(Text, { style: { color: '#ddd', fontSize: 12, fontVariant: ['tabular-nums'] }, children: duration(positionMs) }), _jsx(Pressable, { accessibilityRole: "adjustable", accessibilityLabel: uiText("Video position"), accessibilityValue: { min: 0, max: Math.ceil(durationMs / 1000), now: Math.floor(positionMs / 1000) }, accessibilityActions: [{ name: 'increment' }, { name: 'decrement' }], onAccessibilityAction: e => onSeek?.(Math.max(0, Math.min(durationMs, positionMs + (e.nativeEvent.actionName === 'increment' ? 5000 : -5000)))), onLayout: e => { width.current = e.nativeEvent.layout.width; }, onPress: e => { const bounds = Platform.OS === "web" ? e.currentTarget.getBoundingClientRect() : null; const x = bounds ? e.nativeEvent.clientX - bounds.left : e.nativeEvent.locationX, total = bounds?.width ?? width.current; if (total > 0 && Number.isFinite(x))
|
|
28
|
+
onSeek?.(Math.max(0, Math.min(1, x / total)) * durationMs); }, style: { flex: 1, height: 44, justifyContent: 'center' }, children: _jsx(View, { style: { height: 3, borderRadius: 2, backgroundColor: '#ffffff44' }, children: _jsx(View, { style: { height: 3, borderRadius: 2, backgroundColor: '#fff', width: `${durationMs ? Math.min(100, positionMs / durationMs * 100) : 0}%` } }) }) }), _jsx(Text, { style: { color: '#bbb', fontSize: 12, fontVariant: ['tabular-nums'] }, children: duration(durationMs) }), onFullscreen && _jsx(Pressable, { accessibilityRole: "button", accessibilityLabel: fullscreen ? uiText("Exit full screen") : uiText("Full screen video"), disabled: loading, onPress: onFullscreen, style: { width: 44, height: 44, alignItems: "center", justifyContent: "center" }, children: _jsx(MessageIcon, { name: fullscreen ? "close" : "open-in-new", color: "#fff", size: 22 }) })] }), !!error && _jsx(Text, { accessibilityRole: "alert", style: { color: '#fff', fontSize: 13 }, children: error })] });
|
|
26
29
|
}
|
|
27
30
|
/** A full-size photo surface. The same tap-to-hide-controls, pinch and pan handling runs in the preview and native app. */
|
|
28
|
-
export function ZoomablePhoto({ uri, label
|
|
31
|
+
export function ZoomablePhoto({ uri, label, onTap, expanded = false }) {
|
|
32
|
+
const { text: uiText, locale: uiLocale, number: uiNumber, count: uiCount } = useRobonoLocale();
|
|
33
|
+
label ??= uiText('Photo');
|
|
29
34
|
const mediaReady = useContext(MediaReadiness);
|
|
30
35
|
const [transform, setTransform] = useState({ zoom: 1, x: 0, y: 0 }), [error, setError] = useState(false);
|
|
31
36
|
const [viewport, setViewport] = useState({ width: 1, height: 1 }), [imageSize, setImageSize] = useState({ width: 1, height: 1 });
|
|
@@ -96,13 +101,14 @@ export function ZoomablePhoto({ uri, label = 'Photo', onTap, expanded = false })
|
|
|
96
101
|
} }, () => { }); return () => { active = false; }; }, [uri, controller]);
|
|
97
102
|
useEffect(() => { controller.reset(); }, [expanded, controller]);
|
|
98
103
|
useEffect(() => { controller.layout(stageWidth, stageHeight); }, [controller, stageWidth, stageHeight]);
|
|
99
|
-
return _jsxs(View, { style: { flex: 1, minHeight: 0, overflow: 'hidden', alignItems: 'center', justifyContent: 'center' }, onLayout: e => setViewport(e.nativeEvent.layout), children: [_jsx(View, { testID: "photo-image-stage", style: { width: stageWidth, height: stageHeight, transform: rotated ? [{ rotate: '90deg' }] : [] }, children: _jsx(View, { ref: webRef, ...(Platform.OS === 'web' ? {} : pan.panHandlers), testID: "photo-gesture-surface", accessible: true, accessibilityRole: "button", accessibilityLabel: "Toggle photo controls", onAccessibilityTap: () => tap.current?.(), ...(Platform.OS === 'web' ? { tabIndex: 0 } : {}), style: { flex: 1, overflow: 'hidden', ...(Platform.OS === 'web' ? { touchAction: 'none', userSelect: 'none' } : {}) }, children: _jsx(Image, { testID: "photo-zoom-image", accessibilityLabel: label, source: { uri }, resizeMode: "contain", onLoad: () => mediaReady('image-loaded', uri), onError: () => setError(true), style: { width: '100%', height: '100%', transform: [{ translateX: transform.x }, { translateY: transform.y }, { scale: transform.zoom }] } }) }) }), error && _jsx(Text, { accessibilityRole: "alert", style: { position: 'absolute', top: '50%', alignSelf: 'center', color: '#fff' }, children: "This photo could not be opened." })] });
|
|
104
|
+
return _jsxs(View, { style: { flex: 1, minHeight: 0, overflow: 'hidden', alignItems: 'center', justifyContent: 'center' }, onLayout: e => setViewport(e.nativeEvent.layout), children: [_jsx(View, { testID: "photo-image-stage", style: { width: stageWidth, height: stageHeight, transform: rotated ? [{ rotate: '90deg' }] : [] }, children: _jsx(View, { ref: webRef, ...(Platform.OS === 'web' ? {} : pan.panHandlers), testID: "photo-gesture-surface", accessible: true, accessibilityRole: "button", accessibilityLabel: uiText("Toggle photo controls"), onAccessibilityTap: () => tap.current?.(), ...(Platform.OS === 'web' ? { tabIndex: 0 } : {}), style: { flex: 1, overflow: 'hidden', ...(Platform.OS === 'web' ? { touchAction: 'none', userSelect: 'none' } : {}) }, children: _jsx(Image, { testID: "photo-zoom-image", accessibilityLabel: label, source: { uri }, resizeMode: "contain", onLoad: () => mediaReady('image-loaded', uri), onError: () => setError(true), style: { width: '100%', height: '100%', transform: [{ translateX: transform.x }, { translateY: transform.y }, { scale: transform.zoom }] } }) }) }), error && _jsx(Text, { accessibilityRole: "alert", style: { position: 'absolute', top: '50%', alignSelf: 'center', color: '#fff' }, children: uiText("This photo could not be opened.") })] });
|
|
100
105
|
}
|
|
101
106
|
export function MediaViewer({ title, subtitle, kind, uri, fileName, byteSize, children, loading, error, onClose, onFileAction, reviewAction, onReview, onDownload, onShare, onForward, onDelete, downloading = false, expanded, onExpandedChange }) {
|
|
107
|
+
const { text: uiText, locale: uiLocale, number: uiNumber, count: uiCount } = useRobonoLocale();
|
|
102
108
|
const [localExpanded, setLocalExpanded] = useState(false);
|
|
103
109
|
const photoExpanded = kind === 'image' && (expanded ?? localExpanded), controls = !photoExpanded;
|
|
104
110
|
const setExpanded = (value) => { setLocalExpanded(value); onExpandedChange?.(value); };
|
|
105
111
|
useEffect(() => { setLocalExpanded(false); }, [uri]);
|
|
106
112
|
const visual = kind === 'image' || kind === 'video';
|
|
107
|
-
return _jsxs(View, { testID: "messenger-media-viewer", accessibilityViewIsModal: true, style: { flex: 1, minHeight: 0, backgroundColor: controls ? 'rgba(0,0,0,0.94)' : '#000000' }, children: [_jsxs(View, { style: { flex: 1, minHeight: 0, justifyContent: 'center', paddingHorizontal: visual && controls ? 12 : 0 }, children: [controls && _jsxs(View, { testID: "media-action-toolbar", style: { position: 'absolute', top: 12, left: 12, right: 12, zIndex: 3, minHeight: 52, borderRadius: 26, borderWidth: 1, borderColor: 'rgba(255,255,255,0.12)', backgroundColor: 'rgba(18,18,18,0.92)', paddingLeft: 8, paddingRight: 4, flexDirection: 'row', alignItems: 'center', gap: 2 }, children: [[[onDownload, 'Download', 'download-outline', downloading, false], [onDelete, 'Delete', 'trash-outline', false, true], [onForward, 'Forward', 'arrow-redo-outline', false, false], [onShare, 'Share', 'share-social-outline', false, false]].filter(([action, label]) => !!action || (visual && !reviewAction && label !== 'Download')).map(([action, label, icon, busy, destructive]) => _jsxs(Pressable, { accessibilityRole: "button", accessibilityLabel:
|
|
113
|
+
return _jsxs(View, { testID: "messenger-media-viewer", accessibilityViewIsModal: true, style: { flex: 1, minHeight: 0, backgroundColor: controls ? 'rgba(0,0,0,0.94)' : '#000000' }, children: [_jsxs(View, { style: { flex: 1, minHeight: 0, justifyContent: 'center', paddingHorizontal: visual && controls ? 12 : 0 }, children: [controls && _jsxs(View, { testID: "media-action-toolbar", style: { position: 'absolute', top: 12, left: 12, right: 12, zIndex: 3, minHeight: 52, borderRadius: 26, borderWidth: 1, borderColor: 'rgba(255,255,255,0.12)', backgroundColor: 'rgba(18,18,18,0.92)', paddingLeft: 8, paddingRight: 4, flexDirection: 'row', alignItems: 'center', gap: 2 }, children: [[[onDownload, 'Download', 'download-outline', downloading, false], [onDelete, 'Delete', 'trash-outline', false, true], [onForward, 'Forward', 'arrow-redo-outline', false, false], [onShare, 'Share', 'share-social-outline', false, false]].filter(([action, label]) => !!action || (visual && !reviewAction && label !== 'Download')).map(([action, label, icon, busy, destructive]) => _jsxs(Pressable, { accessibilityRole: "button", accessibilityLabel: uiText("{value1} media", { value1: label }), accessibilityState: { busy }, disabled: !action || busy || downloading, onPress: action, style: { opacity: !action ? .5 : 1, flex: 1, minWidth: 44, minHeight: 44, borderRadius: 22, alignItems: 'center', justifyContent: 'center', gap: 1, paddingHorizontal: 2 }, children: [busy ? _jsx(ActivityIndicator, { size: "small", color: "#e8843a" }) : _jsx(MessageIcon, { name: icon, size: 20, color: !action ? '#A3A3A3' : destructive ? '#EF4444' : '#F7F7F7' }), _jsx(Text, { numberOfLines: 1, style: { color: !action ? '#A3A3A3' : destructive ? '#EF4444' : '#F7F7F7', fontSize: 10, fontWeight: '800' }, children: label })] }, label)), _jsx(View, { style: { flex: (!visual || !!reviewAction) && !onDownload && !onDelete && !onForward && !onShare ? 1 : 0, minWidth: 0 }, children: (!visual || !!reviewAction) && !onDownload && !onDelete && !onForward && !onShare && _jsx(Text, { numberOfLines: 1, style: { color: '#fff', fontSize: 14, paddingLeft: 8 }, children: title }) }), _jsx(Pressable, { accessibilityRole: "button", accessibilityLabel: uiText("Close media"), onPress: onClose, style: { width: 44, height: 44, borderRadius: 22, backgroundColor: 'rgba(255,255,255,0.08)', alignItems: 'center', justifyContent: 'center' }, children: _jsx(MessageIcon, { name: "close", size: 24, color: "#fff" }) })] }), loading ? _jsxs(View, { style: { flex: 1, alignItems: 'center', justifyContent: 'center', gap: 16 }, children: [_jsx(ActivityIndicator, { color: "#fff" }), _jsx(Text, { style: { color: '#c1c7ce' }, children: uiText("Opening…") })] }) : kind === 'image' && uri ? _jsx(View, { style: { height: controls ? '82%' : '100%', width: '100%' }, children: _jsx(ZoomablePhoto, { uri: uri, label: fileName, expanded: photoExpanded, onTap: () => setExpanded(!photoExpanded) }) }) : kind === 'document' ? _jsxs(View, { style: { flex: 1, alignItems: 'center', justifyContent: 'center', padding: 32, gap: 20 }, children: [_jsx(View, { style: { width: 88, height: 106, borderRadius: 16, backgroundColor: '#ffffff12', alignItems: 'center', justifyContent: 'center' }, children: _jsx(MessageIcon, { name: "document", color: "#fff", size: 40 }) }), _jsx(Text, { style: { color: '#fff', fontSize: 19, fontWeight: '600', textAlign: 'center' }, children: fileName }), _jsxs(Text, { style: { color: '#9ba3ad', fontSize: 13 }, children: [fileName?.split('.').pop()?.toUpperCase(), " \u00B7 ", fileSize(byteSize)] }), onFileAction && _jsx(Pressable, { accessibilityRole: "button", onPress: onFileAction, style: { borderRadius: 24, paddingHorizontal: 28, paddingVertical: 14, backgroundColor: '#fff' }, children: _jsx(Text, { style: { color: '#15191e', fontWeight: '600' }, children: uiText("Open file") }) })] }) : kind === 'video' ? _jsx(View, { testID: "media-video-stage", style: { height: '72%', width: '100%' }, children: children }) : children] }), !!error && _jsx(Text, { accessibilityRole: "alert", style: { color: '#fff', padding: 16 }, children: error }), controls && reviewAction && onReview && _jsx(View, { style: { padding: 16, borderTopWidth: 1, borderTopColor: '#ffffff15' }, children: _jsx(Pressable, { accessibilityRole: "button", onPress: onReview, style: { minHeight: 48, borderRadius: 24, backgroundColor: '#fff', alignItems: 'center', justifyContent: 'center' }, children: _jsx(Text, { style: { fontWeight: '600', color: '#121619' }, children: reviewAction }) }) })] });
|
|
108
114
|
}
|
package/dist/message-menu.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { useRobonoLocale } from './localization.js';
|
|
2
3
|
import { messagingPopupBackdrop, MessagingMenuSurface } from './popup.js';
|
|
3
4
|
import { useEffect } from 'react';
|
|
4
5
|
import { Platform, Pressable, ScrollView, Text, View } from 'react-native';
|
|
@@ -14,6 +15,7 @@ export function MessageMenuRow({ icon, label, onPress, disabled, danger, divider
|
|
|
14
15
|
return _jsxs(_Fragment, { children: [divider && _jsx(View, { style: { height: 1, marginHorizontal: 12, marginVertical: 4, backgroundColor: '#ffffff22' } }), _jsxs(Pressable, { accessibilityRole: "button", accessibilityLabel: label, disabled: disabled, onPress: onPress, style: ({ pressed }) => ({ minHeight: 43, paddingHorizontal: 12, flexDirection: 'row', alignItems: 'center', gap: 8, opacity: disabled ? .5 : 1, backgroundColor: pressed ? '#ffffff12' : 'transparent' }), children: [_jsx(MessageIcon, { name: icon, color: danger ? '#F87171' : '#f5f5f5', size: 22 }), _jsx(Text, { style: { flex: 1, color: danger ? '#F87171' : '#f5f5f5', fontSize: 15, fontWeight: '700' }, children: label })] })] });
|
|
15
16
|
}
|
|
16
17
|
export function MessageMenu({ visible, position, screenHeight, mine, reactions, children, onClose, onDismiss }) {
|
|
18
|
+
const { text: uiText, locale: uiLocale, number: uiNumber, count: uiCount } = useRobonoLocale();
|
|
17
19
|
useEffect(() => { if (!visible || Platform.OS !== 'web')
|
|
18
20
|
return; const key = (e) => { if (e.key === 'Escape') {
|
|
19
21
|
e.stopPropagation();
|
|
@@ -23,6 +25,6 @@ export function MessageMenu({ visible, position, screenHeight, mine, reactions,
|
|
|
23
25
|
const menuWidth = Math.min(300, rect.width - 24), y = Math.max(12, Math.min(position.y - rect.top, rect.height - 12));
|
|
24
26
|
const local = { ...position, x: Math.max(12, Math.min(position.x - rect.left, rect.width - menuWidth - 12)), y, available: Math.max(44, (position.above ? y : rect.height - y) - 24) };
|
|
25
27
|
const card = _jsx(View, { testID: "message-action-card", style: { width: 218, maxWidth: '100%', maxHeight: Math.min(310, Math.max(43, local.available - (reactions ? 58 : 0))), borderRadius: 16, borderWidth: .5, borderColor: '#ffffff22', backgroundColor: '#151515', paddingVertical: 4, overflow: 'hidden', boxShadow: '0 8px 18px #0006', alignSelf: mine ? 'flex-end' : 'flex-start' }, children: _jsx(ScrollView, { showsVerticalScrollIndicator: false, bounces: false, keyboardShouldPersistTaps: "handled", children: children }) });
|
|
26
|
-
return _jsxs(View, { style: { flex: 1 }, children: [_jsx(Pressable, { accessibilityLabel: "Close message options", onPress: onClose, style: { position: 'absolute', inset: 0, backgroundColor: messagingPopupBackdrop } }), _jsxs(View, { testID: "message-context-menu", style: { position: 'absolute', left: local.x, ...(position.above ? { bottom: rect.height - local.y + 12 } : { top: local.y + 12 }), width: menuWidth, maxWidth: '94%', gap: 8 }, children: [position.above ? card : reactions, position.above ? reactions : card] })] });
|
|
28
|
+
return _jsxs(View, { style: { flex: 1 }, children: [_jsx(Pressable, { accessibilityLabel: uiText("Close message options"), onPress: onClose, style: { position: 'absolute', inset: 0, backgroundColor: messagingPopupBackdrop } }), _jsxs(View, { testID: "message-context-menu", style: { position: 'absolute', left: local.x, ...(position.above ? { bottom: rect.height - local.y + 12 } : { top: local.y + 12 }), width: menuWidth, maxWidth: '94%', gap: 8 }, children: [position.above ? card : reactions, position.above ? reactions : card] })] });
|
|
27
29
|
} });
|
|
28
30
|
}
|
package/dist/native-outbox.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import type { TranslationPreference, MessageContent, MessageMedia, MessagingOutbox, SavedOutgoingMessage } from '@robono/messaging';
|
|
1
|
+
import type { SavedMessageAction, TranslationPreference, MessageContent, MessageMedia, MessagingOutbox, SavedOutgoingMessage } from '@robono/messaging';
|
|
2
2
|
import type { LocalMessagingMedia } from './media-controller.js';
|
|
3
3
|
export interface NativeDraftFiles {
|
|
4
|
+
conversations?(scope: string): Promise<string[]>;
|
|
4
5
|
read(scope: string, conversation: string): Promise<NativeDraftState>;
|
|
5
6
|
write(scope: string, conversation: string, state: NativeDraftState): Promise<void>;
|
|
6
7
|
copy(scope: string, conversation: string, id: string, source: LocalMessagingMedia): Promise<string>;
|
|
@@ -9,6 +10,8 @@ export interface NativeDraftFiles {
|
|
|
9
10
|
clear(scope: string, conversation?: string): Promise<void>;
|
|
10
11
|
}
|
|
11
12
|
export interface NativeDraftState {
|
|
13
|
+
actions?: SavedMessageAction[];
|
|
14
|
+
conversationId?: string;
|
|
12
15
|
translationPreference?: TranslationPreference;
|
|
13
16
|
quickMessage?: string;
|
|
14
17
|
draft: string;
|
|
@@ -28,9 +31,14 @@ export declare class NativeMessagingOutbox implements MessagingOutbox {
|
|
|
28
31
|
private scope;
|
|
29
32
|
constructor(endpoint: string, userId: string, files: NativeDraftFiles, namespace?: string);
|
|
30
33
|
private serial;
|
|
34
|
+
conversations(): Promise<string[]>;
|
|
35
|
+
actions(cid: string): Promise<SavedMessageAction[]>;
|
|
36
|
+
putAction(cid: string, action: SavedMessageAction): Promise<void>;
|
|
37
|
+
removeAction(cid: string, id: string): Promise<void>;
|
|
31
38
|
load(cid: string): Promise<{
|
|
32
39
|
draft: string;
|
|
33
40
|
messages: {
|
|
41
|
+
retry?: import("@robono/messaging").OutgoingRetry;
|
|
34
42
|
clientMessageId: string;
|
|
35
43
|
createdAt: string;
|
|
36
44
|
content: MessageContent;
|
package/dist/native-outbox.js
CHANGED
|
@@ -12,6 +12,13 @@ export class NativeMessagingOutbox {
|
|
|
12
12
|
}
|
|
13
13
|
serial(work) { const task = (queues.get(this.scope) ?? Promise.resolve()).catch(() => { }).then(work); queues.set(this.scope, task); void task.finally(() => { if (queues.get(this.scope) === task)
|
|
14
14
|
queues.delete(this.scope); }).catch(() => { }); return task; }
|
|
15
|
+
conversations() { return this.serial(() => this.files.conversations?.(this.scope) ?? Promise.resolve([])); }
|
|
16
|
+
actions(cid) { return this.serial(async () => (await this.files.read(this.scope, cid)).actions ?? []); }
|
|
17
|
+
putAction(cid, action) { return this.serial(async () => { const state = await this.files.read(this.scope, cid); const actions = state.actions ?? [], index = actions.findIndex(a => a.id === action.id); if (index < 0)
|
|
18
|
+
actions.push(action);
|
|
19
|
+
else
|
|
20
|
+
actions[index] = action; state.actions = actions; await this.files.write(this.scope, cid, state); }); }
|
|
21
|
+
removeAction(cid, id) { return this.serial(async () => { const state = await this.files.read(this.scope, cid); state.actions = (state.actions ?? []).filter(a => a.id !== id); await this.files.write(this.scope, cid, state); }); }
|
|
15
22
|
load(cid) { return this.serial(async () => { const saved = await this.files.read(this.scope, cid), used = new Set(saved.messages.map(m => m.fileId)); return { ...(saved.translationPreference ? { translationPreference: saved.translationPreference } : {}), ...(saved.quickMessage ? { quickMessage: saved.quickMessage } : {}), draft: saved.draft, messages: saved.messages.map(({ fileId, ...message }) => message), attachments: Object.entries(saved.files).filter(([id]) => !used.has(id)).map(([, f]) => ({ kind: f.kind, media: f.media })) }; }); }
|
|
16
23
|
put(cid, message) { return this.serial(async () => { const state = await this.files.read(this.scope, cid), previous = state.messages.find(m => m.clientMessageId === message.clientMessageId); state.messages = state.messages.filter(m => m.clientMessageId !== message.clientMessageId); state.messages.push({ ...message, fileId: message.needsPreparation ? message.content.media?.assetId : previous?.fileId }); await this.files.write(this.scope, cid, state); }); }
|
|
17
24
|
remove(cid, id) { return this.serial(async () => { const state = await this.files.read(this.scope, cid), fileId = state.messages.find(m => m.clientMessageId === id)?.fileId; state.messages = state.messages.filter(m => m.clientMessageId !== id); if (fileId && !state.messages.some(m => m.fileId === fileId)) {
|
package/dist/polls.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { useRobonoLocale } from './localization.js';
|
|
2
3
|
import { messagingPopupColors } from './popup.js';
|
|
3
4
|
import { ExtraPopup } from './extras.js';
|
|
4
5
|
import { MessageIcon } from './icons.js';
|
|
@@ -10,6 +11,7 @@ const PollArea = View;
|
|
|
10
11
|
import { MessagingPopup as PollModal } from './popup.js';
|
|
11
12
|
/** Optional reusable poll content. Votes are submitted as a complete selection, not a toggle request. */
|
|
12
13
|
export function PollMessage({ message, userId, ink, accent, enabled, onVote, onClose, resolveVoterName, background = '#101010', foreground = '#fff', renderDetails, detailsOpen, onDetailsOpenChange, onLongPress }) {
|
|
14
|
+
const { text: uiText, locale: uiLocale, number: uiNumber, count: uiCount } = useRobonoLocale();
|
|
13
15
|
const [busy, setBusy] = useState(false), [error, setError] = useState(''), [localShowVotes, setLocalShowVotes] = useState(false);
|
|
14
16
|
const showVotes = detailsOpen ?? localShowVotes;
|
|
15
17
|
const setShowVotes = (open) => { setLocalShowVotes(open); onDetailsOpenChange?.(open); };
|
|
@@ -20,7 +22,7 @@ export function PollMessage({ message, userId, ink, accent, enabled, onVote, onC
|
|
|
20
22
|
const queue = useRef(null);
|
|
21
23
|
if (!queue.current)
|
|
22
24
|
queue.current = createPollVoteQueue(choices => current.current.onVote(choices), choices => { if (choices)
|
|
23
|
-
selection.current = choices; setPending(choices); }, () => setError(
|
|
25
|
+
selection.current = choices; setPending(choices); }, () => setError(uiText("The poll could not be updated. Try again.")));
|
|
24
26
|
const actionBusy = useRef(false);
|
|
25
27
|
const poll = message.poll;
|
|
26
28
|
if (!poll)
|
|
@@ -35,26 +37,27 @@ export function PollMessage({ message, userId, ink, accent, enabled, onVote, onC
|
|
|
35
37
|
await work();
|
|
36
38
|
}
|
|
37
39
|
catch {
|
|
38
|
-
setError(
|
|
40
|
+
setError(uiText("The poll could not be updated. Try again."));
|
|
39
41
|
}
|
|
40
42
|
finally {
|
|
41
43
|
actionBusy.current = false;
|
|
42
44
|
setBusy(false);
|
|
43
45
|
} }
|
|
44
|
-
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 ?
|
|
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) => {
|
|
45
47
|
const people = votes.filter(v => v.options.includes(index));
|
|
46
48
|
const names = people.map(v => v.userId === userId ? 'You' : resolveVoterName?.(v.userId) || 'Chat member').join(', ');
|
|
47
49
|
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);
|
|
48
|
-
}) }), 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: "End poll" }) }), !!error && _jsx(Text, { accessibilityRole: "alert", style: { color: messagingPopupColors.foreground, fontSize: 13, lineHeight: 18 }, children: error })] });
|
|
49
|
-
return _jsxs(_Fragment, { children: [_jsxs(Pressable, { accessible: false, onLongPress: onLongPress, onPress: event => { event.stopPropagation(); setShowVotes(true); }, accessibilityLabel:
|
|
50
|
+
}) }), 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
|
+
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) => {
|
|
50
52
|
const count = votes.filter(v => v.options.includes(index)).length, checked = selected.includes(index);
|
|
51
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)
|
|
52
54
|
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);
|
|
53
|
-
}), !!error && _jsx(Text, { accessibilityRole: "alert", style: { color: ink }, children: error })] }), showVotes && (renderDetails ? renderDetails(details, () => setShowVotes(false)) : _jsx(ExtraPopup, { title: "Votes", background: background, foreground: foreground, onClose: () => setShowVotes(false), children: _jsx(ScrollView, { style: { flexShrink: 1 }, children: details }) }))] });
|
|
55
|
+
}), !!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 }) }))] });
|
|
54
56
|
}
|
|
55
57
|
/** Recipient-only decision. Keep the attempted answer on an uncertain failure so
|
|
56
58
|
* retry cannot silently switch an answer the server may already have accepted. */
|
|
57
59
|
function YesNoQuestion({ message, userId, ink, accent, enabled, onVote, onClose, resolveVoterName, renderDetails, detailsOpen, onDetailsOpenChange, onLongPress, background = '#111111', foreground = '#fff' }) {
|
|
60
|
+
const { text: uiText, locale: uiLocale, number: uiNumber, count: uiCount } = useRobonoLocale();
|
|
58
61
|
const [attempt, setAttempt] = useState(null), [busy, setBusy] = useState(false), [error, setError] = useState(''), [localOpen, setLocalOpen] = useState(false);
|
|
59
62
|
const inFlight = useRef(false), firstChoice = useRef(null);
|
|
60
63
|
const poll = message.poll, answer = message.pollState?.votes.find(v => v.userId !== message.senderId)?.options[0];
|
|
@@ -76,7 +79,7 @@ function YesNoQuestion({ message, userId, ink, accent, enabled, onVote, onClose,
|
|
|
76
79
|
await onVote([index]);
|
|
77
80
|
}
|
|
78
81
|
catch {
|
|
79
|
-
setError(
|
|
82
|
+
setError(uiText("Answer not confirmed. Tap to retry."));
|
|
80
83
|
}
|
|
81
84
|
finally {
|
|
82
85
|
inFlight.current = false;
|
|
@@ -84,12 +87,13 @@ function YesNoQuestion({ message, userId, ink, accent, enabled, onVote, onClose,
|
|
|
84
87
|
}
|
|
85
88
|
}
|
|
86
89
|
const respondent = message.pollState?.votes.find(v => v.userId !== message.senderId)?.userId;
|
|
87
|
-
const details = _jsxs(View, { style: { gap: 12 }, children: [_jsx(Text, { style: { color: messagingPopupColors.foreground, fontSize: 17, lineHeight: 23, fontWeight: '600' }, children: poll.question }), _jsx(Text, { style: { color: messagingPopupColors.foreground, fontSize: 15, lineHeight: 21 }, children: answer !== undefined ?
|
|
88
|
-
return; inFlight.current = true; void onClose().catch(() => setError(
|
|
89
|
-
return _jsxs(_Fragment, { children: [_jsxs(Pressable, { accessible: false, onLongPress: onLongPress, onPress: e => { e.stopPropagation(); setOpen(true); }, testID: "yes-no-question", style: { maxWidth: 270, minWidth: 160 }, children: [_jsxs(View, { style: { flexDirection: 'row', alignItems: 'flex-start', gap: 6 }, children: [_jsx(View, { style: { paddingTop: 3 }, children: _jsx(MessageIcon, { name: "poll", color: accent, size: 16 }) }), _jsx(Text, { accessibilityRole: "button", onLongPress: onLongPress, onPress: () => setOpen(true), style: { flexShrink: 1, fontSize: 16, lineHeight: 22, fontWeight: '600', color: ink }, children: poll.question })] }), _jsx(View, { style: { flexDirection: 'row', gap: 20, marginTop: 2 }, children: poll.options.map((label, index) => _jsxs(Pressable, { accessibilityRole: "radio", accessibilityLabel: label, accessibilityState: { checked: chosen === index, disabled: !enabled || mine || closed || attempt !== null, busy }, disabled: !enabled || mine || closed || attempt !== null, onPress: e => { e.stopPropagation(); void choose(index); }, hitSlop: 4, style: { flexDirection: 'row', alignItems: 'center', gap: 8, minHeight: 32 }, children: [_jsx(View, { style: { width: 20, height: 20, borderRadius: 10, borderWidth: 2, borderColor: chosen === index ? accent : ink, alignItems: 'center', justifyContent: 'center', opacity: chosen === undefined && mine ? .5 : 1 }, children: chosen === index && _jsx(View, { style: { width: 10, height: 10, borderRadius: 5, backgroundColor: accent } }) }), _jsx(Text, { style: { color: ink, fontSize: 16, fontWeight: chosen === index ? '600' : '400' }, children: label })] }, index)) }), !!error && !closed && _jsx(Pressable, { accessibilityRole: "button", accessibilityLabel: "Retry answer", disabled: busy || closed || attempt === null, onPress: e => { e.stopPropagation(); if (attempt !== null)
|
|
90
|
-
void choose(attempt); }, children: _jsx(Text, { accessibilityRole: "alert", style: { color: ink, fontSize: 13 }, children: error }) })] }), open && (renderDetails ? renderDetails(details, () => setOpen(false)) : _jsx(ExtraPopup, { title: "Answer", background: background, foreground: foreground, onClose: () => setOpen(false), children: _jsx(ScrollView, { style: { flexShrink: 1 }, children: details }) }))] });
|
|
90
|
+
const details = _jsxs(View, { style: { gap: 12 }, children: [_jsx(Text, { style: { color: messagingPopupColors.foreground, fontSize: 17, lineHeight: 23, fontWeight: '600' }, children: poll.question }), _jsx(Text, { style: { color: messagingPopupColors.foreground, fontSize: 15, lineHeight: 21 }, children: answer !== undefined ? uiText("{value1} answered {value2}.", { value1: respondent === userId ? 'You' : resolveVoterName?.(respondent) || 'Recipient', value2: poll.options[answer] }) : closed ? uiText("Closed without an answer.") : attempt !== null ? uiText("Confirming your answer…") : uiText("Waiting for an answer.") }), mine && !closed && enabled && _jsx(Pressable, { accessibilityRole: "button", onPress: () => { if (inFlight.current)
|
|
91
|
+
return; inFlight.current = true; void onClose().catch(() => setError(uiText("Could not close the question. Try again."))).finally(() => { inFlight.current = false; }); }, style: { minHeight: 36, alignSelf: 'flex-end', justifyContent: 'center' }, children: _jsx(Text, { style: { color: accent }, children: uiText("End question") }) })] });
|
|
92
|
+
return _jsxs(_Fragment, { children: [_jsxs(Pressable, { accessible: false, onLongPress: onLongPress, onPress: e => { e.stopPropagation(); setOpen(true); }, testID: "yes-no-question", style: { maxWidth: 270, minWidth: 160 }, children: [_jsxs(View, { style: { flexDirection: 'row', alignItems: 'flex-start', gap: 6 }, children: [_jsx(View, { style: { paddingTop: 3 }, children: _jsx(MessageIcon, { name: "poll", color: accent, size: 16 }) }), _jsx(Text, { accessibilityRole: "button", onLongPress: onLongPress, onPress: () => setOpen(true), style: { flexShrink: 1, fontSize: 16, lineHeight: 22, fontWeight: '600', color: ink }, children: poll.question })] }), _jsx(View, { style: { flexDirection: 'row', gap: 20, marginTop: 2 }, children: poll.options.map((label, index) => _jsxs(Pressable, { accessibilityRole: "radio", accessibilityLabel: label, accessibilityState: { checked: chosen === index, disabled: !enabled || mine || closed || attempt !== null, busy }, disabled: !enabled || mine || closed || attempt !== null, onPress: e => { e.stopPropagation(); void choose(index); }, hitSlop: 4, style: { flexDirection: 'row', alignItems: 'center', gap: 8, minHeight: 32 }, children: [_jsx(View, { style: { width: 20, height: 20, borderRadius: 10, borderWidth: 2, borderColor: chosen === index ? accent : ink, alignItems: 'center', justifyContent: 'center', opacity: chosen === undefined && mine ? .5 : 1 }, children: chosen === index && _jsx(View, { style: { width: 10, height: 10, borderRadius: 5, backgroundColor: accent } }) }), _jsx(Text, { style: { color: ink, fontSize: 16, fontWeight: chosen === index ? '600' : '400' }, children: label })] }, index)) }), !!error && !closed && _jsx(Pressable, { accessibilityRole: "button", accessibilityLabel: uiText("Retry answer"), disabled: busy || closed || attempt === null, onPress: e => { e.stopPropagation(); if (attempt !== null)
|
|
93
|
+
void choose(attempt); }, children: _jsx(Text, { accessibilityRole: "alert", style: { color: ink, fontSize: 13 }, children: error }) })] }), open && (renderDetails ? renderDetails(details, () => setOpen(false)) : _jsx(ExtraPopup, { title: uiText("Answer"), background: background, foreground: foreground, onClose: () => setOpen(false), children: _jsx(ScrollView, { style: { flexShrink: 1 }, children: details }) }))] });
|
|
91
94
|
}
|
|
92
95
|
export function PollComposer({ appearance, onSend, onCancel, retrySafe = false, yesNo = false }) {
|
|
96
|
+
const { text: uiText, locale: uiLocale, number: uiNumber, count: uiCount } = useRobonoLocale();
|
|
93
97
|
const [question, setQuestion] = useState(''), [options, setOptions] = useState(['', '']), [multiple, setMultiple] = useState(false), [busy, setBusy] = useState(false), [error, setError] = useState(''), [submitted, setSubmitted] = useState(false);
|
|
94
98
|
const c = { ...appearance.colors, ...messagingPopupColors };
|
|
95
99
|
async function send() { if (busy || submitted)
|
|
@@ -106,7 +110,7 @@ export function PollComposer({ appearance, onSend, onCancel, retrySafe = false,
|
|
|
106
110
|
catch {
|
|
107
111
|
if (retrySafe)
|
|
108
112
|
setSubmitted(false);
|
|
109
|
-
setError(retrySafe ?
|
|
113
|
+
setError(retrySafe ? uiText("The poll was not sent. Try again.") : uiText("The poll was not sent. Check the conversation for a retry."));
|
|
110
114
|
}
|
|
111
115
|
finally {
|
|
112
116
|
setBusy(false);
|
|
@@ -118,6 +122,6 @@ export function PollComposer({ appearance, onSend, onCancel, retrySafe = false,
|
|
|
118
122
|
ready = true;
|
|
119
123
|
}
|
|
120
124
|
catch { }
|
|
121
|
-
return _jsx(PollModal, { contentWidth: 640, form: true, title: yesNo ?
|
|
122
|
-
onCancel(); }, children: _jsxs(PollArea, { style: { flex: 1, minHeight: 0, backgroundColor: c.background }, children: [_jsx(View, { style: { flexShrink: 1 }, children: _jsxs(ScrollView, { keyboardShouldPersistTaps: "handled", keyboardDismissMode: "on-drag", contentContainerStyle: { padding: 0, paddingBottom: 12, gap: 8 }, children: [_jsx(Text, { style: { color: c.secondary, fontSize: 13, fontWeight: '600', marginTop: 4 }, children: "Question" }), _jsx(TextInput, { accessibilityLabel: "Poll question", placeholder: "Ask a question", placeholderTextColor: c.secondary, maxLength: 300, multiline: true, value: question, onChangeText: setQuestion, editable: !busy, style: inputStyle }), !yesNo && _jsxs(_Fragment, { children: [_jsx(Text, { style: { color: c.secondary, fontSize: 13, fontWeight: '600', marginTop: 12 }, children: "Answers" }), options.map((option, i) => _jsxs(View, { style: { flexDirection: 'row', alignItems: 'center', gap: 8 }, children: [_jsx(TextInput, { accessibilityLabel:
|
|
125
|
+
return _jsx(PollModal, { contentWidth: 640, form: true, title: yesNo ? uiText("Yes/No question") : uiText("New poll"), foreground: c.foreground, closeDisabled: busy, background: c.background, onClose: () => { if (!busy)
|
|
126
|
+
onCancel(); }, children: _jsxs(PollArea, { style: { flex: 1, minHeight: 0, backgroundColor: c.background }, children: [_jsx(View, { style: { flexShrink: 1 }, children: _jsxs(ScrollView, { keyboardShouldPersistTaps: "handled", keyboardDismissMode: "on-drag", contentContainerStyle: { padding: 0, paddingBottom: 12, gap: 8 }, children: [_jsx(Text, { style: { color: c.secondary, fontSize: 13, fontWeight: '600', marginTop: 4 }, children: uiText("Question") }), _jsx(TextInput, { accessibilityLabel: uiText("Poll question"), placeholder: uiText("Ask a question"), placeholderTextColor: c.secondary, maxLength: 300, multiline: true, value: question, onChangeText: setQuestion, editable: !busy, style: inputStyle }), !yesNo && _jsxs(_Fragment, { children: [_jsx(Text, { style: { color: c.secondary, fontSize: 13, fontWeight: '600', marginTop: 12 }, children: uiText("Answers") }), options.map((option, i) => _jsxs(View, { style: { flexDirection: 'row', alignItems: 'center', gap: 8 }, children: [_jsx(TextInput, { accessibilityLabel: uiText("Option {value1}", { value1: i + 1 }), placeholder: uiText("Add an option"), placeholderTextColor: c.secondary, maxLength: 100, value: option, onChangeText: v => setOptions(items => items.map((x, n) => n === i ? v : x)), editable: !busy, style: { ...inputStyle, flex: 1 } }), options.length > 2 && _jsx(Pressable, { accessibilityRole: "button", accessibilityLabel: uiText("Remove option {value1}", { value1: i + 1 }), disabled: busy, onPress: () => setOptions(items => items.filter((_, n) => n !== i)), style: { width: 44, height: 44, alignItems: 'center', justifyContent: 'center' }, children: _jsx(Text, { style: { color: c.secondary, fontSize: 26 }, children: "\u00D7" }) })] }, i)), options.length < 12 && _jsx(Pressable, { accessibilityRole: "button", disabled: busy, onPress: () => setOptions(items => [...items, '']), style: { minHeight: 44, justifyContent: 'center' }, children: _jsx(Text, { style: { color: c.accent, fontSize: 16 }, children: uiText("Add option") }) }), _jsxs(View, { style: { minHeight: 48, flexDirection: 'row', gap: 12, alignItems: 'center', marginTop: 8 }, children: [_jsx(Text, { style: { color: c.foreground, fontSize: 16, flex: 1 }, children: uiText("Allow multiple answers") }), _jsx(Switch, { accessibilityLabel: uiText("Allow multiple answers"), disabled: busy, value: multiple, onValueChange: setMultiple, trackColor: { true: c.accent } })] }), _jsx(Text, { style: { color: c.secondary, fontSize: 13, lineHeight: 19 }, children: uiText("Everyone in the chat can see the votes.") })] }), yesNo && _jsx(Text, { style: { color: c.secondary, fontSize: 13, lineHeight: 19, marginTop: 8 }, children: uiText("Their first Yes or No answer is final.") }), !!error && _jsx(Text, { accessibilityRole: "alert", style: { color: c.foreground, marginTop: 8 }, children: error })] }) }), _jsxs(View, { style: { flexDirection: 'row', gap: 8, marginTop: 8 }, children: [_jsx(Pressable, { accessibilityRole: "button", accessibilityLabel: uiText("Cancel"), disabled: busy, onPress: onCancel, style: { flex: 1, minHeight: 44, borderRadius: 12, backgroundColor: c.secondary + '20', alignItems: 'center', justifyContent: 'center' }, children: _jsx(Text, { style: { color: c.foreground, fontSize: 16, fontWeight: '600' }, children: uiText("Cancel") }) }), _jsx(Pressable, { accessibilityRole: "button", accessibilityLabel: yesNo ? uiText("Send question") : uiText("Send poll"), disabled: busy || submitted || !ready, onPress: () => void send(), style: { flex: 1, minHeight: 44, borderRadius: 12, backgroundColor: c.accent, opacity: (busy || submitted || !ready) ? 0.45 : 1, alignItems: 'center', justifyContent: 'center' }, children: busy ? _jsx(ActivityIndicator, { color: "#fff" }) : _jsx(Text, { style: { color: '#fff', fontSize: 16, fontWeight: '600' }, children: uiText("Send") }) })] })] }) });
|
|
123
127
|
}
|
package/dist/popup.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { useRobonoLocale } from './localization.js';
|
|
2
3
|
import { createContext, useContext, useEffect, useLayoutEffect, useRef, useState, useCallback, useMemo } from 'react';
|
|
3
4
|
import { Keyboard, KeyboardAvoidingView, Modal, Platform, Pressable, SafeAreaView, Text, View, useWindowDimensions } from 'react-native';
|
|
4
5
|
import { MessageIcon } from './icons.js';
|
|
@@ -7,8 +8,9 @@ export const MessagingPopupSurface = createContext(null);
|
|
|
7
8
|
export const messagingPopupBackdrop = '#000000d9';
|
|
8
9
|
export const messagingPopupColors = { background: '#000000', foreground: '#ffffff', secondary: '#aaaaaa', incoming: '#202020' };
|
|
9
10
|
/** One title/close row for forms, menus and expanded content. */
|
|
10
|
-
export function MessagingPopupHeader({ title, foreground, onClose, closeLabel
|
|
11
|
-
|
|
11
|
+
export function MessagingPopupHeader({ title, foreground, onClose, closeLabel, disabled = false, back = false }) {
|
|
12
|
+
const { text: uiText } = useRobonoLocale();
|
|
13
|
+
return _jsxs(View, { style: { flexDirection: 'row', alignItems: 'center', gap: 8, minHeight: 44, borderBottomWidth: 1, borderBottomColor: '#ffffff18' }, children: [_jsx(Text, { accessibilityRole: "header", style: { flex: 1, color: foreground, fontSize: 17, lineHeight: 23, fontWeight: '600' }, children: title }), _jsx(Pressable, { accessibilityRole: "button", accessibilityLabel: closeLabel ?? uiText('Close {name}', { name: title }), disabled: disabled, accessibilityState: { disabled }, onPress: onClose, style: { width: 44, height: 44, alignItems: 'center', justifyContent: 'center', opacity: disabled ? 0.45 : 1 }, children: _jsx(MessageIcon, { name: back ? 'back' : 'close', color: foreground, size: 24 }) })] });
|
|
12
14
|
}
|
|
13
15
|
// Measure the host and its persistent header, including embedded native layouts.
|
|
14
16
|
const PopupBounds = createContext(null);
|
|
@@ -21,12 +23,13 @@ export function MessagingPopupLayer({ children }) {
|
|
|
21
23
|
return update ? null : _jsx(_Fragment, { children: children });
|
|
22
24
|
}
|
|
23
25
|
export function MessagingPopupHost({ children }) {
|
|
26
|
+
const { direction } = useRobonoLocale();
|
|
24
27
|
const root = useRef(null), header = useRef(null), [revision, setRevision] = useState(0);
|
|
25
28
|
const [layer, setLayer] = useState(null);
|
|
26
29
|
const update = useCallback((owner, node) => setLayer(current => node ? { owner, node } : current?.owner === owner ? null : current), []);
|
|
27
30
|
const changed = useCallback(() => setRevision(value => value + 1), []);
|
|
28
31
|
const value = useMemo(() => ({ root, header, revision, changed }), [revision, changed]);
|
|
29
|
-
return _jsx(PopupBounds.Provider, { value: value, children: _jsx(PopupLayer.Provider, { value: update, children: _jsxs(View, { ref: root, collapsable: false, onLayout: changed, style: { flex: 1, minHeight: 0 }, children: [children, layer?.node] }) }) });
|
|
32
|
+
return _jsx(PopupBounds.Provider, { value: value, children: _jsx(PopupLayer.Provider, { value: update, children: _jsxs(View, { ref: root, collapsable: false, onLayout: changed, style: { flex: 1, minHeight: 0, direction }, children: [children, layer?.node] }) }) });
|
|
30
33
|
}
|
|
31
34
|
export function MessagingPopupHeaderBoundary({ children }) {
|
|
32
35
|
const bounds = useContext(PopupBounds);
|
package/dist/quick-send.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { useRobonoLocale } from './localization.js';
|
|
2
3
|
import { MessagingPopupHeader, messagingPopupBackdrop, MessagingMenuSurface } from './popup.js';
|
|
3
4
|
import { useEffect, useRef, useState } from 'react';
|
|
4
5
|
import { Platform, Pressable, ScrollView, Text, View, useWindowDimensions } from 'react-native';
|
|
@@ -6,13 +7,14 @@ import { quickMessageOptions, validQuickMessage } from '@robono/messaging';
|
|
|
6
7
|
import { MessageIcon, chatSurface } from './icons.js';
|
|
7
8
|
/** Messenger's empty-composer shortcut and anchored picker. Selecting never sends. */
|
|
8
9
|
export function QuickSendButton({ choice, empty, enabled, disabled, editing, sendAvailable, appearance, onChoose, onSend, onQuickSend, onSchedule }) {
|
|
10
|
+
const { text: uiText, locale: uiLocale, number: uiNumber, count: uiCount } = useRobonoLocale();
|
|
9
11
|
const [anchor, setAnchor] = useState(null), held = useRef(false), button = useRef(null), viewport = useWindowDimensions();
|
|
10
12
|
const emoji = validQuickMessage(choice) ? choice : '👍', quick = empty && enabled && !editing, unavailable = disabled || (!quick && !sendAvailable), c = appearance.colors;
|
|
11
13
|
useEffect(() => { if (!quick || disabled)
|
|
12
14
|
setAnchor(null); }, [quick, disabled]);
|
|
13
15
|
const show = () => { held.current = true; button.current?.measureInWindow((x, y, width) => setAnchor({ x: x + width, y })); };
|
|
14
16
|
const label = quickMessageOptions.find(option => option.emoji === emoji).label;
|
|
15
|
-
return _jsxs(_Fragment, { children: [_jsx(Pressable, { ref: button, testID: "quick-send-button", accessibilityRole: "button", accessibilityLabel: quick ? 'Send '
|
|
17
|
+
return _jsxs(_Fragment, { children: [_jsx(Pressable, { ref: button, testID: "quick-send-button", accessibilityRole: "button", accessibilityLabel: quick ? uiText('Send {label}', { label: uiText(label) }) : editing ? uiText("Save message") : appearance.labels.send, accessibilityHint: quick ? uiText("Hold to choose a different quick message.") : onSchedule ? uiText("Hold to schedule this message.") : undefined, accessibilityState: { disabled: unavailable, expanded: anchor !== null }, accessibilityActions: quick ? [{ name: 'choose', label: uiText("Choose quick message") }] : onSchedule ? [{ name: 'schedule', label: uiText("Send later") }] : undefined, onAccessibilityAction: event => { if (event.nativeEvent.actionName === 'choose' && quick && !unavailable)
|
|
16
18
|
show();
|
|
17
19
|
else if (event.nativeEvent.actionName === 'schedule' && !unavailable)
|
|
18
20
|
onSchedule?.(); }, disabled: unavailable, delayLongPress: 350, onPressIn: () => { held.current = false; }, onLongPress: quick ? show : onSchedule ? () => { held.current = true; onSchedule(); } : undefined, onPress: () => { if (held.current) {
|
|
@@ -34,5 +36,5 @@ export function QuickSendButton({ choice, empty, enabled, disabled, editing, sen
|
|
|
34
36
|
held.current = true;
|
|
35
37
|
onSchedule?.();
|
|
36
38
|
}
|
|
37
|
-
} } } : {}), style: { width: 44, height: 44, alignItems: 'center', justifyContent: 'center', opacity: unavailable ? .4 : 1 }, children: quick ? _jsx(Text, { style: { fontSize: 26, lineHeight: 32 }, children: emoji }) : _jsx(MessageIcon, { name: editing ? 'check' : 'send', size: 24, color: c.accent }) }), _jsx(MessagingMenuSurface, { visible: anchor !== null, onClose: () => setAnchor(null), children: rect => _jsxs(View, { accessibilityViewIsModal: true, style: { flex: 1 }, children: [_jsx(Pressable, { accessibilityLabel: "Close quick messages", onPress: () => setAnchor(null), style: { position: 'absolute', inset: 0, backgroundColor: messagingPopupBackdrop } }), _jsxs(View, { style: { position: 'absolute', width: Math.min(304, rect.width - 24), maxHeight: Math.max(100, rect.height - 24), left: Math.max(12, Math.min((anchor?.x ?? rect.width) - rect.left - 304, rect.width - Math.min(304, rect.width - 24) - 12)), top: Math.max(12, Math.min((anchor?.y ?? rect.height) - rect.top - 282, rect.height - 312)), backgroundColor: chatSurface(c.background), borderRadius: 18, borderWidth: 1, borderColor: c.secondary + '40', padding: 8, boxShadow: '0 4px 18px #0005' }, children: [_jsx(MessagingPopupHeader, { title: "Quick message", foreground: c.foreground, closeLabel: "Close quick message picker", onClose: () => setAnchor(null) }), _jsx(ScrollView, { style: { maxHeight: Math.min(220, Math.max(44, rect.height - 90)) }, keyboardShouldPersistTaps: "handled", contentContainerStyle: { flexDirection: 'row', flexWrap: 'wrap', justifyContent: 'center' }, children: quickMessageOptions.map(item => _jsx(Pressable, { accessibilityRole: "button", accessibilityLabel: item.label, accessibilityState: { selected: emoji === item.emoji }, onPress: () => { held.current = false; setAnchor(null); onChoose(item.emoji); }, style: { width: 46, height: 44, alignItems: 'center', justifyContent: 'center', borderRadius: 10, backgroundColor: emoji === item.emoji ? c.accent + '20' : 'transparent' }, children: _jsx(Text, { style: { fontSize: 26, lineHeight: 32 }, children: item.emoji }) }, item.emoji)) })] })] }) })] });
|
|
39
|
+
} } } : {}), style: { width: 44, height: 44, alignItems: 'center', justifyContent: 'center', opacity: unavailable ? .4 : 1 }, children: quick ? _jsx(Text, { style: { fontSize: 26, lineHeight: 32 }, children: emoji }) : _jsx(MessageIcon, { name: editing ? 'check' : 'send', size: 24, color: c.accent }) }), _jsx(MessagingMenuSurface, { visible: anchor !== null, onClose: () => setAnchor(null), children: rect => _jsxs(View, { accessibilityViewIsModal: true, style: { flex: 1 }, children: [_jsx(Pressable, { accessibilityLabel: uiText("Close quick messages"), onPress: () => setAnchor(null), style: { position: 'absolute', inset: 0, backgroundColor: messagingPopupBackdrop } }), _jsxs(View, { style: { position: 'absolute', width: Math.min(304, rect.width - 24), maxHeight: Math.max(100, rect.height - 24), left: Math.max(12, Math.min((anchor?.x ?? rect.width) - rect.left - 304, rect.width - Math.min(304, rect.width - 24) - 12)), top: Math.max(12, Math.min((anchor?.y ?? rect.height) - rect.top - 282, rect.height - 312)), backgroundColor: chatSurface(c.background), borderRadius: 18, borderWidth: 1, borderColor: c.secondary + '40', padding: 8, boxShadow: '0 4px 18px #0005' }, children: [_jsx(MessagingPopupHeader, { title: uiText("Quick message"), foreground: c.foreground, closeLabel: uiText("Close quick message picker"), onClose: () => setAnchor(null) }), _jsx(ScrollView, { style: { maxHeight: Math.min(220, Math.max(44, rect.height - 90)) }, keyboardShouldPersistTaps: "handled", contentContainerStyle: { flexDirection: 'row', flexWrap: 'wrap', justifyContent: 'center' }, children: quickMessageOptions.map(item => _jsx(Pressable, { accessibilityRole: "button", accessibilityLabel: uiText(item.label), accessibilityState: { selected: emoji === item.emoji }, onPress: () => { held.current = false; setAnchor(null); onChoose(item.emoji); }, style: { width: 46, height: 44, alignItems: 'center', justifyContent: 'center', borderRadius: 10, backgroundColor: emoji === item.emoji ? c.accent + '20' : 'transparent' }, children: _jsx(Text, { style: { fontSize: 26, lineHeight: 32 }, children: item.emoji }) }, item.emoji)) })] })] }) })] });
|
|
38
40
|
}
|
package/dist/safety.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { useRobonoLocale } from './localization.js';
|
|
2
3
|
import { MessagingMenuSurface } from './popup.js';
|
|
3
4
|
import { messagingPopupColors } from './popup.js';
|
|
4
5
|
import { useEffect, useRef, useState } from 'react';
|
|
@@ -7,6 +8,7 @@ import { MessageIcon } from './icons.js';
|
|
|
7
8
|
import { Platform, Pressable, ScrollView, Text, TextInput, View, useWindowDimensions } from 'react-native';
|
|
8
9
|
/** Uses the authenticated messaging client's durable safety operations, or the host can keep its own UI. */
|
|
9
10
|
export function MessagingSafety({ client, state, appearance, name, messageId, onClose, onSearch, onDetails, onPins, onScheduled, onSaved, children, menuAnchor }) {
|
|
11
|
+
const { text: uiText, locale: uiLocale, number: uiNumber, count: uiCount } = useRobonoLocale();
|
|
10
12
|
const [mode, setMode] = useState(messageId ? 'report' : 'menu');
|
|
11
13
|
const [category, setCategory] = useState(''), [details, setDetails] = useState(''), [busy, setBusy] = useState(false), [error, setError] = useState('');
|
|
12
14
|
const reportRequest = useRef(null), reportContent = useRef('');
|
|
@@ -24,14 +26,14 @@ export function MessagingSafety({ client, state, appearance, name, messageId, on
|
|
|
24
26
|
const search = () => { afterClose.current = onSearch ?? null; setVisible(false); };
|
|
25
27
|
const c = { ...appearance.colors, ...messagingPopupColors }, safety = state.conversation?.safety, blocked = safety?.blockedByMe;
|
|
26
28
|
const primaryInk = (() => { const hex = c.accent.replace('#', ''); const expanded = hex.length === 3 ? hex.split('').map(x => x + x).join('') : hex; const value = parseInt(expanded, 16); return Number.isFinite(value) && ((value >> 16) * .299 + ((value >> 8) & 255) * .587 + (value & 255) * .114) > 155 ? '#111111' : '#ffffff'; })();
|
|
27
|
-
const button = (label, press, disabled = false, kind = 'plain') => _jsx(Pressable, { accessibilityRole: "button", accessibilityLabel: label, accessibilityState: { disabled: busy || disabled }, disabled: busy || disabled, onPress: press, style: ({ pressed }) => ({ minHeight: 44, paddingHorizontal: 12, paddingVertical: 10, justifyContent: 'center', alignItems: kind === 'plain' ? 'flex-start' : 'center', borderRadius: 12, flex: kind === 'plain' ? undefined : 1, backgroundColor: kind === 'primary' ? c.accent : kind === 'danger' ? '#b72d34' : kind === 'cancel' ? c.incoming : 'transparent', opacity: (busy || disabled) ? 0.5 : pressed ? 0.7 : 1 }), children: _jsx(Text, { style: { color: kind === 'primary' ? primaryInk : kind === 'danger' ? '#fff' : c.foreground, fontSize: 16, fontWeight: kind === 'plain' ? '400' : '600' }, children: label }) });
|
|
28
|
-
const menuRow = (label, icon, press, accessibilityLabel = label) => _jsxs(Pressable, { accessibilityRole: "button", accessibilityLabel: accessibilityLabel, onPress: press, style: ({ pressed }) => ({ minHeight: 44, paddingHorizontal: 12, flexDirection: 'row', alignItems: 'center', gap: 12, backgroundColor: pressed ? '#ffffff0c' : 'transparent' }), children: [_jsx(MessageIcon, { name: icon, color: c.secondary, size: 22 }), _jsx(Text, { style: { flex: 1, color: c.foreground, fontSize: 15, fontWeight: '600' }, children: label })] });
|
|
29
|
+
const button = (label, press, disabled = false, kind = 'plain') => _jsx(Pressable, { accessibilityRole: "button", accessibilityLabel: uiText(label), accessibilityState: { disabled: busy || disabled }, disabled: busy || disabled, onPress: press, style: ({ pressed }) => ({ minHeight: 44, paddingHorizontal: 12, paddingVertical: 10, justifyContent: 'center', alignItems: kind === 'plain' ? 'flex-start' : 'center', borderRadius: 12, flex: kind === 'plain' ? undefined : 1, backgroundColor: kind === 'primary' ? c.accent : kind === 'danger' ? '#b72d34' : kind === 'cancel' ? c.incoming : 'transparent', opacity: (busy || disabled) ? 0.5 : pressed ? 0.7 : 1 }), children: _jsx(Text, { style: { color: kind === 'primary' ? primaryInk : kind === 'danger' ? '#fff' : c.foreground, fontSize: 16, fontWeight: kind === 'plain' ? '400' : '600' }, children: uiText(label) }) });
|
|
30
|
+
const menuRow = (label, icon, press, accessibilityLabel = label) => _jsxs(Pressable, { accessibilityRole: "button", accessibilityLabel: uiText(accessibilityLabel), onPress: press, style: ({ pressed }) => ({ minHeight: 44, paddingHorizontal: 12, flexDirection: 'row', alignItems: 'center', gap: 12, backgroundColor: pressed ? '#ffffff0c' : 'transparent' }), children: [_jsx(MessageIcon, { name: icon, color: c.secondary, size: 22 }), _jsx(Text, { style: { flex: 1, color: c.foreground, fontSize: 15, fontWeight: '600' }, children: uiText(label) })] });
|
|
29
31
|
async function block() { setBusy(true); setError(''); try {
|
|
30
32
|
await client.setBlocked(!blocked);
|
|
31
33
|
onClose();
|
|
32
34
|
}
|
|
33
35
|
catch {
|
|
34
|
-
setError(
|
|
36
|
+
setError(uiText("The change was not saved. Try again."));
|
|
35
37
|
}
|
|
36
38
|
finally {
|
|
37
39
|
setBusy(false);
|
|
@@ -53,7 +55,7 @@ export function MessagingSafety({ client, state, appearance, name, messageId, on
|
|
|
53
55
|
setMode('sent');
|
|
54
56
|
}
|
|
55
57
|
catch {
|
|
56
|
-
setError(
|
|
58
|
+
setError(uiText("The report could not be confirmed. Try again."));
|
|
57
59
|
}
|
|
58
60
|
finally {
|
|
59
61
|
setBusy(false);
|
|
@@ -62,8 +64,8 @@ export function MessagingSafety({ client, state, appearance, name, messageId, on
|
|
|
62
64
|
const choose = (next) => { afterClose.current = () => { setMode(next); setVisible(true); }; setVisible(false); };
|
|
63
65
|
if (mode === 'menu' && menuAnchor) {
|
|
64
66
|
const count = Number(!!onSearch) + Number(!!onDetails) + Number(!!onPins) + Number(!!onScheduled) + Number(!!children) + Number(!!safety?.canBlock) + Number(!!safety?.canReport), height = count * 44 + 8;
|
|
65
|
-
return _jsx(MessagingMenuSurface, { visible: visible, onDismiss: dismissed, onClose: onClose, children: rect => _jsxs(View, { accessibilityViewIsModal: true, style: { flex: 1 }, children: [_jsx(Pressable, { accessibilityLabel: "Close contact options", onPress: onClose, style: { position: 'absolute', inset: 0 } }), _jsx(View, { testID: "messaging-contact-dropdown", style: { position: 'absolute', left: Math.max(12, Math.min(menuAnchor.x - rect.left, rect.width - Math.min(240, rect.width - 24) - 12)), top: Math.max(12, Math.min(menuAnchor.y - rect.top, rect.height - height - 12)), width: Math.min(240, rect.width - 24), maxHeight: rect.height - 24, borderRadius: 16, borderWidth: .5, borderColor: '#ffffff22', backgroundColor: '#151515', paddingVertical: 4, overflow: 'hidden', boxShadow: '0 8px 18px #0006' }, children: _jsxs(ScrollView, { bounces: false, keyboardShouldPersistTaps: "handled", children: [onDetails && menuRow(state.conversation?.kind === 'group' ? 'Group details' : 'Contact details', 'person', () => { afterClose.current = onDetails; setVisible(false); }), onPins && menuRow('Pinned messages', 'bookmark', () => { afterClose.current = onPins; setVisible(false); }), onScheduled && menuRow('Scheduled messages', 'send', () => { afterClose.current = onScheduled; setVisible(false); }), onSearch && menuRow('Search this chat', 'search', search), children && menuRow('Translation', 'translate', () => choose('translation')), safety?.canBlock && menuRow(blocked ? 'Unblock contact' : 'Block contact', 'shield', () => choose('block')), safety?.canReport && menuRow('Report', 'flag-outline', () => choose('report'))] }) })] }) });
|
|
67
|
+
return _jsx(MessagingMenuSurface, { visible: visible, onDismiss: dismissed, onClose: onClose, children: rect => _jsxs(View, { accessibilityViewIsModal: true, style: { flex: 1 }, children: [_jsx(Pressable, { accessibilityLabel: uiText("Close contact options"), onPress: onClose, style: { position: 'absolute', inset: 0 } }), _jsx(View, { testID: "messaging-contact-dropdown", style: { position: 'absolute', left: Math.max(12, Math.min(menuAnchor.x - rect.left, rect.width - Math.min(240, rect.width - 24) - 12)), top: Math.max(12, Math.min(menuAnchor.y - rect.top, rect.height - height - 12)), width: Math.min(240, rect.width - 24), maxHeight: rect.height - 24, borderRadius: 16, borderWidth: .5, borderColor: '#ffffff22', backgroundColor: '#151515', paddingVertical: 4, overflow: 'hidden', boxShadow: '0 8px 18px #0006' }, children: _jsxs(ScrollView, { bounces: false, keyboardShouldPersistTaps: "handled", children: [onDetails && menuRow(state.conversation?.kind === 'group' ? 'Group details' : 'Contact details', 'person', () => { afterClose.current = onDetails; setVisible(false); }), onPins && menuRow('Pinned messages', 'bookmark', () => { afterClose.current = onPins; setVisible(false); }), onScheduled && menuRow('Scheduled messages', 'send', () => { afterClose.current = onScheduled; setVisible(false); }), onSearch && menuRow('Search this chat', 'search', search), children && menuRow('Translation', 'translate', () => choose('translation')), safety?.canBlock && menuRow(blocked ? 'Unblock contact' : 'Block contact', 'shield', () => choose('block')), safety?.canReport && menuRow('Report', 'flag-outline', () => choose('report'))] }) })] }) });
|
|
66
68
|
}
|
|
67
|
-
return _jsx(MessagingPopup, { compact: mode === 'block' || mode === 'sent', withinChat: mode === 'translation', contentWidth: 640, visible: visible, onDismiss: dismissed, background: c.background, foreground: c.foreground, closeLabel: "Close contact options", closeDisabled: busy, title: mode === 'menu' ? name : mode === 'translation' ?
|
|
68
|
-
onClose(); }, children: _jsxs(ScrollView, { keyboardShouldPersistTaps: "handled", style: { ...(mode === 'block' || mode === 'sent' ? { flexGrow: 0, flexShrink: 1 } : { flex: 1 }), minHeight: 0 }, contentContainerStyle: { gap: 20, paddingBottom: 12 }, children: [mode === 'menu' && _jsxs(_Fragment, { children: [onSearch && _jsx(View, { style: { backgroundColor: '#111111', borderRadius: 12, borderWidth: 1, borderColor: '#ffffff14', overflow: 'hidden' }, children: menuRow('Search this chat', 'search', search) }), children, (safety?.canBlock || safety?.canReport) && _jsxs(View, { style: { backgroundColor: '#111111', borderRadius: 12, borderWidth: 1, borderColor: '#ffffff14', overflow: 'hidden' }, children: [safety?.canBlock && menuRow(blocked ? 'Unblock contact' : 'Block contact', 'shield', () => setMode('block')), safety?.canReport && menuRow('Report', 'flag-outline', () => setMode('report'))] })] }), mode === 'translation' && children, mode === 'block' && _jsx(_Fragment, { children: _jsx(Text, { style: { fontSize: 14, lineHeight: 21, color: c.secondary }, children: blocked ?
|
|
69
|
+
return _jsx(MessagingPopup, { compact: mode === 'block' || mode === 'sent', withinChat: mode === 'translation', contentWidth: 640, visible: visible, onDismiss: dismissed, background: c.background, foreground: c.foreground, closeLabel: uiText("Close contact options"), closeDisabled: busy, title: mode === 'menu' ? name : mode === 'translation' ? uiText("Translation") : mode === 'block' ? blocked ? uiText('Unblock {name}?', { name }) : uiText('Block {name}?', { name }) : mode === 'sent' ? uiText("Report sent") : messageId ? uiText("Report message") : uiText("Report conversation"), onClose: () => { if (!busy)
|
|
70
|
+
onClose(); }, children: _jsxs(ScrollView, { keyboardShouldPersistTaps: "handled", style: { ...(mode === 'block' || mode === 'sent' ? { flexGrow: 0, flexShrink: 1 } : { flex: 1 }), minHeight: 0 }, contentContainerStyle: { gap: 20, paddingBottom: 12 }, children: [mode === 'menu' && _jsxs(_Fragment, { children: [onSearch && _jsx(View, { style: { backgroundColor: '#111111', borderRadius: 12, borderWidth: 1, borderColor: '#ffffff14', overflow: 'hidden' }, children: menuRow('Search this chat', 'search', search) }), children, (safety?.canBlock || safety?.canReport) && _jsxs(View, { style: { backgroundColor: '#111111', borderRadius: 12, borderWidth: 1, borderColor: '#ffffff14', overflow: 'hidden' }, children: [safety?.canBlock && menuRow(blocked ? 'Unblock contact' : 'Block contact', 'shield', () => setMode('block')), safety?.canReport && menuRow('Report', 'flag-outline', () => setMode('report'))] })] }), mode === 'translation' && children, mode === 'block' && _jsx(_Fragment, { children: _jsx(Text, { style: { fontSize: 14, lineHeight: 21, color: c.secondary }, children: blocked ? uiText("You will be able to message and call each other again. Messages attempted while blocked will not be delivered.") : uiText("This stops direct messages and calls between you. Your existing chat stays. They will not receive a notification that you blocked them. Shared group messages are not hidden.") }) }), mode === 'report' && _jsxs(_Fragment, { children: [_jsxs(Text, { style: { fontSize: 14, lineHeight: 21, color: c.secondary }, children: [messageId ? uiText("This message and your reason will be shared with this app’s moderation team.") : uiText("Up to five recent messages from other people in this conversation and your reason will be shared with this app’s moderation team."), uiText("Report details are kept for up to 30 days. Reporting does not block this contact.")] }), ['Spam', 'Harassment', 'Inappropriate content', 'Other'].map(value => _jsxs(Pressable, { accessibilityRole: "radio", accessibilityLabel: uiText(value), accessibilityState: { checked: category === value }, disabled: busy, onPress: () => setCategory(value), style: { minHeight: 40, flexDirection: 'row', alignItems: 'center', gap: 10 }, children: [_jsx(View, { style: { width: 18, height: 18, borderRadius: 9, borderWidth: 2, borderColor: c.accent, backgroundColor: category === value ? c.accent : 'transparent' } }), _jsx(Text, { style: { color: c.foreground, fontSize: 15 }, children: uiText(value) })] }, value)), _jsx(TextInput, { accessibilityLabel: uiText("Report details"), placeholder: uiText("Tell us what happened (optional)"), placeholderTextColor: c.secondary, value: details, onChangeText: setDetails, multiline: true, maxLength: 1800, editable: !busy, style: { color: c.foreground, minHeight: 80, borderWidth: 1, borderColor: c.secondary + '60', borderRadius: 10, padding: 12, fontSize: 15 } })] }), mode === 'sent' && _jsx(Text, { style: { color: c.secondary, fontSize: 15, lineHeight: 21 }, children: uiText("Your report was saved for review. The other person is not notified.") }), !!error && _jsx(Text, { accessibilityRole: "alert", style: { color: c.foreground }, children: error }), mode !== 'menu' && mode !== 'translation' && _jsxs(View, { style: { flexDirection: 'row', gap: 8, marginTop: 8 }, children: [(mode === 'block' || mode === 'report') && button('Cancel', onClose, false, 'cancel'), mode === 'block' && button(blocked ? 'Unblock' : 'Block', () => void block(), false, blocked ? 'primary' : 'danger'), mode === 'report' && button(busy ? 'Sending…' : 'Send report', () => void report(), !category && !details.trim(), 'primary'), mode === 'sent' && button('Done', onClose, false, 'primary')] })] }) });
|
|
69
71
|
}
|