@robono/messaging-react-native 0.2.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 +51 -0
- package/README.md +84 -7
- package/SBOM.spdx.json +1295 -239
- 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-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 +46 -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.js +2 -1
- package/dist/expo-privacy.d.ts +4 -0
- package/dist/expo-privacy.js +7 -0
- package/dist/expo-screen.d.ts +42 -0
- package/dist/expo-screen.js +557 -0
- package/dist/expo.d.ts +2 -42
- package/dist/expo.js +2 -549
- 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.js +4 -3
- 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/inbox-search.d.ts +11 -0
- package/dist/inbox-search.js +42 -0
- package/dist/inbox.d.ts +5 -2
- package/dist/inbox.js +19 -15
- package/dist/index.d.ts +33 -3
- package/dist/index.js +347 -129
- package/dist/media-controller.d.ts +3 -0
- package/dist/media-controller.js +39 -0
- package/dist/media-ui.d.ts +14 -3
- package/dist/media-ui.js +90 -28
- package/dist/message-menu.d.ts +28 -0
- package/dist/message-menu.js +24 -0
- package/dist/message-order.d.ts +3 -0
- package/dist/message-order.js +10 -0
- package/dist/messenger-video-view.d.ts +8 -0
- package/dist/messenger-video-view.js +46 -0
- package/dist/native-outbox.d.ts +8 -2
- package/dist/native-outbox.js +3 -1
- package/dist/photo-gesture.d.ts +20 -0
- package/dist/photo-gesture.js +52 -0
- package/dist/polls.d.ts +2 -1
- package/dist/polls.js +19 -17
- package/dist/popup.d.ts +43 -1
- package/dist/popup.js +72 -6
- package/dist/quick-send.d.ts +15 -0
- package/dist/quick-send.js +26 -0
- package/dist/safety.d.ts +9 -1
- package/dist/safety.js +21 -6
- package/dist/saved.d.ts +1 -1
- package/dist/saved.js +9 -4
- package/dist/search.d.ts +2 -2
- package/dist/search.js +32 -14
- package/dist/social.d.ts +9 -3
- package/dist/social.js +11 -15
- 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/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-waveform.d.ts +2 -0
- package/dist/voice-waveform.js +21 -0
- package/package.json +24 -4
package/dist/polls.js
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { messagingPopupColors } from './popup.js';
|
|
3
|
+
import { ExtraPopup } from './extras.js';
|
|
4
|
+
import { MessageIcon } from './icons.js';
|
|
2
5
|
import { useRef, useState } from 'react';
|
|
3
6
|
import { createPollVoteQueue } from './pollVoteQueue';
|
|
4
|
-
import { ActivityIndicator,
|
|
7
|
+
import { ActivityIndicator, Pressable, ScrollView, Switch, Text, TextInput, View } from 'react-native';
|
|
5
8
|
import { validatePoll } from '@robono/messaging';
|
|
6
9
|
const PollArea = View;
|
|
7
10
|
import { MessagingPopup as PollModal } from './popup.js';
|
|
8
11
|
/** Optional reusable poll content. Votes are submitted as a complete selection, not a toggle request. */
|
|
9
|
-
export function PollMessage({ message, userId, ink, accent, enabled, onVote, onClose, resolveVoterName, background = '#101010', foreground = '#fff', renderDetails, detailsOpen, onDetailsOpenChange }) {
|
|
12
|
+
export function PollMessage({ message, userId, ink, accent, enabled, onVote, onClose, resolveVoterName, background = '#101010', foreground = '#fff', renderDetails, detailsOpen, onDetailsOpenChange, onLongPress }) {
|
|
10
13
|
const [busy, setBusy] = useState(false), [error, setError] = useState(''), [localShowVotes, setLocalShowVotes] = useState(false);
|
|
11
14
|
const showVotes = detailsOpen ?? localShowVotes;
|
|
12
15
|
const setShowVotes = (open) => { setLocalShowVotes(open); onDetailsOpenChange?.(open); };
|
|
@@ -23,7 +26,7 @@ export function PollMessage({ message, userId, ink, accent, enabled, onVote, onC
|
|
|
23
26
|
if (!poll)
|
|
24
27
|
return null;
|
|
25
28
|
if (poll.kind === 'yes_no')
|
|
26
|
-
return _jsx(YesNoQuestion, { message, userId, ink, accent, enabled, onVote, onClose, resolveVoterName, renderDetails, detailsOpen, onDetailsOpenChange, background, foreground });
|
|
29
|
+
return _jsx(YesNoQuestion, { message, userId, ink, accent, enabled, onVote, onClose, resolveVoterName, renderDetails, detailsOpen, onDetailsOpenChange, onLongPress, background, foreground });
|
|
27
30
|
const savedVotes = message.pollState?.votes ?? [], selected = pending ?? savedVotes.find(v => v.userId === userId)?.options ?? [], closed = !!message.pollState?.closed;
|
|
28
31
|
selection.current = selected;
|
|
29
32
|
const votes = pending === null ? savedVotes : [...savedVotes.filter(v => v.userId !== userId), ...(pending.length ? [{ userId, options: pending }] : [])];
|
|
@@ -38,20 +41,20 @@ export function PollMessage({ message, userId, ink, accent, enabled, onVote, onC
|
|
|
38
41
|
actionBusy.current = false;
|
|
39
42
|
setBusy(false);
|
|
40
43
|
} }
|
|
41
|
-
const details = _jsxs(View, { style: { gap: 14 }, children: [_jsxs(View, { style: { gap: 4 }, children: [_jsx(Text, { style: { fontSize: 17, lineHeight: 23, fontWeight: '600', color: foreground }, children: poll.question }), _jsxs(Text, { style: { fontSize: 12, lineHeight: 17, color: foreground, opacity: .65 }, children: [votes.length, " ", votes.length === 1 ? 'person voted' : 'people voted', " \u00B7 ", closed ? 'Closed' : poll.multiple ? 'Multiple answers' : 'One answer per person'] })] }), _jsx(View, { style: { gap: 14 }, children: poll.options.map((option, index) => {
|
|
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 ? 'person voted' : 'people voted', " \u00B7 ", closed ? 'Closed' : poll.multiple ? 'Multiple answers' : 'One answer per person'] })] }), _jsx(View, { style: { gap: 14 }, children: poll.options.map((option, index) => {
|
|
42
45
|
const people = votes.filter(v => v.options.includes(index));
|
|
43
46
|
const names = people.map(v => v.userId === userId ? 'You' : resolveVoterName?.(v.userId) || 'Chat member').join(', ');
|
|
44
|
-
return _jsxs(View, { style: { gap: 5 }, children: [_jsxs(View, { style: { flexDirection: 'row', alignItems: 'baseline', gap: 12 }, children: [_jsx(Text, { style: { flex: 1, color: foreground, fontSize: 15, lineHeight: 21, fontWeight: '600' }, children: option }), _jsx(Text, { style: { color: 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: foreground, opacity: .7, fontSize: 13, lineHeight: 18 }, children: names })] }, index);
|
|
45
|
-
}) }), 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: foreground, fontSize: 13, lineHeight: 18 }, children: error })] });
|
|
46
|
-
return _jsxs(_Fragment, { children: [_jsxs(Pressable, { accessible: false, onPress: event => { event.stopPropagation(); setShowVotes(true); }, accessibilityLabel: `Poll: ${poll.question}. Open votes`, style: { width: 270, maxWidth: '100%', gap: 0, padding: 0 }, children: [_jsxs(Text, { accessibilityRole: "button", onPress: () => setShowVotes(true), style: { fontSize: 17, lineHeight: 23, fontWeight: '600', color: ink }, accessibilityHint: closed ? 'Poll closed' : poll.multiple ? 'Choose one or more answers' : 'Choose one answer', children: [poll.question, closed ? ' · Closed' : ''] }), poll.options.map((label, index) => {
|
|
47
|
+
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: `Poll: ${poll.question}. Open votes`, 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 ? 'Poll closed' : poll.multiple ? 'Choose one or more answers' : 'Choose one answer', children: [poll.question, closed ? ' · Closed' : ''] }), poll.options.map((label, index) => {
|
|
47
50
|
const count = votes.filter(v => v.options.includes(index)).length, checked = selected.includes(index);
|
|
48
51
|
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)
|
|
49
52
|
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);
|
|
50
|
-
}), !!error && _jsx(Text, { accessibilityRole: "alert", style: { color: ink }, children: error })] }), showVotes && (renderDetails ? renderDetails(details, () => setShowVotes(false)) : _jsx(
|
|
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 }) }))] });
|
|
51
54
|
}
|
|
52
55
|
/** Recipient-only decision. Keep the attempted answer on an uncertain failure so
|
|
53
56
|
* retry cannot silently switch an answer the server may already have accepted. */
|
|
54
|
-
function YesNoQuestion({ message, userId, ink, accent, enabled, onVote, onClose, resolveVoterName, renderDetails, detailsOpen, onDetailsOpenChange, background = '#111111', foreground = '#fff' }) {
|
|
57
|
+
function YesNoQuestion({ message, userId, ink, accent, enabled, onVote, onClose, resolveVoterName, renderDetails, detailsOpen, onDetailsOpenChange, onLongPress, background = '#111111', foreground = '#fff' }) {
|
|
55
58
|
const [attempt, setAttempt] = useState(null), [busy, setBusy] = useState(false), [error, setError] = useState(''), [localOpen, setLocalOpen] = useState(false);
|
|
56
59
|
const inFlight = useRef(false), firstChoice = useRef(null);
|
|
57
60
|
const poll = message.poll, answer = message.pollState?.votes.find(v => v.userId !== message.senderId)?.options[0];
|
|
@@ -81,14 +84,14 @@ function YesNoQuestion({ message, userId, ink, accent, enabled, onVote, onClose,
|
|
|
81
84
|
}
|
|
82
85
|
}
|
|
83
86
|
const respondent = message.pollState?.votes.find(v => v.userId !== message.senderId)?.userId;
|
|
84
|
-
const details = _jsxs(View, { style: { gap: 12 }, children: [_jsx(Text, { style: { color: foreground, fontSize: 17, lineHeight: 23, fontWeight: '600' }, children: poll.question }), _jsx(Text, { style: { color: foreground, fontSize: 15, lineHeight: 21 }, children: answer !== undefined ? `${respondent === userId ? 'You' : resolveVoterName?.(respondent) || 'Recipient'} answered ${poll.options[answer]}.` : closed ? 'Closed without an answer.' : attempt !== null ? 'Confirming your answer…' : 'Waiting for an answer.' }), mine && !closed && enabled && _jsx(Pressable, { accessibilityRole: "button", onPress: () => { if (inFlight.current)
|
|
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 ? `${respondent === userId ? 'You' : resolveVoterName?.(respondent) || 'Recipient'} answered ${poll.options[answer]}.` : closed ? 'Closed without an answer.' : attempt !== null ? 'Confirming your answer…' : 'Waiting for an answer.' }), mine && !closed && enabled && _jsx(Pressable, { accessibilityRole: "button", onPress: () => { if (inFlight.current)
|
|
85
88
|
return; inFlight.current = true; void onClose().catch(() => setError('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: "End question" }) })] });
|
|
86
|
-
return _jsxs(_Fragment, { children: [_jsxs(Pressable, { accessible: false, onPress: e => { e.stopPropagation(); setOpen(true); }, style: { maxWidth: '
|
|
87
|
-
void choose(attempt); }, children: _jsx(Text, { accessibilityRole: "alert", style: { color: ink, fontSize: 13 }, children: error }) })] }), open && (renderDetails ? renderDetails(details, () => setOpen(false)) :
|
|
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 }) }))] });
|
|
88
91
|
}
|
|
89
92
|
export function PollComposer({ appearance, onSend, onCancel, retrySafe = false, yesNo = false }) {
|
|
90
93
|
const [question, setQuestion] = useState(''), [options, setOptions] = useState(['', '']), [multiple, setMultiple] = useState(false), [busy, setBusy] = useState(false), [error, setError] = useState(''), [submitted, setSubmitted] = useState(false);
|
|
91
|
-
const c = appearance.colors;
|
|
94
|
+
const c = { ...appearance.colors, ...messagingPopupColors };
|
|
92
95
|
async function send() { if (busy || submitted)
|
|
93
96
|
return; let poll; try {
|
|
94
97
|
poll = validatePoll(yesNo ? { question, options: ['Yes', 'No'], multiple: false, kind: 'yes_no' } : { question, options, multiple });
|
|
@@ -108,14 +111,13 @@ export function PollComposer({ appearance, onSend, onCancel, retrySafe = false,
|
|
|
108
111
|
finally {
|
|
109
112
|
setBusy(false);
|
|
110
113
|
} }
|
|
111
|
-
const inputStyle = { fontSize: 17, color: c.foreground, minHeight: 48,
|
|
112
|
-
const web = true;
|
|
114
|
+
const inputStyle = { fontSize: 17, color: c.foreground, minHeight: 48, borderWidth: 1, borderColor: '#ffffff20', borderRadius: 10, backgroundColor: '#111111', padding: 12 };
|
|
113
115
|
let ready = false;
|
|
114
116
|
try {
|
|
115
117
|
validatePoll(yesNo ? { question, options: ['Yes', 'No'], multiple: false, kind: 'yes_no' } : { question, options, multiple });
|
|
116
118
|
ready = true;
|
|
117
119
|
}
|
|
118
120
|
catch { }
|
|
119
|
-
return _jsx(PollModal, { form: true, background: c.background, onClose: () => { if (!busy)
|
|
120
|
-
onCancel(); }, children: _jsxs(PollArea, { style: {
|
|
121
|
+
return _jsx(PollModal, { contentWidth: 640, form: true, title: yesNo ? 'Yes/No question' : 'New poll', foreground: c.foreground, closeDisabled: busy, background: c.background, onClose: () => { if (!busy)
|
|
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: `Option ${i + 1}`, placeholder: "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: `Remove option ${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: "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: "Allow multiple answers" }), _jsx(Switch, { accessibilityLabel: "Allow multiple answers", disabled: busy, value: multiple, onValueChange: setMultiple, trackColor: { true: c.accent } })] }), _jsx(Text, { style: { color: c.secondary, fontSize: 13, lineHeight: 19 }, children: "Everyone in the chat can see the votes." })] }), yesNo && _jsx(Text, { style: { color: c.secondary, fontSize: 13, lineHeight: 19, marginTop: 8 }, children: "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: "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: "Cancel" }) }), _jsx(Pressable, { accessibilityRole: "button", accessibilityLabel: yesNo ? "Send question" : "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: "Send" }) })] })] }) });
|
|
121
123
|
}
|
package/dist/popup.d.ts
CHANGED
|
@@ -1,9 +1,51 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
export
|
|
2
|
+
export interface MessagingPopupSurfaceProps {
|
|
3
|
+
children: React.ReactNode;
|
|
4
|
+
visible: boolean;
|
|
5
|
+
onDismiss?: () => void;
|
|
6
|
+
onClose: () => void;
|
|
7
|
+
title?: string;
|
|
8
|
+
}
|
|
9
|
+
/** Browser hosts can keep settings dialogs inside their embedded chat. */
|
|
10
|
+
export declare const MessagingPopupSurface: React.Context<React.ComponentType<MessagingPopupSurfaceProps> | null>;
|
|
11
|
+
export declare const messagingPopupBackdrop = "#000000d9";
|
|
12
|
+
export declare const messagingPopupColors: {
|
|
13
|
+
background: string;
|
|
14
|
+
foreground: string;
|
|
15
|
+
secondary: string;
|
|
16
|
+
incoming: string;
|
|
17
|
+
};
|
|
18
|
+
/** One title/close row for forms, menus and expanded content. */
|
|
19
|
+
export declare function MessagingPopupHeader({ title, foreground, onClose, closeLabel, disabled, back }: {
|
|
20
|
+
title: string;
|
|
21
|
+
foreground: string;
|
|
22
|
+
onClose: () => void;
|
|
23
|
+
closeLabel?: string;
|
|
24
|
+
disabled?: boolean;
|
|
25
|
+
back?: boolean;
|
|
26
|
+
}): React.JSX.Element;
|
|
27
|
+
export declare function MessagingPopupHost({ children }: {
|
|
28
|
+
children: React.ReactNode;
|
|
29
|
+
}): React.JSX.Element;
|
|
30
|
+
export declare function MessagingPopupHeaderBoundary({ children }: {
|
|
31
|
+
children: React.ReactNode;
|
|
32
|
+
}): React.JSX.Element;
|
|
33
|
+
/** Content panels occupy the area below the persistent header; confirmations stay compact. */
|
|
34
|
+
export declare function MessagingPopup({ children, onClose, background, form, visible, onDismiss, expanded, bottomPadding, title, foreground, closeLabel, closeDisabled, contentWidth, headerBack, withinChat, compact }: {
|
|
35
|
+
compact?: boolean;
|
|
36
|
+
withinChat?: boolean;
|
|
3
37
|
children: React.ReactNode;
|
|
4
38
|
onClose: () => void;
|
|
5
39
|
background: string;
|
|
6
40
|
form?: boolean;
|
|
7
41
|
visible?: boolean;
|
|
8
42
|
onDismiss?: () => void;
|
|
43
|
+
expanded?: boolean;
|
|
44
|
+
bottomPadding?: number;
|
|
45
|
+
title?: string;
|
|
46
|
+
foreground?: string;
|
|
47
|
+
closeLabel?: string;
|
|
48
|
+
closeDisabled?: boolean;
|
|
49
|
+
contentWidth?: number;
|
|
50
|
+
headerBack?: boolean;
|
|
9
51
|
}): React.JSX.Element;
|
package/dist/popup.js
CHANGED
|
@@ -1,7 +1,73 @@
|
|
|
1
|
-
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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 });
|
|
7
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/safety.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
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, onSearch }: {
|
|
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;
|
|
@@ -9,4 +9,12 @@ export declare function MessagingSafety({ client, state, appearance, name, messa
|
|
|
9
9
|
messageId?: string;
|
|
10
10
|
onClose: () => void;
|
|
11
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
|
+
};
|
|
12
20
|
}): React.JSX.Element;
|
package/dist/safety.js
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
|
-
import { jsx as _jsx,
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { messagingPopupColors } from './popup.js';
|
|
2
3
|
import { useEffect, useRef, useState } from 'react';
|
|
3
|
-
import {
|
|
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, onSearch }) {
|
|
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('');
|
|
@@ -11,10 +14,17 @@ export function MessagingSafety({ client, state, appearance, name, messageId, on
|
|
|
11
14
|
useEffect(() => { if (!visible && Platform.OS !== 'ios')
|
|
12
15
|
dismissed(); }, [visible]);
|
|
13
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]);
|
|
14
23
|
const search = () => { afterClose.current = onSearch ?? null; setVisible(false); };
|
|
15
|
-
const c = appearance.colors, safety = state.conversation?.safety, blocked = safety?.blockedByMe;
|
|
24
|
+
const c = { ...appearance.colors, ...messagingPopupColors }, safety = state.conversation?.safety, blocked = safety?.blockedByMe;
|
|
16
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'; })();
|
|
17
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 })] });
|
|
18
28
|
async function block() { setBusy(true); setError(''); try {
|
|
19
29
|
await client.setBlocked(!blocked);
|
|
20
30
|
onClose();
|
|
@@ -48,6 +58,11 @@ export function MessagingSafety({ client, state, appearance, name, messageId, on
|
|
|
48
58
|
setBusy(false);
|
|
49
59
|
}
|
|
50
60
|
}
|
|
51
|
-
|
|
52
|
-
|
|
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')] })] }) });
|
|
53
68
|
}
|
package/dist/saved.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import type { MessagingAppearance, SavedMessage } from '@robono/messaging';
|
|
3
|
-
/** Same
|
|
3
|
+
/** Same app-sized popup and media viewer as the conversation; no server-history copy. */
|
|
4
4
|
export declare function SavedMessagesPopup({ records, appearance, onClose, onOpen, onRemove }: {
|
|
5
5
|
records: SavedMessage[];
|
|
6
6
|
appearance: MessagingAppearance;
|
package/dist/saved.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { messagingPopupColors } from './popup.js';
|
|
2
3
|
import { useEffect, useRef, useState } from 'react';
|
|
3
4
|
import { Platform, Pressable, ScrollView, Text, View } from 'react-native';
|
|
4
5
|
import { MessagingPopup } from './popup.js';
|
|
5
6
|
import { MessageIcon } from './icons.js';
|
|
6
|
-
/** Same
|
|
7
|
+
/** Same app-sized popup and media viewer as the conversation; no server-history copy. */
|
|
7
8
|
export function SavedMessagesPopup({ records, appearance, onClose, onOpen, onRemove }) {
|
|
8
|
-
const c = appearance.colors, [limit, setLimit] = useState(30), [error, setError] = useState(''), [busy, setBusy] = useState(false), [detail, setDetail] = useState(null);
|
|
9
|
+
const c = { ...appearance.colors, ...messagingPopupColors }, [limit, setLimit] = useState(30), [error, setError] = useState(''), [busy, setBusy] = useState(false), [detail, setDetail] = useState(null);
|
|
9
10
|
const [visible, setVisible] = useState(true), pending = useRef(null), alive = useRef(true);
|
|
10
11
|
useEffect(() => { alive.current = true; return () => { alive.current = false; pending.current = null; }; }, []);
|
|
11
12
|
// iOS presents one controller at a time. Finish dismissing Saved before opening
|
|
@@ -41,6 +42,10 @@ export function SavedMessagesPopup({ records, appearance, onClose, onOpen, onRem
|
|
|
41
42
|
return;
|
|
42
43
|
} if (busy)
|
|
43
44
|
return; setBusy(true); setError(''); pending.current = record; setVisible(false); }
|
|
44
|
-
return _jsxs(MessagingPopup, { visible: visible, onDismiss: () => void dismissed(), background: c.background, onClose: () => { if (!busy)
|
|
45
|
-
|
|
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 })] });
|
|
46
51
|
}
|
package/dist/search.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import type { MessagingAppearance, MessagingClient } from '@robono/messaging';
|
|
3
|
-
/**
|
|
4
|
-
export declare function MessagingSearch({ client, appearance, onClose
|
|
3
|
+
/** Messenger's in-conversation search: keep the chat visible and move between matches. */
|
|
4
|
+
export declare function MessagingSearch({ client, appearance, onClose }: {
|
|
5
5
|
client: MessagingClient;
|
|
6
6
|
appearance: MessagingAppearance;
|
|
7
7
|
onClose: () => void;
|
package/dist/search.js
CHANGED
|
@@ -1,13 +1,22 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useEffect, useRef, useState } from 'react';
|
|
3
|
-
import { ActivityIndicator,
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
const
|
|
8
|
-
|
|
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
|
+
} }
|
|
9
17
|
async function search(more = false) {
|
|
10
|
-
|
|
18
|
+
clearTimeout(timer.current);
|
|
19
|
+
const term = query.trim();
|
|
11
20
|
if (!term)
|
|
12
21
|
return;
|
|
13
22
|
request.current?.abort();
|
|
@@ -16,17 +25,17 @@ export function MessagingSearch({ client, appearance, onClose, resolveName }) {
|
|
|
16
25
|
const id = ++generation.current;
|
|
17
26
|
setBusy(true);
|
|
18
27
|
setError('');
|
|
19
|
-
if (!more) {
|
|
20
|
-
setMessages([]);
|
|
21
|
-
setNext(null);
|
|
22
|
-
setSearched(term);
|
|
23
|
-
}
|
|
24
28
|
try {
|
|
25
29
|
const result = await client.search(term, { ...(more && next ? { before: next } : {}), signal: control.signal });
|
|
26
30
|
if (id !== generation.current)
|
|
27
31
|
return;
|
|
28
|
-
|
|
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);
|
|
29
36
|
setNext(result.hasMore ? result.nextCursor : null);
|
|
37
|
+
if (rows[target])
|
|
38
|
+
await focus(rows[target], id);
|
|
30
39
|
}
|
|
31
40
|
catch (e) {
|
|
32
41
|
if (id === generation.current && !control.signal.aborted)
|
|
@@ -37,5 +46,14 @@ export function MessagingSearch({ client, appearance, onClose, resolveName }) {
|
|
|
37
46
|
setBusy(false);
|
|
38
47
|
}
|
|
39
48
|
}
|
|
40
|
-
|
|
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 })] });
|
|
41
59
|
}
|
package/dist/social.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React, { type ComponentType } from 'react';
|
|
1
|
+
import React, { type ComponentType, type ReactNode } from 'react';
|
|
2
2
|
import { type MessagingClient, type MessagingMessage } from '@robono/messaging';
|
|
3
3
|
export interface SocialPlayerProps {
|
|
4
4
|
html: string;
|
|
@@ -8,7 +8,7 @@ export interface SocialPlayerProps {
|
|
|
8
8
|
onError: () => void;
|
|
9
9
|
onClose: () => void;
|
|
10
10
|
}
|
|
11
|
-
|
|
11
|
+
interface SocialMessagePreviewProps {
|
|
12
12
|
message: MessagingMessage;
|
|
13
13
|
client: MessagingClient;
|
|
14
14
|
Player: ComponentType<SocialPlayerProps>;
|
|
@@ -16,4 +16,10 @@ export declare function SocialMessagePreview({ message, client, Player, origin,
|
|
|
16
16
|
accent: string;
|
|
17
17
|
foreground: string;
|
|
18
18
|
background: string;
|
|
19
|
-
|
|
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 {};
|