@robono/messaging-react-native 0.1.0 → 0.3.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 +65 -0
- package/README.md +160 -8
- package/SBOM.spdx.json +1985 -209
- package/dist/account.d.ts +15 -0
- package/dist/account.js +27 -0
- package/dist/call-message.js +7 -1
- package/dist/calling.js +5 -9
- package/dist/conversation-details.d.ts +24 -0
- package/dist/conversation-details.js +83 -0
- package/dist/emoji-text.d.ts +2 -0
- package/dist/emoji-text.js +8 -0
- package/dist/expo-download.d.ts +3 -0
- package/dist/expo-download.js +43 -0
- package/dist/expo-history.d.ts +3 -0
- package/dist/expo-history.js +59 -0
- package/dist/expo-media.d.ts +1 -0
- package/dist/expo-media.js +69 -43
- package/dist/expo-messenger.d.ts +35 -0
- package/dist/expo-messenger.js +39 -0
- package/dist/expo-notifications.d.ts +31 -0
- package/dist/expo-notifications.js +92 -0
- package/dist/expo-outbox.d.ts +13 -0
- package/dist/expo-outbox.js +82 -0
- package/dist/expo-privacy.d.ts +4 -0
- package/dist/expo-privacy.js +7 -0
- package/dist/expo-saved.d.ts +24 -0
- package/dist/expo-saved.js +76 -0
- package/dist/expo-screen.d.ts +42 -0
- package/dist/expo-screen.js +557 -0
- package/dist/expo-social.d.ts +3 -0
- package/dist/expo-social.js +21 -0
- package/dist/expo-thumbnails.d.ts +9 -0
- package/dist/expo-thumbnails.js +68 -0
- package/dist/expo.d.ts +2 -36
- package/dist/expo.js +2 -429
- package/dist/extras-lifecycle.d.ts +2 -0
- package/dist/extras-lifecycle.js +27 -0
- package/dist/extras.d.ts +49 -0
- package/dist/extras.js +49 -0
- package/dist/forward.d.ts +13 -0
- package/dist/forward.js +62 -0
- package/dist/group-editor.d.ts +18 -0
- package/dist/group-editor.js +98 -0
- package/dist/header-calls.d.ts +18 -0
- package/dist/header-calls.js +45 -0
- package/dist/icons.d.ts +1 -1
- package/dist/inbox-search.d.ts +11 -0
- package/dist/inbox-search.js +42 -0
- package/dist/inbox.d.ts +19 -0
- package/dist/inbox.js +52 -0
- package/dist/index.d.ts +76 -3
- package/dist/index.js +648 -167
- package/dist/media-controller.d.ts +21 -0
- package/dist/media-controller.js +157 -3
- package/dist/media-readiness.d.ts +1 -1
- package/dist/media-ui.d.ts +27 -5
- package/dist/media-ui.js +95 -29
- package/dist/message-links.d.ts +5 -0
- package/dist/message-links.js +30 -0
- package/dist/message-menu.d.ts +28 -0
- package/dist/message-menu.js +24 -0
- package/dist/message-order.d.ts +7 -0
- package/dist/message-order.js +27 -0
- package/dist/messenger-video-view.d.ts +8 -0
- package/dist/messenger-video-view.js +46 -0
- package/dist/native-outbox.d.ts +55 -0
- package/dist/native-outbox.js +53 -0
- package/dist/photo-gesture.d.ts +20 -0
- package/dist/photo-gesture.js +52 -0
- package/dist/pollVoteQueue.d.ts +3 -0
- package/dist/pollVoteQueue.js +29 -0
- package/dist/polls.d.ts +7 -2
- package/dist/polls.js +89 -20
- package/dist/popup.d.ts +51 -0
- package/dist/popup.js +73 -0
- package/dist/quick-send.d.ts +15 -0
- package/dist/quick-send.js +26 -0
- package/dist/recent-media.js +1 -0
- package/dist/safety.d.ts +10 -1
- package/dist/safety.js +29 -8
- package/dist/saved.d.ts +10 -0
- package/dist/saved.js +51 -0
- package/dist/search.d.ts +9 -0
- package/dist/search.js +59 -0
- package/dist/social.d.ts +25 -0
- package/dist/social.js +31 -0
- package/dist/translation-settings.d.ts +12 -0
- package/dist/translation-settings.js +16 -0
- package/dist/use-quote-gesture.d.ts +5 -1
- package/dist/use-quote-gesture.js +35 -17
- package/dist/visual/RobonoIcon.js +2 -1
- package/dist/visual/WeatherArtwork.d.ts +9 -0
- package/dist/visual/WeatherArtwork.js +43 -0
- package/dist/visual/weather-artwork.d.ts +17 -0
- package/dist/visual/weather-artwork.js +86 -0
- package/dist/visual/weather-conditions.d.ts +5 -0
- package/dist/visual/weather-conditions.js +2 -0
- package/dist/voice-composer.js +2 -2
- package/dist/voice-waveform.d.ts +2 -0
- package/dist/voice-waveform.js +21 -0
- package/package.json +41 -5
package/dist/popup.js
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { createContext, useContext, useEffect, useLayoutEffect, useRef, useState, useCallback, useMemo } from 'react';
|
|
3
|
+
import { Keyboard, KeyboardAvoidingView, Modal, Platform, Pressable, SafeAreaView, Text, View, useWindowDimensions } from 'react-native';
|
|
4
|
+
import { MessageIcon } from './icons.js';
|
|
5
|
+
/** Browser hosts can keep settings dialogs inside their embedded chat. */
|
|
6
|
+
export const MessagingPopupSurface = createContext(null);
|
|
7
|
+
export const messagingPopupBackdrop = '#000000d9';
|
|
8
|
+
export const messagingPopupColors = { background: '#000000', foreground: '#ffffff', secondary: '#aaaaaa', incoming: '#202020' };
|
|
9
|
+
/** One title/close row for forms, menus and expanded content. */
|
|
10
|
+
export function MessagingPopupHeader({ title, foreground, onClose, closeLabel = 'Close ' + title.toLowerCase(), disabled = false, back = false }) {
|
|
11
|
+
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, 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
|
+
}
|
|
13
|
+
// Measure the host and its persistent header, including embedded native layouts.
|
|
14
|
+
const PopupBounds = createContext(null);
|
|
15
|
+
export function MessagingPopupHost({ children }) {
|
|
16
|
+
const root = useRef(null), header = useRef(null), [revision, setRevision] = useState(0);
|
|
17
|
+
const changed = useCallback(() => setRevision(value => value + 1), []);
|
|
18
|
+
const value = useMemo(() => ({ root, header, revision, changed }), [revision, changed]);
|
|
19
|
+
return _jsx(PopupBounds.Provider, { value: value, children: _jsx(View, { ref: root, collapsable: false, onLayout: changed, style: { flex: 1, minHeight: 0 }, children: children }) });
|
|
20
|
+
}
|
|
21
|
+
export function MessagingPopupHeaderBoundary({ children }) {
|
|
22
|
+
const bounds = useContext(PopupBounds);
|
|
23
|
+
return _jsx(View, { ref: bounds?.header, collapsable: false, onLayout: bounds?.changed, children: children });
|
|
24
|
+
}
|
|
25
|
+
/** Content panels occupy the area below the persistent header; confirmations stay compact. */
|
|
26
|
+
export function MessagingPopup({ children, onClose, background, form = false, visible = true, onDismiss, expanded = true, bottomPadding, title, foreground, closeLabel, closeDisabled = false, contentWidth, headerBack = false, withinChat = false, compact = false }) {
|
|
27
|
+
const ChatSurface = useContext(MessagingPopupSurface), bounds = useContext(PopupBounds), viewport = useWindowDimensions();
|
|
28
|
+
const [rect, setRect] = useState(null);
|
|
29
|
+
useLayoutEffect(() => {
|
|
30
|
+
if (!visible || Platform.OS === 'web' || !bounds)
|
|
31
|
+
return;
|
|
32
|
+
let active = true;
|
|
33
|
+
// Fabric's Android measureInWindow subtracts the activity's system inset,
|
|
34
|
+
// while the edge-to-edge modal starts at the screen origin. Page coordinates
|
|
35
|
+
// retain that inset and keep the popup below the actual visible header.
|
|
36
|
+
const measure = (view, done) => {
|
|
37
|
+
if (Platform.OS === 'android')
|
|
38
|
+
view.measure((_x, _y, width, height, x, y) => done(x, y, width, height));
|
|
39
|
+
else
|
|
40
|
+
view.measureInWindow(done);
|
|
41
|
+
};
|
|
42
|
+
const frame = requestAnimationFrame(() => {
|
|
43
|
+
const root = bounds.root.current;
|
|
44
|
+
if (!root)
|
|
45
|
+
return;
|
|
46
|
+
measure(root, (x, y, width, height) => {
|
|
47
|
+
const apply = (top) => { if (active)
|
|
48
|
+
setRect({ left: x, top, width, height: Math.max(0, y + height - top) }); };
|
|
49
|
+
if (bounds.header.current)
|
|
50
|
+
measure(bounds.header.current, (_x, hy, _w, hh) => apply(Math.max(y, hy + hh)));
|
|
51
|
+
else
|
|
52
|
+
apply(y);
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
return () => { active = false; cancelAnimationFrame(frame); };
|
|
56
|
+
}, [visible, bounds, viewport.width, viewport.height]);
|
|
57
|
+
useEffect(() => { if (visible && Platform.OS !== 'web')
|
|
58
|
+
Keyboard.dismiss(); }, [visible]);
|
|
59
|
+
const close = useRef(onClose);
|
|
60
|
+
close.current = onClose;
|
|
61
|
+
useEffect(() => { if (!visible || Platform.OS !== 'web')
|
|
62
|
+
return; const escape = (event) => { if (event.key === 'Escape') {
|
|
63
|
+
event.preventDefault();
|
|
64
|
+
event.stopPropagation();
|
|
65
|
+
close.current();
|
|
66
|
+
} }; document.addEventListener('keydown', escape, true); return () => document.removeEventListener('keydown', escape, true); }, [visible]);
|
|
67
|
+
const bounded = Platform.OS !== 'web' && !!bounds;
|
|
68
|
+
const Frame = Platform.OS === 'ios' && !bounded ? SafeAreaView : View;
|
|
69
|
+
const content = _jsx(Frame, { style: [{ flex: 1, backgroundColor: messagingPopupBackdrop }, bounded ? { position: 'absolute', ...(rect ?? { left: 0, top: 0, width: 0, height: 0 }) } : undefined], children: _jsxs(KeyboardAvoidingView, { testID: "messaging-popup-backdrop", behavior: Platform.OS === 'ios' ? 'padding' : undefined, style: { flex: 1, justifyContent: 'center', alignItems: 'center', padding: compact ? 24 : 8 }, children: [_jsx(Pressable, { accessible: false, onPress: onClose, style: { position: 'absolute', inset: 0 } }), _jsxs(View, { testID: "messaging-popup-panel", accessibilityViewIsModal: true, onAccessibilityEscape: onClose, style: { width: '100%', ...(compact ? { maxWidth: 360, maxHeight: '100%', flexShrink: 1 } : { flex: 1, minHeight: 0 }), backgroundColor: compact ? '#111111' : messagingPopupColors.background, borderRadius: compact ? 20 : 18, paddingHorizontal: compact ? 20 : 8, paddingTop: compact ? 8 : 0, paddingBottom: bottomPadding ?? (compact ? 20 : 4), overflow: 'hidden' }, children: [title && _jsx(MessagingPopupHeader, { title: title, foreground: messagingPopupColors.foreground, onClose: onClose, closeLabel: closeLabel, disabled: closeDisabled, back: headerBack }), contentWidth ? _jsx(View, { style: { ...(compact ? { flexShrink: 1 } : { flex: 1, minHeight: 0 }), width: '100%', maxWidth: contentWidth, alignSelf: 'center', paddingTop: 12 }, children: children }) : children] })] }) });
|
|
70
|
+
if (Platform.OS === 'web' && ChatSurface)
|
|
71
|
+
return _jsx(ChatSurface, { visible: visible, title: title, onDismiss: onDismiss, onClose: onClose, children: content });
|
|
72
|
+
return _jsx(Modal, { visible: visible, statusBarTranslucent: true, navigationBarTranslucent: true, onDismiss: onDismiss, transparent: true, animationType: "fade", onRequestClose: onClose, children: content });
|
|
73
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { type MessagingAppearance } from '@robono/messaging';
|
|
3
|
+
/** Messenger's empty-composer shortcut and anchored picker. Selecting never sends. */
|
|
4
|
+
export declare function QuickSendButton({ choice, empty, enabled, disabled, editing, sendAvailable, appearance, onChoose, onSend, onQuickSend }: {
|
|
5
|
+
choice?: string;
|
|
6
|
+
empty: boolean;
|
|
7
|
+
enabled: boolean;
|
|
8
|
+
disabled: boolean;
|
|
9
|
+
editing: boolean;
|
|
10
|
+
sendAvailable: boolean;
|
|
11
|
+
appearance: MessagingAppearance;
|
|
12
|
+
onChoose: (emoji: string) => void;
|
|
13
|
+
onSend: () => void;
|
|
14
|
+
onQuickSend: (emoji: string) => void;
|
|
15
|
+
}): React.JSX.Element;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { MessagingPopupHeader, messagingPopupBackdrop } from './popup.js';
|
|
3
|
+
import { useEffect, useRef, useState } from 'react';
|
|
4
|
+
import { Modal, Platform, Pressable, ScrollView, Text, View, useWindowDimensions } from 'react-native';
|
|
5
|
+
import { quickMessageOptions, validQuickMessage } from '@robono/messaging';
|
|
6
|
+
import { MessageIcon, chatSurface } from './icons.js';
|
|
7
|
+
/** Messenger's empty-composer shortcut and anchored picker. Selecting never sends. */
|
|
8
|
+
export function QuickSendButton({ choice, empty, enabled, disabled, editing, sendAvailable, appearance, onChoose, onSend, onQuickSend }) {
|
|
9
|
+
const [anchor, setAnchor] = useState(null), held = useRef(false), button = useRef(null), viewport = useWindowDimensions();
|
|
10
|
+
const emoji = validQuickMessage(choice) ? choice : '👍', quick = empty && enabled && !editing, unavailable = disabled || (!quick && !sendAvailable), c = appearance.colors;
|
|
11
|
+
useEffect(() => { if (!quick || disabled)
|
|
12
|
+
setAnchor(null); }, [quick, disabled]);
|
|
13
|
+
const show = () => { held.current = true; button.current?.measureInWindow((x, y, width) => setAnchor({ x: x + width, y })); };
|
|
14
|
+
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 ' + label : editing ? 'Save message' : appearance.labels.send, accessibilityHint: quick ? 'Hold to choose a different quick message.' : undefined, accessibilityState: { disabled: unavailable, expanded: anchor !== null }, accessibilityActions: quick ? [{ name: 'choose', label: 'Choose quick message' }] : undefined, onAccessibilityAction: event => { if (event.nativeEvent.actionName === 'choose' && quick && !unavailable)
|
|
16
|
+
show(); }, disabled: unavailable, delayLongPress: 350, onPressIn: () => { held.current = false; }, onLongPress: quick ? show : undefined, onPress: () => { if (held.current) {
|
|
17
|
+
held.current = false;
|
|
18
|
+
return;
|
|
19
|
+
} quick ? onQuickSend(emoji) : onSend(); }, ...(Platform.OS === 'web' ? { onContextMenu: (event) => { if (quick && !unavailable) {
|
|
20
|
+
event.preventDefault();
|
|
21
|
+
show();
|
|
22
|
+
} }, onKeyDown: (event) => { if (quick && !unavailable && (event.key === 'F10' && event.shiftKey || event.key === 'ArrowUp')) {
|
|
23
|
+
event.preventDefault();
|
|
24
|
+
show();
|
|
25
|
+
} } } : {}), 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(Modal, { visible: anchor !== null, transparent: true, animationType: "fade", onRequestClose: () => setAnchor(null), children: _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, viewport.width - 24), left: Math.max(12, Math.min((anchor?.x ?? viewport.width) - 304, viewport.width - 316)), top: Math.max(48, Math.min((anchor?.y ?? viewport.height) - 282, viewport.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: 220 }, 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)) })] })] }) })] });
|
|
26
|
+
}
|
package/dist/recent-media.js
CHANGED
|
@@ -13,6 +13,7 @@ export class RecentMessagingMedia {
|
|
|
13
13
|
this.now = now;
|
|
14
14
|
}
|
|
15
15
|
storage = {
|
|
16
|
+
thumbnail: (message, context) => this.source.thumbnail?.(message, context) ?? Promise.resolve(null),
|
|
16
17
|
stream: (message, context) => this.source.stream?.(message, context) ?? Promise.resolve(null),
|
|
17
18
|
upload: (file, context) => this.source.upload(file, context), discard: id => this.source.discard(id),
|
|
18
19
|
download: async (message, context) => {
|
package/dist/safety.d.ts
CHANGED
|
@@ -1,11 +1,20 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import type { MessagingClient, MessagingAppearance, MessagingState } from '@robono/messaging';
|
|
3
3
|
/** Uses the authenticated messaging client's durable safety operations, or the host can keep its own UI. */
|
|
4
|
-
export declare function MessagingSafety({ client, state, appearance, name, messageId, onClose }: {
|
|
4
|
+
export declare function MessagingSafety({ client, state, appearance, name, messageId, onClose, onSearch, onDetails, onPins, onSaved, children, menuAnchor }: {
|
|
5
5
|
client: MessagingClient;
|
|
6
6
|
state: MessagingState;
|
|
7
7
|
appearance: MessagingAppearance;
|
|
8
8
|
name: string;
|
|
9
9
|
messageId?: string;
|
|
10
10
|
onClose: () => void;
|
|
11
|
+
onSearch?: () => void;
|
|
12
|
+
onDetails?: () => void;
|
|
13
|
+
onPins?: () => void;
|
|
14
|
+
onSaved?: () => void;
|
|
15
|
+
children?: React.ReactNode;
|
|
16
|
+
menuAnchor?: {
|
|
17
|
+
x: number;
|
|
18
|
+
y: number;
|
|
19
|
+
};
|
|
11
20
|
}): React.JSX.Element;
|
package/dist/safety.js
CHANGED
|
@@ -1,14 +1,30 @@
|
|
|
1
|
-
import { jsx as _jsx,
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { messagingPopupColors } from './popup.js';
|
|
3
|
+
import { useEffect, useRef, useState } from 'react';
|
|
4
|
+
import { MessagingPopup } from './popup.js';
|
|
5
|
+
import { MessageIcon } from './icons.js';
|
|
6
|
+
import { Modal, Platform, Pressable, ScrollView, Text, TextInput, View, useWindowDimensions } from 'react-native';
|
|
4
7
|
/** Uses the authenticated messaging client's durable safety operations, or the host can keep its own UI. */
|
|
5
|
-
export function MessagingSafety({ client, state, appearance, name, messageId, onClose }) {
|
|
8
|
+
export function MessagingSafety({ client, state, appearance, name, messageId, onClose, onSearch, onDetails, onPins, onSaved, children, menuAnchor }) {
|
|
6
9
|
const [mode, setMode] = useState(messageId ? 'report' : 'menu');
|
|
7
10
|
const [category, setCategory] = useState(''), [details, setDetails] = useState(''), [busy, setBusy] = useState(false), [error, setError] = useState('');
|
|
8
11
|
const reportRequest = useRef(null), reportContent = useRef('');
|
|
9
|
-
const
|
|
12
|
+
const [visible, setVisible] = useState(true), afterClose = useRef(null);
|
|
13
|
+
const dismissed = () => { const next = afterClose.current; afterClose.current = null; next?.(); };
|
|
14
|
+
useEffect(() => { if (!visible && Platform.OS !== 'ios')
|
|
15
|
+
dismissed(); }, [visible]);
|
|
16
|
+
useEffect(() => () => { afterClose.current = null; }, [client]);
|
|
17
|
+
const viewport = useWindowDimensions();
|
|
18
|
+
useEffect(() => { if (!visible || mode !== 'menu' || !menuAnchor || Platform.OS !== 'web')
|
|
19
|
+
return; const close = (event) => { if (event.key === 'Escape') {
|
|
20
|
+
event.stopPropagation();
|
|
21
|
+
onClose();
|
|
22
|
+
} }; document.addEventListener('keydown', close, true); return () => document.removeEventListener('keydown', close, true); }, [visible, mode, menuAnchor, onClose]);
|
|
23
|
+
const search = () => { afterClose.current = onSearch ?? null; setVisible(false); };
|
|
24
|
+
const c = { ...appearance.colors, ...messagingPopupColors }, safety = state.conversation?.safety, blocked = safety?.blockedByMe;
|
|
10
25
|
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'; })();
|
|
11
|
-
const button = (label, press, disabled = false, kind = 'plain') => _jsx(Pressable, { accessibilityRole: "button", 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 }) });
|
|
26
|
+
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 }) });
|
|
27
|
+
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 })] });
|
|
12
28
|
async function block() { setBusy(true); setError(''); try {
|
|
13
29
|
await client.setBlocked(!blocked);
|
|
14
30
|
onClose();
|
|
@@ -42,6 +58,11 @@ export function MessagingSafety({ client, state, appearance, name, messageId, on
|
|
|
42
58
|
setBusy(false);
|
|
43
59
|
}
|
|
44
60
|
}
|
|
45
|
-
|
|
46
|
-
|
|
61
|
+
const choose = (next) => { afterClose.current = () => { setMode(next); setVisible(true); }; setVisible(false); };
|
|
62
|
+
if (mode === 'menu' && menuAnchor) {
|
|
63
|
+
const count = Number(!!onSearch) + Number(!!onDetails) + Number(!!onPins) + Number(!!children) + Number(!!safety?.canBlock) + Number(!!safety?.canReport), height = count * 44 + 8;
|
|
64
|
+
return _jsx(Modal, { visible: visible, transparent: true, animationType: "none", onDismiss: dismissed, onRequestClose: onClose, children: _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: menuAnchor.x, top: Math.max(12, Math.min(menuAnchor.y, viewport.height - height - 12)), width: Math.min(240, viewport.width - 24), maxHeight: viewport.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); }), 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'))] }) })] }) });
|
|
65
|
+
}
|
|
66
|
+
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' ? 'Translation' : mode === 'block' ? `${blocked ? 'Unblock' : 'Block'} ${name}?` : mode === 'sent' ? 'Report sent' : messageId ? 'Report message' : 'Report conversation', onClose: () => { if (!busy)
|
|
67
|
+
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 ? 'You will be able to message and call each other again. Messages attempted while blocked will not be delivered.' : '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 ? 'This message and your reason will be shared with this app’s moderation team.' : 'Up to five recent messages from other people in this conversation and your reason will be shared with this app’s moderation team.', " 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: 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: value })] }, value)), _jsx(TextInput, { accessibilityLabel: "Report details", placeholder: "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: "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')] })] }) });
|
|
47
68
|
}
|
package/dist/saved.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import type { MessagingAppearance, SavedMessage } from '@robono/messaging';
|
|
3
|
+
/** Same app-sized popup and media viewer as the conversation; no server-history copy. */
|
|
4
|
+
export declare function SavedMessagesPopup({ records, appearance, onClose, onOpen, onRemove }: {
|
|
5
|
+
records: SavedMessage[];
|
|
6
|
+
appearance: MessagingAppearance;
|
|
7
|
+
onClose: () => void;
|
|
8
|
+
onOpen: (record: SavedMessage) => Promise<void>;
|
|
9
|
+
onRemove: (key: string) => Promise<void>;
|
|
10
|
+
}): React.JSX.Element;
|
package/dist/saved.js
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { messagingPopupColors } from './popup.js';
|
|
3
|
+
import { useEffect, useRef, useState } from 'react';
|
|
4
|
+
import { Platform, Pressable, ScrollView, Text, View } from 'react-native';
|
|
5
|
+
import { MessagingPopup } from './popup.js';
|
|
6
|
+
import { MessageIcon } from './icons.js';
|
|
7
|
+
/** Same app-sized popup and media viewer as the conversation; no server-history copy. */
|
|
8
|
+
export function SavedMessagesPopup({ records, appearance, onClose, onOpen, onRemove }) {
|
|
9
|
+
const c = { ...appearance.colors, ...messagingPopupColors }, [limit, setLimit] = useState(30), [error, setError] = useState(''), [busy, setBusy] = useState(false), [detail, setDetail] = useState(null);
|
|
10
|
+
const [visible, setVisible] = useState(true), pending = useRef(null), alive = useRef(true);
|
|
11
|
+
useEffect(() => { alive.current = true; return () => { alive.current = false; pending.current = null; }; }, []);
|
|
12
|
+
// iOS presents one controller at a time. Finish dismissing Saved before opening
|
|
13
|
+
// the media controller; otherwise iOS can silently discard the new viewer.
|
|
14
|
+
async function dismissed() {
|
|
15
|
+
const record = pending.current;
|
|
16
|
+
if (!record)
|
|
17
|
+
return;
|
|
18
|
+
pending.current = null;
|
|
19
|
+
try {
|
|
20
|
+
// Let React remove the dismissed native host before mounting the viewer.
|
|
21
|
+
// Presenting it in the dismissal callback can leave controls with no window.
|
|
22
|
+
if (Platform.OS === 'ios')
|
|
23
|
+
await new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(() => resolve())));
|
|
24
|
+
if (!alive.current)
|
|
25
|
+
return;
|
|
26
|
+
await onOpen(record);
|
|
27
|
+
if (alive.current)
|
|
28
|
+
onClose();
|
|
29
|
+
}
|
|
30
|
+
catch (e) {
|
|
31
|
+
if (alive.current) {
|
|
32
|
+
setError(e instanceof Error ? e.message : 'This saved attachment could not be opened.');
|
|
33
|
+
setBusy(false);
|
|
34
|
+
setVisible(true);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
useEffect(() => { if (!visible && Platform.OS !== 'ios')
|
|
39
|
+
void dismissed(); }, [visible]);
|
|
40
|
+
function open(record) { if (record.message.kind === 'text') {
|
|
41
|
+
setDetail(record);
|
|
42
|
+
return;
|
|
43
|
+
} if (busy)
|
|
44
|
+
return; setBusy(true); setError(''); pending.current = record; setVisible(false); }
|
|
45
|
+
return _jsxs(MessagingPopup, { contentWidth: 640, visible: visible, onDismiss: () => void dismissed(), background: c.background, title: detail ? 'Saved message' : 'Saved', foreground: c.foreground, closeLabel: detail ? 'Back to Saved' : 'Close Saved', closeDisabled: busy, headerBack: !!detail, onClose: () => { if (!busy) {
|
|
46
|
+
if (detail)
|
|
47
|
+
setDetail(null);
|
|
48
|
+
else
|
|
49
|
+
onClose();
|
|
50
|
+
} }, children: [!detail && _jsx(Text, { style: { color: c.secondary, fontSize: 13, marginBottom: 12 }, children: "Saved from all chats" }), _jsxs(ScrollView, { style: { flex: 1, minHeight: 0 }, children: [!records.length && _jsx(Text, { style: { color: c.secondary, marginVertical: 16 }, children: "No saved messages yet." }), detail ? _jsxs(_Fragment, { children: [_jsxs(Text, { style: { color: c.secondary, fontSize: 12, marginBottom: 10 }, children: [detail.conversationTitle, " \u00B7 ", new Date(detail.message.createdAt).toLocaleDateString()] }), _jsx(Text, { selectable: true, style: { color: c.foreground, fontSize: 16, lineHeight: 23 }, children: detail.message.text })] }) : records.slice(0, limit).map(record => _jsxs(View, { style: { flexDirection: 'row', alignItems: 'center', borderBottomWidth: 1, borderBottomColor: c.secondary + '20' }, children: [_jsxs(Pressable, { accessibilityRole: "button", accessibilityLabel: 'Open saved message: ' + (record.message.text || record.message.media?.fileName || 'Attachment'), disabled: busy, onPress: () => void open(record), style: { flex: 1, paddingVertical: 14, paddingHorizontal: 8, gap: 4 }, children: [_jsxs(Text, { style: { color: c.secondary, fontSize: 12 }, children: [record.conversationTitle, " \u00B7 ", new Date(record.message.createdAt).toLocaleDateString()] }), _jsx(Text, { numberOfLines: 2, style: { color: c.foreground, fontSize: 15, lineHeight: 20 }, children: record.message.text || record.message.media?.fileName || 'Attachment' }), record.message.media && !!record.message.text && _jsx(Text, { numberOfLines: 1, style: { color: c.secondary, fontSize: 12 }, children: record.message.media.fileName || record.message.kind })] }), _jsx(Pressable, { accessibilityRole: "button", accessibilityLabel: 'Remove saved message: ' + (record.message.text || record.message.media?.fileName || 'Attachment'), disabled: busy, onPress: () => { setBusy(true); setError(''); void onRemove(record.key).catch(e => setError(e instanceof Error ? e.message : 'This saved message could not be removed.')).finally(() => setBusy(false)); }, style: { minWidth: 44, minHeight: 44, alignItems: 'center', justifyContent: 'center' }, children: _jsx(MessageIcon, { name: "trash", color: c.secondary, size: 20 }) })] }, record.key))] }), !detail && records.length > limit && _jsx(Pressable, { accessibilityRole: "button", onPress: () => setLimit(n => n + 30), style: { minHeight: 44, justifyContent: 'center', alignSelf: 'flex-end' }, children: _jsx(Text, { style: { color: c.accent }, children: "More saved messages" }) }), !!error && _jsx(Text, { accessibilityRole: "alert", style: { color: c.foreground, marginTop: 8 }, children: error })] });
|
|
51
|
+
}
|
package/dist/search.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import type { MessagingAppearance, MessagingClient } from '@robono/messaging';
|
|
3
|
+
/** Messenger's in-conversation search: keep the chat visible and move between matches. */
|
|
4
|
+
export declare function MessagingSearch({ client, appearance, onClose }: {
|
|
5
|
+
client: MessagingClient;
|
|
6
|
+
appearance: MessagingAppearance;
|
|
7
|
+
onClose: () => void;
|
|
8
|
+
resolveName?: (id: string) => string;
|
|
9
|
+
}): React.JSX.Element;
|
package/dist/search.js
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useRef, useState } from 'react';
|
|
3
|
+
import { ActivityIndicator, Pressable, Text, TextInput, View } from 'react-native';
|
|
4
|
+
import { MessageIcon } from './icons.js';
|
|
5
|
+
/** Messenger's in-conversation search: keep the chat visible and move between matches. */
|
|
6
|
+
export function MessagingSearch({ client, appearance, onClose }) {
|
|
7
|
+
const c = appearance.colors, [query, setQuery] = useState(''), [messages, setMessages] = useState([]), [index, setIndex] = useState(0), [next, setNext] = useState(null), [busy, setBusy] = useState(false), [error, setError] = useState('');
|
|
8
|
+
const request = useRef(null), generation = useRef(0), timer = useRef(undefined);
|
|
9
|
+
useEffect(() => () => { generation.current++; request.current?.abort(); clearTimeout(timer.current); }, []);
|
|
10
|
+
async function focus(message, id) { try {
|
|
11
|
+
await client.focusMessage(message.id);
|
|
12
|
+
}
|
|
13
|
+
catch (e) {
|
|
14
|
+
if (id === generation.current)
|
|
15
|
+
setError(e instanceof Error ? e.message : 'This message is unavailable.');
|
|
16
|
+
} }
|
|
17
|
+
async function search(more = false) {
|
|
18
|
+
clearTimeout(timer.current);
|
|
19
|
+
const term = query.trim();
|
|
20
|
+
if (!term)
|
|
21
|
+
return;
|
|
22
|
+
request.current?.abort();
|
|
23
|
+
const control = new AbortController();
|
|
24
|
+
request.current = control;
|
|
25
|
+
const id = ++generation.current;
|
|
26
|
+
setBusy(true);
|
|
27
|
+
setError('');
|
|
28
|
+
try {
|
|
29
|
+
const result = await client.search(term, { ...(more && next ? { before: next } : {}), signal: control.signal });
|
|
30
|
+
if (id !== generation.current)
|
|
31
|
+
return;
|
|
32
|
+
const rows = more ? [...messages, ...result.messages.filter(m => !messages.some(old => old.id === m.id))] : result.messages;
|
|
33
|
+
const target = more ? Math.min(index + 1, Math.max(0, rows.length - 1)) : 0;
|
|
34
|
+
setMessages(rows);
|
|
35
|
+
setIndex(target);
|
|
36
|
+
setNext(result.hasMore ? result.nextCursor : null);
|
|
37
|
+
if (rows[target])
|
|
38
|
+
await focus(rows[target], id);
|
|
39
|
+
}
|
|
40
|
+
catch (e) {
|
|
41
|
+
if (id === generation.current && !control.signal.aborted)
|
|
42
|
+
setError(e instanceof Error ? e.message : 'Search could not be completed. Try again.');
|
|
43
|
+
}
|
|
44
|
+
finally {
|
|
45
|
+
if (id === generation.current)
|
|
46
|
+
setBusy(false);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
useEffect(() => { if (query.trim())
|
|
50
|
+
timer.current = setTimeout(() => void search(), 300); return () => clearTimeout(timer.current); }, [query]);
|
|
51
|
+
function change(value) { generation.current++; request.current?.abort(); clearTimeout(timer.current); setBusy(Boolean(value.trim())); setQuery(value); setMessages([]); setIndex(0); setNext(null); setError(''); }
|
|
52
|
+
async function move(direction) { if (busy || !messages.length)
|
|
53
|
+
return; if (direction > 0 && index === messages.length - 1 && next) {
|
|
54
|
+
await search(true);
|
|
55
|
+
return;
|
|
56
|
+
} const target = (index + direction + messages.length) % messages.length; setIndex(target); await focus(messages[target], generation.current); }
|
|
57
|
+
const nav = (label, icon, direction) => _jsx(Pressable, { accessibilityRole: "button", accessibilityLabel: label, disabled: busy || !messages.length, onPress: () => void move(direction), style: { width: 36, height: 44, alignItems: 'center', justifyContent: 'center', opacity: (busy || !messages.length) ? 0.4 : 1 }, children: _jsx(MessageIcon, { name: icon, size: 20, color: c.foreground }) });
|
|
58
|
+
return _jsxs(View, { testID: "conversation-search", style: { backgroundColor: c.background, borderBottomWidth: 1, borderBottomColor: c.foreground + '20' }, children: [_jsxs(View, { style: { flexDirection: 'row', alignItems: 'center', paddingLeft: 10, gap: 4 }, children: [_jsx(MessageIcon, { name: "search", size: 18, color: c.secondary }), _jsx(TextInput, { accessibilityLabel: "Search this chat", placeholder: "Search this chat", value: query, onChangeText: change, onSubmitEditing: () => void search(), autoFocus: true, autoCapitalize: "none", autoCorrect: false, maxLength: 200, returnKeyType: "search", placeholderTextColor: c.secondary, style: { flex: 1, minWidth: 0, minHeight: 44, color: c.foreground, fontSize: 15 } }), busy ? _jsx(ActivityIndicator, { accessibilityLabel: "Searching messages", color: c.accent }) : !!query.trim() && _jsx(Text, { accessibilityLiveRegion: "polite", style: { color: c.secondary, fontSize: 12 }, children: messages.length ? `${index + 1}/${messages.length}${next ? '+' : ''}` : '0' }), nav('Previous search result', 'chevron-up', -1), nav('Next search result', 'chevron-down', 1), _jsx(Pressable, { accessibilityRole: "button", accessibilityLabel: "Close search", onPress: onClose, style: { width: 44, height: 44, alignItems: 'center', justifyContent: 'center' }, children: _jsx(MessageIcon, { name: "close", size: 20, color: c.foreground }) })] }), !!error && _jsx(Text, { accessibilityRole: "alert", style: { color: c.foreground, paddingHorizontal: 12, paddingBottom: 8, fontSize: 13 }, children: error })] });
|
|
59
|
+
}
|
package/dist/social.d.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import React, { type ComponentType, type ReactNode } from 'react';
|
|
2
|
+
import { type MessagingClient, type MessagingMessage } from '@robono/messaging';
|
|
3
|
+
export interface SocialPlayerProps {
|
|
4
|
+
html: string;
|
|
5
|
+
frame: string | null;
|
|
6
|
+
origin: string;
|
|
7
|
+
provider: string;
|
|
8
|
+
onError: () => void;
|
|
9
|
+
onClose: () => void;
|
|
10
|
+
}
|
|
11
|
+
interface SocialMessagePreviewProps {
|
|
12
|
+
message: MessagingMessage;
|
|
13
|
+
client: MessagingClient;
|
|
14
|
+
Player: ComponentType<SocialPlayerProps>;
|
|
15
|
+
origin: string;
|
|
16
|
+
accent: string;
|
|
17
|
+
foreground: string;
|
|
18
|
+
background: string;
|
|
19
|
+
onLongPress?: () => void;
|
|
20
|
+
renderMessage?: (action: ReactNode) => ReactNode;
|
|
21
|
+
}
|
|
22
|
+
export declare const SocialMessagePreview: React.ForwardRefExoticComponent<SocialMessagePreviewProps & React.RefAttributes<{
|
|
23
|
+
open: () => void;
|
|
24
|
+
}>>;
|
|
25
|
+
export {};
|
package/dist/social.js
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { forwardRef, useImperativeHandle, useEffect, useState } from 'react';
|
|
3
|
+
import { ActivityIndicator, AppState, Linking, Pressable, Text, View } from 'react-native';
|
|
4
|
+
import { socialShare } from '@robono/messaging';
|
|
5
|
+
import { MessagingPopup } from './popup.js';
|
|
6
|
+
import { MessageIcon } from './icons.js';
|
|
7
|
+
import { messageLinks } from './message-links.js';
|
|
8
|
+
export const SocialMessagePreview = forwardRef(function SocialMessagePreview({ message, client, Player, origin, accent, foreground, background, onLongPress, renderMessage }, ref) {
|
|
9
|
+
const share = messageLinks(message.text ?? '').map(link => link.url ? socialShare(link.url, origin) : null).find(Boolean);
|
|
10
|
+
const [opened, setOpened] = useState(null);
|
|
11
|
+
useImperativeHandle(ref, () => ({ open: () => { if (share && !message.deleted)
|
|
12
|
+
setOpened({ client, message }); } }), [share, client, message]);
|
|
13
|
+
const open = opened?.client === client && opened.message.id === message.id && opened.message.conversationId === message.conversationId && opened.message.text === message.text && opened.message.revision === message.revision;
|
|
14
|
+
useEffect(() => { const sub = AppState.addEventListener('change', state => { if (state !== 'active')
|
|
15
|
+
setOpened(null); }); return () => sub.remove(); }, []);
|
|
16
|
+
if (!share || message.deleted)
|
|
17
|
+
return null;
|
|
18
|
+
const action = _jsx(Pressable, { testID: "social-preview-action", hitSlop: 6, accessibilityRole: "button", accessibilityLabel: `Play ${share.provider} preview`, onLongPress: onLongPress, onPress: event => { event.stopPropagation(); setOpened({ client, message }); }, style: ({ pressed }) => ({ flexShrink: 0, width: 32, height: 32, borderRadius: 16, borderWidth: 1, borderColor: accent + '70', backgroundColor: accent + (pressed ? '30' : '0d'), flexDirection: 'row', alignItems: 'center', justifyContent: 'center' }), children: _jsx(MessageIcon, { name: "play", color: accent, size: 16 }) });
|
|
19
|
+
return _jsxs(_Fragment, { children: [renderMessage ? renderMessage(action) : action, open && _jsx(SocialPreviewPopup, { message: message, client: client, Player: Player, origin: origin, provider: share.provider, url: share.url, accent: accent, foreground: foreground, background: background, onClose: () => setOpened(null) }, message.id)] });
|
|
20
|
+
});
|
|
21
|
+
function SocialPreviewPopup({ message, client, Player, origin, provider, url, accent, foreground, background, onClose }) {
|
|
22
|
+
const [preview, setPreview] = useState(null), [busy, setBusy] = useState(true), [error, setError] = useState('');
|
|
23
|
+
useEffect(() => { let active = true; void client.socialPreview(message.id, origin).then(result => { if (active) {
|
|
24
|
+
setPreview(result);
|
|
25
|
+
if (!result.html)
|
|
26
|
+
setError('This post cannot be played here. Open the original below.');
|
|
27
|
+
} }).catch(() => { if (active)
|
|
28
|
+
setError('This preview could not be loaded. Open the original below.'); }).finally(() => { if (active)
|
|
29
|
+
setBusy(false); }); return () => { active = false; }; }, [client, message.id, origin]);
|
|
30
|
+
return _jsxs(MessagingPopup, { expanded: true, bottomPadding: 4, background: "#000000", title: provider, foreground: "#ffffff", closeLabel: "Close social preview", onClose: onClose, children: [busy && _jsx(View, { style: { flex: 1, alignItems: 'center', justifyContent: 'center' }, children: _jsx(ActivityIndicator, { accessibilityLabel: "Loading social preview", color: accent }) }), !!preview?.html && _jsx(View, { testID: "social-preview-player", style: { flex: 1, minHeight: 0, overflow: 'hidden', borderRadius: 10 }, children: _jsx(Player, { html: preview.html, frame: preview.frame, origin: origin, provider: provider, onClose: onClose, onError: () => setError('This preview is unavailable. Open the original below.') }) }), !!error && _jsx(Text, { accessibilityRole: "alert", style: { color: '#ffffff', fontSize: 13, paddingVertical: 8 }, children: error }), _jsx(Pressable, { accessibilityRole: "link", accessibilityLabel: `Open ${provider}`, onPress: () => { void Linking.openURL(url).catch(() => setError('This link could not be opened.')); }, style: { minHeight: 44, justifyContent: 'center' }, children: _jsxs(Text, { style: { color: accent, fontWeight: '600' }, children: ["Open ", provider] }) })] });
|
|
31
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { type TranslationPreference, type MessagingClient, type MessagingAppearance } from '@robono/messaging';
|
|
3
|
+
/** The choice belongs to the conversation, never to an individual message. */
|
|
4
|
+
export declare function TranslationSettings({ client, appearance, preference, language, sourceLanguage, voiceAvailable, onSaved }: {
|
|
5
|
+
client: MessagingClient;
|
|
6
|
+
appearance: MessagingAppearance;
|
|
7
|
+
preference?: TranslationPreference;
|
|
8
|
+
language?: string;
|
|
9
|
+
sourceLanguage?: string;
|
|
10
|
+
voiceAvailable: boolean;
|
|
11
|
+
onSaved: () => void;
|
|
12
|
+
}): React.JSX.Element;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { MessageIcon } from './icons.js';
|
|
3
|
+
import { messagingPopupColors } from './popup.js';
|
|
4
|
+
import { useState } from 'react';
|
|
5
|
+
import { Pressable, ScrollView, Text, View } from 'react-native';
|
|
6
|
+
import { normalizeLanguageCode, supportedLanguages, supportedLanguageLabel } from '@robono/messaging';
|
|
7
|
+
/** The choice belongs to the conversation, never to an individual message. */
|
|
8
|
+
export function TranslationSettings({ client, appearance, preference, language, sourceLanguage, voiceAvailable, onSaved }) {
|
|
9
|
+
const [value, setValue] = useState(() => preference ?? { mode: 'off', language: normalizeLanguageCode(language ?? Intl.DateTimeFormat().resolvedOptions().locale) ?? 'en', sourceLanguage: normalizeLanguageCode(sourceLanguage) });
|
|
10
|
+
const [choosing, setChoosing] = useState(null), [busy, setBusy] = useState(false), [error, setError] = useState('');
|
|
11
|
+
const c = { ...appearance.colors, ...messagingPopupColors };
|
|
12
|
+
return _jsxs(View, { style: { gap: 8 }, children: [_jsx(Text, { style: { fontSize: 13, fontWeight: '600', color: c.secondary, paddingHorizontal: 4 }, children: "Translation" }), _jsxs(View, { style: { backgroundColor: '#111111', borderRadius: 12, borderWidth: 1, borderColor: '#ffffff14', padding: 12, gap: 4 }, children: [_jsx(View, { accessibilityRole: "radiogroup", style: { flexDirection: 'row', borderWidth: 1, borderColor: '#ffffff14', borderRadius: 9, overflow: 'hidden', padding: 3, gap: 3 }, children: ['off', 'text', 'voice'].map(mode => _jsx(Pressable, { accessibilityRole: "radio", accessibilityLabel: 'Translation ' + mode, accessibilityState: { checked: value.mode === mode, disabled: busy || mode === 'voice' && !voiceAvailable }, disabled: busy || mode === 'voice' && !voiceAvailable, onPress: () => setValue(v => ({ ...v, mode })), style: { flex: 1, minHeight: 36, borderRadius: 6, alignItems: 'center', justifyContent: 'center', backgroundColor: value.mode === mode ? c.accent + '25' : 'transparent', opacity: mode === 'voice' && !voiceAvailable ? .4 : 1 }, children: _jsx(Text, { style: { color: value.mode === mode ? c.accent : c.secondary, fontSize: 14, fontWeight: '600' }, children: mode === 'off' ? 'Off' : mode === 'text' ? 'Text' : 'Voice' }) }, mode)) }), ['sourceLanguage', 'language'].map(field => _jsxs(View, { children: [_jsxs(Pressable, { accessibilityRole: "button", accessibilityLabel: field === 'language' ? 'Translation language' : 'Incoming language', disabled: busy, onPress: () => setChoosing(choosing === field ? null : field), style: { minHeight: 48, flexDirection: 'row', alignItems: 'center', gap: 10, borderBottomWidth: field === 'sourceLanguage' ? 1 : 0, borderBottomColor: '#ffffff14' }, children: [_jsx(Text, { style: { flex: 1, color: c.secondary, fontSize: 14 }, children: field === 'language' ? 'Translate into' : 'Messages in' }), _jsx(Text, { style: { color: c.accent, fontSize: 15, flexShrink: 1, textAlign: 'right' }, children: value[field] ? supportedLanguageLabel(value[field]) : 'Choose language' }), _jsx(MessageIcon, { name: choosing === field ? 'chevron-up' : 'chevron-down', color: c.secondary, size: 16 })] }), choosing === field && _jsx(ScrollView, { style: { maxHeight: 180 }, keyboardShouldPersistTaps: "handled", children: supportedLanguages.map(item => _jsx(Pressable, { accessibilityRole: "button", accessibilityLabel: 'Choose ' + item.name, onPress: () => { setValue(v => ({ ...v, [field]: item.code })); setChoosing(null); }, style: { minHeight: 40, paddingHorizontal: 8, justifyContent: 'center' }, children: _jsx(Text, { style: { color: c.foreground }, children: item.name }) }, item.code)) })] }, field)), !!error && _jsx(Text, { accessibilityRole: "alert", style: { color: c.foreground }, children: error }), _jsx(Pressable, { accessibilityRole: "button", accessibilityLabel: "Save translation preference", disabled: busy, onPress: () => { if (value.mode !== 'off' && !value.sourceLanguage) {
|
|
13
|
+
setError('Choose the incoming language once for this conversation.');
|
|
14
|
+
return;
|
|
15
|
+
} setError(''); setBusy(true); void client.setTranslationPreference(value).then(onSaved).catch(() => setError('The translation preference could not be saved. Try again.')).finally(() => setBusy(false)); }, style: { minHeight: 40, minWidth: 96, paddingHorizontal: 18, marginTop: 8, alignSelf: 'flex-end', alignItems: 'center', justifyContent: 'center', borderRadius: 9, backgroundColor: c.accent, opacity: busy ? 0.5 : 1 }, children: _jsx(Text, { style: { color: '#111111', fontSize: 14, fontWeight: '600' }, children: busy ? 'Saving…' : 'Save' }) })] })] });
|
|
16
|
+
}
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import { Animated } from 'react-native';
|
|
2
|
-
export declare
|
|
2
|
+
export declare const messageInfoRevealWidth = 176;
|
|
3
|
+
export declare const messageReplyRevealWidth = 104;
|
|
4
|
+
export declare function useQuoteGesture(enabled: boolean, outgoing: boolean, quote: (() => void) | undefined, details?: () => void): {
|
|
3
5
|
offset: number | Animated.Value;
|
|
4
6
|
webRef: import("react").RefObject<HTMLElement | null> | undefined;
|
|
7
|
+
infoLoaded: boolean;
|
|
8
|
+
infoOpacity: number | Animated.AnimatedInterpolation<string | number>;
|
|
5
9
|
revealOpacity: number | Animated.AnimatedInterpolation<string | number>;
|
|
6
10
|
revealWebStyle: {
|
|
7
11
|
transitionProperty: string;
|
|
@@ -1,12 +1,17 @@
|
|
|
1
1
|
import { useEffect, useRef, useState } from 'react';
|
|
2
|
-
import { Animated, PanResponder, Platform } from 'react-native';
|
|
3
|
-
|
|
4
|
-
export
|
|
2
|
+
import { Animated, Easing, I18nManager, PanResponder, Platform } from 'react-native';
|
|
3
|
+
// Same travel, trigger and return timing as Messenger's MessageSwipeReveal.
|
|
4
|
+
export const messageInfoRevealWidth = 176;
|
|
5
|
+
export const messageReplyRevealWidth = 104;
|
|
6
|
+
const threshold = 82;
|
|
7
|
+
export function useQuoteGesture(enabled, outgoing, quote, details) {
|
|
5
8
|
const offset = useRef(new Animated.Value(0)).current;
|
|
6
9
|
const [webOffset, setWebOffset] = useState(0);
|
|
7
10
|
const [webDragging, setWebDragging] = useState(false);
|
|
8
|
-
const
|
|
9
|
-
|
|
11
|
+
const direction = (outgoing !== !!I18nManager.isRTL) ? -1 : 1;
|
|
12
|
+
const [infoLoaded, setInfoLoaded] = useState(false);
|
|
13
|
+
const current = useRef({ enabled, direction, quote, details });
|
|
14
|
+
current.current = { enabled, direction, quote, details };
|
|
10
15
|
const suppressUntil = useRef(0);
|
|
11
16
|
const reset = () => {
|
|
12
17
|
if (Platform.OS === 'web') {
|
|
@@ -14,13 +19,21 @@ export function useQuoteGesture(enabled, outgoing, quote) {
|
|
|
14
19
|
setWebOffset(0);
|
|
15
20
|
}
|
|
16
21
|
else
|
|
17
|
-
Animated.
|
|
22
|
+
Animated.timing(offset, { toValue: 0, duration: 150, easing: Easing.out(Easing.cubic), useNativeDriver: true }).start();
|
|
23
|
+
};
|
|
24
|
+
const accepts = (dx, dy) => {
|
|
25
|
+
const { enabled, direction, quote } = current.current;
|
|
26
|
+
return enabled && Math.abs(dx) >= (Platform.OS === 'ios' ? 10 : 8) &&
|
|
27
|
+
Math.abs(dx) >= Math.abs(dy) * (Platform.OS === 'ios' ? .55 : 1.15) &&
|
|
28
|
+
(dx * direction < 0 || !!quote && dx * direction > 0);
|
|
18
29
|
};
|
|
19
|
-
const accepts = (dx, dy) => current.current.enabled &&
|
|
20
|
-
dx * current.current.direction > 10 && Math.abs(dx) > Math.abs(dy) * 1.5;
|
|
21
30
|
const move = (dx) => {
|
|
22
31
|
suppressUntil.current = Date.now() + 400;
|
|
23
|
-
const
|
|
32
|
+
const info = dx * current.current.direction < 0;
|
|
33
|
+
if (info)
|
|
34
|
+
setInfoLoaded(true);
|
|
35
|
+
const limit = info ? messageInfoRevealWidth : current.current.quote ? messageReplyRevealWidth : 0;
|
|
36
|
+
const value = Math.sign(dx) * Math.min(limit, Math.abs(dx));
|
|
24
37
|
if (Platform.OS === 'web') {
|
|
25
38
|
setWebDragging(true);
|
|
26
39
|
setWebOffset(value);
|
|
@@ -29,20 +42,23 @@ export function useQuoteGesture(enabled, outgoing, quote) {
|
|
|
29
42
|
offset.setValue(value);
|
|
30
43
|
};
|
|
31
44
|
const finish = (dx, cancelled = false) => {
|
|
45
|
+
suppressUntil.current = Date.now() + 400;
|
|
32
46
|
if (!cancelled && current.current.enabled && dx * current.current.direction >= threshold)
|
|
33
|
-
current.current.quote();
|
|
47
|
+
current.current.quote?.();
|
|
48
|
+
else if (!cancelled && current.current.enabled && dx * current.current.direction <= -threshold)
|
|
49
|
+
current.current.details?.();
|
|
34
50
|
reset();
|
|
35
51
|
};
|
|
36
52
|
const responder = useRef(PanResponder.create({
|
|
37
53
|
onStartShouldSetPanResponder: () => { suppressUntil.current = 0; return false; },
|
|
38
54
|
onStartShouldSetPanResponderCapture: () => { suppressUntil.current = 0; return false; },
|
|
39
|
-
onMoveShouldSetPanResponderCapture: (_, gesture) => gesture.numberActiveTouches === 1 && accepts(gesture.dx, gesture.dy),
|
|
55
|
+
onMoveShouldSetPanResponderCapture: (_, gesture) => Platform.OS === 'ios' && gesture.numberActiveTouches === 1 && accepts(gesture.dx, gesture.dy),
|
|
40
56
|
onMoveShouldSetPanResponder: (_, gesture) => accepts(gesture.dx, gesture.dy),
|
|
41
57
|
onPanResponderGrant: () => offset.stopAnimation(),
|
|
42
58
|
onPanResponderMove: (_, gesture) => move(gesture.dx),
|
|
43
59
|
onPanResponderRelease: (_, gesture) => finish(gesture.dx),
|
|
44
60
|
onPanResponderTerminate: () => finish(0, true),
|
|
45
|
-
onPanResponderTerminationRequest: () =>
|
|
61
|
+
onPanResponderTerminationRequest: () => Platform.OS !== 'ios',
|
|
46
62
|
})).current;
|
|
47
63
|
const pointer = useRef(null);
|
|
48
64
|
const webHandlers = {
|
|
@@ -67,7 +83,7 @@ export function useQuoteGesture(enabled, outgoing, quote) {
|
|
|
67
83
|
event.preventDefault();
|
|
68
84
|
move(dx);
|
|
69
85
|
}
|
|
70
|
-
else if (Math.abs(dy) > 10 || dx * current.current.direction
|
|
86
|
+
else if (Math.abs(dy) > 10 || !current.current.quote && dx * current.current.direction > 10)
|
|
71
87
|
pointer.current = null;
|
|
72
88
|
},
|
|
73
89
|
onPointerUp: (event) => {
|
|
@@ -118,10 +134,12 @@ export function useQuoteGesture(enabled, outgoing, quote) {
|
|
|
118
134
|
}, []);
|
|
119
135
|
return { offset: Platform.OS === 'web' ? webOffset : offset,
|
|
120
136
|
webRef: Platform.OS === 'web' ? webRef : undefined,
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
137
|
+
infoLoaded,
|
|
138
|
+
infoOpacity: Platform.OS === 'web' ? Math.min(1, Math.max(0, -webOffset * direction) / 24) : offset.interpolate({ inputRange: direction === 1 ? [-24, -8, 0] : [0, 8, 24], outputRange: direction === 1 ? [1, .35, 0] : [0, .35, 1], extrapolate: 'clamp' }),
|
|
139
|
+
revealOpacity: Platform.OS === 'web' ? Math.min(1, Math.max(0, webOffset * direction) / messageReplyRevealWidth)
|
|
140
|
+
: offset.interpolate({ inputRange: direction === -1 ? [-messageReplyRevealWidth, -18, 0] : [0, 18, messageReplyRevealWidth], outputRange: direction === -1 ? [1, .35, 0] : [0, .35, 1], extrapolate: 'clamp' }),
|
|
141
|
+
revealWebStyle: Platform.OS === 'web' ? { transitionProperty: 'opacity', transitionDuration: webDragging ? '0ms' : '150ms' } : {},
|
|
142
|
+
webStyle: Platform.OS === 'web' ? { transitionProperty: 'transform', transitionDuration: webDragging ? '0ms' : '150ms', transitionTimingFunction: 'cubic-bezier(0.215,0.61,0.355,1)' } : {},
|
|
125
143
|
handlers: Platform.OS === 'web' ? {} : responder.panHandlers,
|
|
126
144
|
suppressPress: () => Date.now() < suppressUntil.current };
|
|
127
145
|
}
|