@robono/messaging-react-native 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/README.md +79 -4
  3. package/SBOM.spdx.json +832 -112
  4. package/dist/calling.js +2 -2
  5. package/dist/expo-download.d.ts +3 -0
  6. package/dist/expo-download.js +43 -0
  7. package/dist/expo-media.js +23 -0
  8. package/dist/expo-outbox.d.ts +13 -0
  9. package/dist/expo-outbox.js +81 -0
  10. package/dist/expo-saved.d.ts +24 -0
  11. package/dist/expo-saved.js +76 -0
  12. package/dist/expo-social.d.ts +3 -0
  13. package/dist/expo-social.js +21 -0
  14. package/dist/expo-thumbnails.d.ts +9 -0
  15. package/dist/expo-thumbnails.js +68 -0
  16. package/dist/expo.d.ts +9 -3
  17. package/dist/expo.js +143 -23
  18. package/dist/forward.d.ts +13 -0
  19. package/dist/forward.js +61 -0
  20. package/dist/icons.d.ts +1 -1
  21. package/dist/inbox.d.ts +16 -0
  22. package/dist/inbox.js +48 -0
  23. package/dist/index.d.ts +45 -2
  24. package/dist/index.js +376 -113
  25. package/dist/media-controller.d.ts +18 -0
  26. package/dist/media-controller.js +118 -3
  27. package/dist/media-readiness.d.ts +1 -1
  28. package/dist/media-ui.d.ts +16 -5
  29. package/dist/media-ui.js +15 -11
  30. package/dist/message-links.d.ts +5 -0
  31. package/dist/message-links.js +30 -0
  32. package/dist/message-order.d.ts +4 -0
  33. package/dist/message-order.js +17 -0
  34. package/dist/native-outbox.d.ts +49 -0
  35. package/dist/native-outbox.js +51 -0
  36. package/dist/pollVoteQueue.d.ts +3 -0
  37. package/dist/pollVoteQueue.js +29 -0
  38. package/dist/polls.d.ts +6 -2
  39. package/dist/polls.js +85 -18
  40. package/dist/popup.d.ts +9 -0
  41. package/dist/popup.js +7 -0
  42. package/dist/recent-media.js +1 -0
  43. package/dist/safety.d.ts +2 -1
  44. package/dist/safety.js +11 -5
  45. package/dist/saved.d.ts +10 -0
  46. package/dist/saved.js +46 -0
  47. package/dist/search.d.ts +9 -0
  48. package/dist/search.js +41 -0
  49. package/dist/social.d.ts +19 -0
  50. package/dist/social.js +35 -0
  51. package/dist/visual/RobonoIcon.js +2 -1
  52. package/dist/voice-composer.js +2 -2
  53. package/package.json +20 -4
package/dist/polls.js CHANGED
@@ -1,36 +1,97 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useState } from 'react';
3
- import { ActivityIndicator, KeyboardAvoidingView, Modal, Platform, Pressable, ScrollView, Switch, Text, TextInput, View } from 'react-native';
4
- import { SafeAreaProvider, SafeAreaView } from 'react-native-safe-area-context';
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { useRef, useState } from 'react';
3
+ import { createPollVoteQueue } from './pollVoteQueue';
4
+ import { ActivityIndicator, KeyboardAvoidingView, Platform, Pressable, ScrollView, Switch, Text, TextInput, View } from 'react-native';
5
5
  import { validatePoll } from '@robono/messaging';
6
+ const PollArea = View;
7
+ import { MessagingPopup as PollModal } from './popup.js';
6
8
  /** Optional reusable poll content. Votes are submitted as a complete selection, not a toggle request. */
7
- export function PollMessage({ message, userId, ink, accent, enabled, onVote, onClose, resolveVoterName, background = '#101010', foreground = '#fff' }) {
8
- const [busy, setBusy] = useState(false), [error, setError] = useState(''), [showVotes, setShowVotes] = useState(false);
9
+ export function PollMessage({ message, userId, ink, accent, enabled, onVote, onClose, resolveVoterName, background = '#101010', foreground = '#fff', renderDetails, detailsOpen, onDetailsOpenChange }) {
10
+ const [busy, setBusy] = useState(false), [error, setError] = useState(''), [localShowVotes, setLocalShowVotes] = useState(false);
11
+ const showVotes = detailsOpen ?? localShowVotes;
12
+ const setShowVotes = (open) => { setLocalShowVotes(open); onDetailsOpenChange?.(open); };
13
+ const [pending, setPending] = useState(null);
14
+ const current = useRef({ onVote });
15
+ current.current = { onVote };
16
+ const selection = useRef([]);
17
+ const queue = useRef(null);
18
+ if (!queue.current)
19
+ queue.current = createPollVoteQueue(choices => current.current.onVote(choices), choices => { if (choices)
20
+ selection.current = choices; setPending(choices); }, () => setError('The poll could not be updated. Try again.'));
21
+ const actionBusy = useRef(false);
9
22
  const poll = message.poll;
10
23
  if (!poll)
11
24
  return null;
12
- const votes = message.pollState?.votes ?? [], selected = votes.find(v => v.userId === userId)?.options ?? [], closed = !!message.pollState?.closed;
13
- async function act(work) { if (busy)
14
- return; setBusy(true); setError(''); try {
25
+ if (poll.kind === 'yes_no')
26
+ return _jsx(YesNoQuestion, { message, userId, ink, accent, enabled, onVote, onClose, resolveVoterName, renderDetails, detailsOpen, onDetailsOpenChange, background, foreground });
27
+ const savedVotes = message.pollState?.votes ?? [], selected = pending ?? savedVotes.find(v => v.userId === userId)?.options ?? [], closed = !!message.pollState?.closed;
28
+ selection.current = selected;
29
+ const votes = pending === null ? savedVotes : [...savedVotes.filter(v => v.userId !== userId), ...(pending.length ? [{ userId, options: pending }] : [])];
30
+ async function act(work) { if (actionBusy.current)
31
+ return; actionBusy.current = true; setBusy(true); setError(''); try {
15
32
  await work();
16
33
  }
17
34
  catch {
18
35
  setError('The poll could not be updated. Try again.');
19
36
  }
20
37
  finally {
38
+ actionBusy.current = false;
21
39
  setBusy(false);
22
40
  } }
23
- return _jsxs(View, { style: { width: 270, maxWidth: '100%', gap: 2, padding: 0 }, children: [_jsx(Text, { style: { fontSize: 17, lineHeight: 23, fontWeight: '600', color: ink }, children: poll.question }), _jsx(Text, { style: { fontSize: 12, color: ink, opacity: .7 }, children: closed ? 'Poll closed' : poll.multiple ? 'Choose one or more' : 'Choose one' }), poll.options.map((label, index) => {
24
- const count = votes.filter(v => v.options.includes(index)).length, checked = selected.includes(index);
25
- return _jsxs(Pressable, { accessibilityRole: poll.multiple ? 'checkbox' : 'radio', accessibilityLabel: `${label}, ${count} ${count === 1 ? 'vote' : 'votes'}`, "aria-checked": checked, accessibilityState: { checked, disabled: !enabled || closed || busy }, disabled: !enabled || closed || busy, onPress: () => void act(() => onVote(checked ? selected.filter(x => x !== index) : poll.multiple ? [...selected, index] : [index])), style: { minHeight: 44, paddingVertical: 5, gap: 3 }, children: [_jsxs(View, { style: { flexDirection: 'row', gap: 10, alignItems: 'center' }, children: [_jsx(View, { 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);
26
- }), _jsxs(Pressable, { accessibilityRole: "button", accessibilityLabel: "View votes", onPress: () => setShowVotes(true), style: { minHeight: 44, flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', gap: 8 }, children: [_jsx(Text, { style: { color: ink, fontSize: 14 }, children: "View votes" }), _jsxs(Text, { style: { color: ink, opacity: .7, fontSize: 12 }, children: [votes.length, " ", votes.length === 1 ? 'vote' : 'votes'] })] }), !!error && _jsx(Text, { accessibilityRole: "alert", style: { color: ink }, children: error }), showVotes && _jsx(Modal, { visible: true, animationType: "slide", onRequestClose: () => setShowVotes(false), children: _jsx(SafeAreaProvider, { children: _jsxs(SafeAreaView, { style: { flex: 1, backgroundColor: background }, children: [_jsxs(View, { style: { height: 56, paddingHorizontal: 20, flexDirection: 'row', alignItems: 'center', gap: 16 }, children: [_jsx(Text, { accessibilityRole: "header", style: { color: foreground, fontSize: 22, fontWeight: '600', flex: 1 }, children: "Votes" }), _jsx(Pressable, { accessibilityRole: "button", accessibilityLabel: "Close votes", onPress: () => setShowVotes(false), style: { minHeight: 44, justifyContent: 'center' }, children: _jsx(Text, { style: { color: accent, fontSize: 17 }, children: "Done" }) })] }), _jsxs(ScrollView, { contentContainerStyle: { padding: 16, gap: 14 }, children: [_jsx(Text, { style: { fontSize: 20, lineHeight: 27, fontWeight: '600', color: foreground }, children: poll.question }), poll.options.map((option, index) => { const people = votes.filter(v => v.options.includes(index)); return _jsxs(View, { style: { gap: 8 }, children: [_jsx(Text, { style: { color: foreground, fontSize: 17, fontWeight: '600' }, children: option }), people.length ? people.map(v => _jsx(Text, { style: { color: foreground, fontSize: 16, paddingVertical: 3 }, children: v.userId === userId ? 'You' : resolveVoterName?.(v.userId) || 'Chat member' }, v.userId)) : _jsx(Text, { style: { color: foreground, opacity: .6, fontSize: 15 }, children: "No votes yet" })] }, index); }), message.senderId === userId && !closed && enabled && _jsx(Pressable, { accessibilityRole: "button", disabled: busy, onPress: () => void act(onClose), style: { minHeight: 48, justifyContent: 'center' }, children: _jsx(Text, { style: { color: accent, fontSize: 17 }, children: "End poll" }) }), closed && _jsx(Text, { style: { color: foreground, opacity: .7 }, children: "Poll closed" }), !!error && _jsx(Text, { accessibilityRole: "alert", style: { color: foreground }, children: error })] })] }) }) })] });
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) => {
42
+ const people = votes.filter(v => v.options.includes(index));
43
+ 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
+ const count = votes.filter(v => v.options.includes(index)).length, checked = selected.includes(index);
48
+ 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
+ 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(PollModal, { background: background, onClose: () => setShowVotes(false), children: _jsxs(PollArea, { style: { flexShrink: 1, backgroundColor: background }, children: [_jsxs(View, { style: { height: 56, paddingHorizontal: 20, flexDirection: 'row', alignItems: 'center', gap: 16 }, children: [_jsx(Text, { accessibilityRole: "header", style: { color: foreground, fontSize: 22, fontWeight: '600', flex: 1 }, children: "Votes" }), _jsx(Pressable, { accessibilityRole: "button", accessibilityLabel: "Close votes", onPress: () => setShowVotes(false), style: { minHeight: 44, justifyContent: 'center' }, children: _jsx(Text, { style: { color: accent, fontSize: 17 }, children: "Done" }) })] }), _jsx(ScrollView, { contentContainerStyle: { padding: 16 }, children: details })] }) }))] });
27
51
  }
28
- export function PollComposer({ appearance, onSend, onCancel, retrySafe = false }) {
52
+ /** Recipient-only decision. Keep the attempted answer on an uncertain failure so
53
+ * 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' }) {
55
+ const [attempt, setAttempt] = useState(null), [busy, setBusy] = useState(false), [error, setError] = useState(''), [localOpen, setLocalOpen] = useState(false);
56
+ const inFlight = useRef(false), firstChoice = useRef(null);
57
+ const poll = message.poll, answer = message.pollState?.votes.find(v => v.userId !== message.senderId)?.options[0];
58
+ const closed = !!message.pollState?.closed, chosen = answer ?? attempt, mine = message.senderId === userId;
59
+ const open = detailsOpen ?? localOpen;
60
+ const setOpen = (value) => { setLocalOpen(value); onDetailsOpenChange?.(value); };
61
+ async function choose(index) {
62
+ if (inFlight.current || !enabled || mine || closed)
63
+ return;
64
+ if (firstChoice.current !== null && firstChoice.current !== index)
65
+ return;
66
+ firstChoice.current = index;
67
+ inFlight.current = true;
68
+ setAttempt(index);
69
+ setBusy(true);
70
+ setError('');
71
+ setOpen(false);
72
+ try {
73
+ await onVote([index]);
74
+ }
75
+ catch {
76
+ setError('Answer not confirmed. Tap to retry.');
77
+ }
78
+ finally {
79
+ inFlight.current = false;
80
+ setBusy(false);
81
+ }
82
+ }
83
+ 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)
85
+ 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: '100%', width: 270 }, children: [_jsx(Text, { accessibilityRole: "button", onPress: () => setOpen(true), style: { fontSize: 17, lineHeight: 23, fontWeight: '600', color: ink }, children: poll.question }), _jsx(View, { style: { flexDirection: 'row', gap: 20, paddingVertical: 6 }, 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); }, style: { flexDirection: 'row', alignItems: 'center', gap: 8, minHeight: 24 }, 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)
87
+ void choose(attempt); }, children: _jsx(Text, { accessibilityRole: "alert", style: { color: ink, fontSize: 13 }, children: error }) })] }), open && (renderDetails ? renderDetails(details, () => setOpen(false)) : _jsxs(PollModal, { background: background, onClose: () => setOpen(false), children: [_jsxs(View, { style: { flexDirection: 'row', alignItems: 'center', marginBottom: 12 }, children: [_jsx(Text, { accessibilityRole: "header", style: { flex: 1, color: foreground, fontSize: 16, fontWeight: '600' }, children: "Answer" }), _jsx(Pressable, { accessibilityRole: "button", accessibilityLabel: "Close answer", onPress: () => setOpen(false), style: { minWidth: 44, minHeight: 44, justifyContent: 'center', alignItems: 'center' }, children: _jsx(Text, { style: { color: foreground, fontSize: 24 }, children: "\u00D7" }) })] }), _jsx(ScrollView, { children: details })] }))] });
88
+ }
89
+ export function PollComposer({ appearance, onSend, onCancel, retrySafe = false, yesNo = false }) {
29
90
  const [question, setQuestion] = useState(''), [options, setOptions] = useState(['', '']), [multiple, setMultiple] = useState(false), [busy, setBusy] = useState(false), [error, setError] = useState(''), [submitted, setSubmitted] = useState(false);
30
91
  const c = appearance.colors;
31
92
  async function send() { if (busy || submitted)
32
93
  return; let poll; try {
33
- poll = validatePoll({ question, options, multiple });
94
+ poll = validatePoll(yesNo ? { question, options: ['Yes', 'No'], multiple: false, kind: 'yes_no' } : { question, options, multiple });
34
95
  }
35
96
  catch (e) {
36
97
  setError(e.message);
@@ -48,7 +109,13 @@ export function PollComposer({ appearance, onSend, onCancel, retrySafe = false }
48
109
  setBusy(false);
49
110
  } }
50
111
  const inputStyle = { fontSize: 17, color: c.foreground, minHeight: 48, borderBottomWidth: 1, borderBottomColor: c.secondary + '40', paddingVertical: 12 };
51
- const ready = question.trim().length > 0 && options.filter(o => o.trim()).length >= 2;
52
- return _jsx(Modal, { visible: true, animationType: "slide", onRequestClose: () => { if (!busy)
53
- onCancel(); }, children: _jsx(SafeAreaProvider, { children: _jsxs(SafeAreaView, { style: { flex: 1, backgroundColor: c.background }, children: [_jsxs(View, { style: { height: 56, paddingHorizontal: 16, flexDirection: 'row', alignItems: 'center', gap: 16 }, children: [_jsx(Pressable, { accessibilityRole: "button", disabled: busy, onPress: onCancel, style: { minHeight: 44, justifyContent: 'center' }, children: _jsx(Text, { style: { color: c.foreground, fontSize: 16 }, children: "Cancel" }) }), _jsx(Text, { accessibilityRole: "header", style: { flex: 1, fontSize: 20, fontWeight: '600', color: c.foreground }, children: "New poll" }), _jsx(Pressable, { accessibilityRole: "button", accessibilityLabel: "Send poll", disabled: busy || submitted || !ready, onPress: () => void send(), style: { minHeight: 44, justifyContent: 'center', opacity: ready ? 1 : .4 }, children: busy ? _jsx(ActivityIndicator, { color: c.accent }) : _jsx(Text, { style: { color: c.accent, fontSize: 17, fontWeight: '600' }, children: "Send" }) })] }), _jsx(KeyboardAvoidingView, { style: { flex: 1 }, behavior: Platform.OS === 'ios' ? 'padding' : 'height', children: _jsxs(ScrollView, { keyboardShouldPersistTaps: "handled", keyboardDismissMode: "on-drag", contentContainerStyle: { padding: 16, paddingBottom: 20, gap: 4 }, children: [_jsx(Text, { style: { color: c.secondary, fontSize: 14, marginTop: 12 }, 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 }), _jsx(Text, { style: { color: c.secondary, fontSize: 14, marginTop: 16 }, 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." }), !!error && _jsx(Text, { accessibilityRole: "alert", style: { color: c.foreground, marginTop: 8 }, children: error })] }) })] }) }) });
112
+ const web = true;
113
+ let ready = false;
114
+ try {
115
+ validatePoll(yesNo ? { question, options: ['Yes', 'No'], multiple: false, kind: 'yes_no' } : { question, options, multiple });
116
+ ready = true;
117
+ }
118
+ catch { }
119
+ return _jsx(PollModal, { form: true, background: c.background, onClose: () => { if (!busy)
120
+ onCancel(); }, children: _jsxs(PollArea, { style: { ...(web ? { flexShrink: 1 } : { flex: 1 }), backgroundColor: c.background }, children: [_jsxs(View, { style: { height: web ? 36 : 56, paddingHorizontal: web ? 0 : 16, flexDirection: 'row', alignItems: 'center', gap: 16 }, children: [!web && _jsx(Pressable, { accessibilityRole: "button", disabled: busy, onPress: onCancel, style: { minHeight: 44, justifyContent: 'center' }, children: _jsx(Text, { style: { color: c.foreground, fontSize: 16 }, children: "Cancel" }) }), _jsx(Text, { accessibilityRole: "header", style: { flex: 1, fontSize: 20, fontWeight: '600', color: c.foreground }, children: yesNo ? 'Yes/No question' : 'New poll' }), !web && _jsx(Pressable, { accessibilityRole: "button", accessibilityLabel: yesNo ? "Send question" : "Send poll", disabled: busy || submitted || !ready, onPress: () => void send(), style: { minHeight: 44, justifyContent: 'center', opacity: ready ? 1 : .4 }, children: busy ? _jsx(ActivityIndicator, { color: c.accent }) : _jsx(Text, { style: { color: c.accent, fontSize: 17, fontWeight: '600' }, children: "Send" }) })] }), _jsx(KeyboardAvoidingView, { style: web ? { flexShrink: 1 } : { flex: 1 }, behavior: Platform.OS === 'ios' ? 'padding' : 'height', children: _jsxs(ScrollView, { keyboardShouldPersistTaps: "handled", keyboardDismissMode: "on-drag", contentContainerStyle: { padding: web ? 0 : 16, paddingBottom: web ? 12 : 20, gap: 4 }, children: [_jsx(Text, { style: { color: c.secondary, fontSize: 14, marginTop: 12 }, 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: 14, marginTop: 16 }, 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 })] }) }), web && _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" }) })] })] }) });
54
121
  }
@@ -0,0 +1,9 @@
1
+ import React from 'react';
2
+ export declare function MessagingPopup({ children, onClose, background, form, visible, onDismiss }: {
3
+ children: React.ReactNode;
4
+ onClose: () => void;
5
+ background: string;
6
+ form?: boolean;
7
+ visible?: boolean;
8
+ onDismiss?: () => void;
9
+ }): React.JSX.Element;
package/dist/popup.js ADDED
@@ -0,0 +1,7 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { KeyboardAvoidingView, Modal, Platform, View } from 'react-native';
3
+ // Messenger's centered popup on every platform. Keyboard movement is contained
4
+ // by the scrim; the form itself must never become a separate full-screen route.
5
+ export function MessagingPopup({ children, onClose, background, form = false, visible = true, onDismiss }) {
6
+ return _jsx(Modal, { visible: visible, onDismiss: onDismiss, transparent: true, animationType: "fade", onRequestClose: onClose, children: _jsx(KeyboardAvoidingView, { behavior: Platform.OS === 'ios' ? 'padding' : undefined, style: { flex: 1, backgroundColor: '#0009', justifyContent: 'center', alignItems: 'center', padding: 24 }, children: _jsx(View, { accessibilityViewIsModal: true, onAccessibilityEscape: onClose, style: { width: '100%', maxWidth: 360, maxHeight: '85%', backgroundColor: background, borderRadius: form ? 20 : 18, padding: form ? 20 : 16, overflow: 'hidden' }, children: children }) }) });
7
+ }
@@ -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,12 @@
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 }: {
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;
11
12
  }): React.JSX.Element;
package/dist/safety.js CHANGED
@@ -1,14 +1,20 @@
1
1
  import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useRef, useState } from 'react';
2
+ import { useEffect, useRef, useState } from 'react';
3
3
  import { KeyboardAvoidingView, Modal, Platform, Pressable, ScrollView, Text, TextInput, View } from 'react-native';
4
4
  /** 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 }) {
5
+ export function MessagingSafety({ client, state, appearance, name, messageId, onClose, onSearch }) {
6
6
  const [mode, setMode] = useState(messageId ? 'report' : 'menu');
7
7
  const [category, setCategory] = useState(''), [details, setDetails] = useState(''), [busy, setBusy] = useState(false), [error, setError] = useState('');
8
8
  const reportRequest = useRef(null), reportContent = useRef('');
9
+ const [visible, setVisible] = useState(true), afterClose = useRef(null);
10
+ const dismissed = () => { const next = afterClose.current; afterClose.current = null; next?.(); };
11
+ useEffect(() => { if (!visible && Platform.OS !== 'ios')
12
+ dismissed(); }, [visible]);
13
+ useEffect(() => () => { afterClose.current = null; }, [client]);
14
+ const search = () => { afterClose.current = onSearch ?? null; setVisible(false); };
9
15
  const c = appearance.colors, safety = state.conversation?.safety, blocked = safety?.blockedByMe;
10
16
  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 }) });
17
+ 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 }) });
12
18
  async function block() { setBusy(true); setError(''); try {
13
19
  await client.setBlocked(!blocked);
14
20
  onClose();
@@ -42,6 +48,6 @@ export function MessagingSafety({ client, state, appearance, name, messageId, on
42
48
  setBusy(false);
43
49
  }
44
50
  }
45
- return _jsx(Modal, { visible: true, transparent: true, animationType: "fade", onRequestClose: () => { if (!busy)
46
- onClose(); }, children: _jsx(KeyboardAvoidingView, { behavior: Platform.OS === 'ios' ? 'padding' : undefined, style: { flex: 1, backgroundColor: '#0009', justifyContent: 'center', alignItems: 'center', padding: 24 }, children: _jsxs(ScrollView, { keyboardShouldPersistTaps: "handled", accessibilityViewIsModal: true, style: { width: '100%', maxWidth: 360, flexGrow: 0, flexShrink: 1, maxHeight: '90%', backgroundColor: c.background, borderRadius: 20, borderWidth: 1, borderColor: c.secondary + '40' }, contentContainerStyle: { padding: 20, gap: 8 }, children: [_jsx(Text, { accessibilityRole: "header", style: { fontSize: 20, fontWeight: '600', color: c.foreground }, children: mode === 'menu' ? name : mode === 'block' ? `${blocked ? 'Unblock' : 'Block'} ${name}?` : mode === 'sent' ? 'Report sent' : messageId ? 'Report message' : 'Report conversation' }), mode === 'menu' && _jsxs(_Fragment, { children: [safety?.canBlock && button(blocked ? 'Unblock contact' : 'Block contact', () => setMode('block')), safety?.canReport && button('Report', () => setMode('report'))] }), 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 }), _jsxs(View, { style: { flexDirection: 'row', gap: 8, marginTop: 8 }, children: [mode !== 'sent' && 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')] })] }) }) });
51
+ return _jsx(Modal, { visible: visible, onDismiss: dismissed, transparent: true, animationType: "fade", onRequestClose: () => { if (!busy)
52
+ onClose(); }, children: _jsx(KeyboardAvoidingView, { behavior: Platform.OS === 'ios' ? 'padding' : undefined, style: { flex: 1, backgroundColor: '#0009', justifyContent: 'center', alignItems: 'center', padding: 24 }, children: _jsxs(ScrollView, { keyboardShouldPersistTaps: "handled", accessibilityViewIsModal: true, style: { width: '100%', maxWidth: 360, flexGrow: 0, flexShrink: 1, maxHeight: '90%', backgroundColor: c.background, borderRadius: 20, borderWidth: 1, borderColor: c.secondary + '40' }, contentContainerStyle: { padding: 20, gap: 8 }, children: [_jsx(Text, { accessibilityRole: "header", style: { fontSize: 20, fontWeight: '600', color: c.foreground }, children: mode === 'menu' ? name : mode === 'block' ? `${blocked ? 'Unblock' : 'Block'} ${name}?` : mode === 'sent' ? 'Report sent' : messageId ? 'Report message' : 'Report conversation' }), mode === 'menu' && _jsxs(_Fragment, { children: [onSearch && button('Search messages', search), safety?.canBlock && button(blocked ? 'Unblock contact' : 'Block contact', () => setMode('block')), safety?.canReport && button('Report', () => setMode('report'))] }), 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 }), _jsxs(View, { style: { flexDirection: 'row', gap: 8, marginTop: 8 }, children: [mode !== 'sent' && 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
53
  }
@@ -0,0 +1,10 @@
1
+ import React from 'react';
2
+ import type { MessagingAppearance, SavedMessage } from '@robono/messaging';
3
+ /** Same compact 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,46 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { useEffect, useRef, useState } from 'react';
3
+ import { Platform, Pressable, ScrollView, Text, View } from 'react-native';
4
+ import { MessagingPopup } from './popup.js';
5
+ import { MessageIcon } from './icons.js';
6
+ /** Same compact popup and media viewer as the conversation; no server-history copy. */
7
+ 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 [visible, setVisible] = useState(true), pending = useRef(null), alive = useRef(true);
10
+ useEffect(() => { alive.current = true; return () => { alive.current = false; pending.current = null; }; }, []);
11
+ // iOS presents one controller at a time. Finish dismissing Saved before opening
12
+ // the media controller; otherwise iOS can silently discard the new viewer.
13
+ async function dismissed() {
14
+ const record = pending.current;
15
+ if (!record)
16
+ return;
17
+ pending.current = null;
18
+ try {
19
+ // Let React remove the dismissed native host before mounting the viewer.
20
+ // Presenting it in the dismissal callback can leave controls with no window.
21
+ if (Platform.OS === 'ios')
22
+ await new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(() => resolve())));
23
+ if (!alive.current)
24
+ return;
25
+ await onOpen(record);
26
+ if (alive.current)
27
+ onClose();
28
+ }
29
+ catch (e) {
30
+ if (alive.current) {
31
+ setError(e instanceof Error ? e.message : 'This saved attachment could not be opened.');
32
+ setBusy(false);
33
+ setVisible(true);
34
+ }
35
+ }
36
+ }
37
+ useEffect(() => { if (!visible && Platform.OS !== 'ios')
38
+ void dismissed(); }, [visible]);
39
+ function open(record) { if (record.message.kind === 'text') {
40
+ setDetail(record);
41
+ return;
42
+ } if (busy)
43
+ 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
+ onClose(); }, children: [_jsxs(View, { style: { flexDirection: 'row', alignItems: 'center' }, children: [_jsx(Text, { accessibilityRole: "header", style: { flex: 1, color: c.foreground, fontSize: 17, fontWeight: '600' }, children: detail ? 'Saved message' : 'Saved' }), _jsx(Pressable, { accessibilityRole: "button", accessibilityLabel: detail ? 'Back to Saved' : 'Close Saved', disabled: busy, onPress: () => detail ? setDetail(null) : onClose(), style: { minWidth: 44, minHeight: 44, alignItems: 'center', justifyContent: 'center' }, children: _jsx(MessageIcon, { name: detail ? 'back' : 'close', color: c.foreground }) })] }), _jsx(ScrollView, { style: { maxHeight: 400 }, children: 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: 12, gap: 3 }, 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)) }), !records.length && _jsx(Text, { style: { color: c.secondary, marginVertical: 16 }, children: "No saved messages yet." }), !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
+ }
@@ -0,0 +1,9 @@
1
+ import React from 'react';
2
+ import type { MessagingAppearance, MessagingClient } from '@robono/messaging';
3
+ /** Uses the same centered popup proportions as Messenger's chat details. */
4
+ export declare function MessagingSearch({ client, appearance, onClose, resolveName }: {
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,41 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useEffect, useRef, useState } from 'react';
3
+ import { ActivityIndicator, KeyboardAvoidingView, Modal, Platform, Pressable, ScrollView, Text, TextInput, View } from 'react-native';
4
+ /** Uses the same centered popup proportions as Messenger's chat details. */
5
+ export function MessagingSearch({ client, appearance, onClose, resolveName }) {
6
+ const c = appearance.colors, [query, setQuery] = useState(''), [searched, setSearched] = useState(''), [messages, setMessages] = useState([]), [next, setNext] = useState(null), [busy, setBusy] = useState(false), [error, setError] = useState('');
7
+ const request = useRef(null), generation = useRef(0);
8
+ useEffect(() => () => { generation.current++; request.current?.abort(); }, []);
9
+ async function search(more = false) {
10
+ const term = (more ? searched : query).trim();
11
+ if (!term)
12
+ return;
13
+ request.current?.abort();
14
+ const control = new AbortController();
15
+ request.current = control;
16
+ const id = ++generation.current;
17
+ setBusy(true);
18
+ setError('');
19
+ if (!more) {
20
+ setMessages([]);
21
+ setNext(null);
22
+ setSearched(term);
23
+ }
24
+ try {
25
+ const result = await client.search(term, { ...(more && next ? { before: next } : {}), signal: control.signal });
26
+ if (id !== generation.current)
27
+ return;
28
+ setMessages(previous => more ? [...previous, ...result.messages] : result.messages);
29
+ setNext(result.hasMore ? result.nextCursor : null);
30
+ }
31
+ catch (e) {
32
+ if (id === generation.current && !control.signal.aborted)
33
+ setError(e instanceof Error ? e.message : 'Search could not be completed. Try again.');
34
+ }
35
+ finally {
36
+ if (id === generation.current)
37
+ setBusy(false);
38
+ }
39
+ }
40
+ return _jsx(Modal, { visible: true, transparent: true, animationType: "fade", onRequestClose: onClose, children: _jsx(KeyboardAvoidingView, { behavior: Platform.OS === 'ios' ? 'padding' : undefined, style: { flex: 1, justifyContent: 'center', alignItems: 'center', backgroundColor: '#0009', padding: 20 }, children: _jsxs(View, { accessibilityViewIsModal: true, onAccessibilityEscape: onClose, style: { width: '100%', maxWidth: 360, maxHeight: '85%', padding: 16, borderRadius: 18, backgroundColor: c.background, gap: 12 }, children: [_jsxs(View, { style: { flexDirection: 'row', alignItems: 'center', gap: 8 }, children: [_jsx(Text, { accessibilityRole: "header", style: { flex: 1, color: c.foreground, fontSize: 16, fontWeight: '600' }, children: "Search messages" }), _jsx(Pressable, { accessibilityRole: "button", accessibilityLabel: "Close search", onPress: onClose, hitSlop: 12, children: _jsx(Text, { style: { color: c.accent, fontSize: 16 }, children: "Done" }) })] }), _jsxs(View, { style: { flexDirection: 'row', alignItems: 'center', gap: 8 }, children: [_jsx(TextInput, { accessibilityLabel: "Search text", value: query, maxLength: 200, autoFocus: true, placeholder: "Find a message", placeholderTextColor: c.secondary, onChangeText: value => { request.current?.abort(); generation.current++; setBusy(false); setQuery(value); setSearched(''); setMessages([]); setNext(null); setError(''); }, onSubmitEditing: () => void search(), returnKeyType: "search", style: { flex: 1, minWidth: 0, minHeight: 44, padding: 10, borderRadius: 10, borderWidth: 1, borderColor: c.secondary + '60', color: c.foreground, fontSize: 16 } }), _jsx(Pressable, { accessibilityRole: "button", accessibilityLabel: "Search", disabled: busy || !query.trim(), onPress: () => void search(), style: { minHeight: 44, justifyContent: 'center', opacity: (busy || !query.trim()) ? 0.5 : 1 }, children: _jsx(Text, { style: { color: c.accent, fontSize: 16 }, children: "Search" }) })] }), !!error && _jsx(Text, { accessibilityRole: "alert", style: { color: c.foreground }, children: error }), _jsxs(ScrollView, { keyboardShouldPersistTaps: "handled", contentContainerStyle: { gap: 14 }, style: { flexShrink: 1 }, children: [messages.map(message => _jsxs(Pressable, { accessibilityRole: "button", accessibilityLabel: 'Go to message: ' + message.text, disabled: busy, onPress: () => { setBusy(true); setError(''); void client.focusMessage(message.id).then(onClose).catch(e => setError(e instanceof Error ? e.message : 'This message is unavailable.')).finally(() => setBusy(false)); }, style: { gap: 4, minHeight: 44 }, children: [_jsxs(Text, { style: { color: c.secondary, fontSize: 12 }, children: [resolveName?.(message.senderId) ?? 'Chat member', " \u00B7 ", new Date(message.createdAt).toLocaleDateString()] }), _jsx(Text, { selectable: true, style: { color: c.foreground, fontSize: 16, lineHeight: 22 }, children: message.text })] }, message.id)), searched && !busy && !error && !messages.length && _jsx(Text, { style: { color: c.secondary }, children: "No matching messages." }), next && !busy && _jsx(Pressable, { accessibilityRole: "button", onPress: () => void search(true), style: { minHeight: 44, justifyContent: 'center' }, children: _jsx(Text, { style: { color: c.accent }, children: "More results" }) })] }), busy && _jsx(ActivityIndicator, { accessibilityLabel: "Searching messages", color: c.accent })] }) }) });
41
+ }
@@ -0,0 +1,19 @@
1
+ import React, { type ComponentType } 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
+ export declare function SocialMessagePreview({ message, client, Player, origin, accent, foreground, background }: {
12
+ message: MessagingMessage;
13
+ client: MessagingClient;
14
+ Player: ComponentType<SocialPlayerProps>;
15
+ origin: string;
16
+ accent: string;
17
+ foreground: string;
18
+ background: string;
19
+ }): React.JSX.Element | null;
package/dist/social.js ADDED
@@ -0,0 +1,35 @@
1
+ import { jsxs as _jsxs, jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { useEffect, useState } from 'react';
3
+ import { ActivityIndicator, AppState, Linking, Platform, Pressable, Text, View, useWindowDimensions } from 'react-native';
4
+ import { socialShare } from '@robono/messaging';
5
+ import { MessagingPopup } from './popup.js';
6
+ import { MessageIcon, chatSurface } from './icons.js';
7
+ import { messageLinks } from './message-links.js';
8
+ export function SocialMessagePreview({ message, client, Player, origin, accent, foreground, background }) {
9
+ const share = messageLinks(message.text ?? '').map(link => link.url ? socialShare(link.url, origin) : null).find(Boolean);
10
+ const [opened, setOpened] = useState(null);
11
+ 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;
12
+ useEffect(() => { const sub = AppState.addEventListener('change', state => { if (state !== 'active')
13
+ setOpened(null); }); return () => sub.remove(); }, []);
14
+ if (!share || message.deleted)
15
+ return null;
16
+ return _jsxs(_Fragment, { children: [_jsx(Pressable, { accessibilityRole: "button", accessibilityLabel: `Play ${share.provider} preview`, onPress: event => { event.stopPropagation(); setOpened({ client, message }); }, style: { paddingVertical: 5 }, children: _jsxs(Text, { numberOfLines: 1, style: { color: accent, fontSize: 13, fontWeight: '600' }, children: ["\u25B6 ", share.provider] }) }), 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)] });
17
+ }
18
+ function SocialPreviewPopup({ message, client, Player, origin, provider, url, accent, foreground, background, onClose }) {
19
+ const [preview, setPreview] = useState(null), [busy, setBusy] = useState(true), [error, setError] = useState('');
20
+ const { height } = useWindowDimensions();
21
+ useEffect(() => { if (Platform.OS !== 'web')
22
+ return; const escape = (event) => { if (event.key === 'Escape') {
23
+ event.preventDefault();
24
+ event.stopPropagation();
25
+ onClose();
26
+ } }; document.addEventListener('keydown', escape, true); return () => document.removeEventListener('keydown', escape, true); }, [onClose]);
27
+ useEffect(() => { let active = true; void client.socialPreview(message.id, origin).then(result => { if (active) {
28
+ setPreview(result);
29
+ if (!result.html)
30
+ setError('This post cannot be played here. Open the original below.');
31
+ } }).catch(() => { if (active)
32
+ setError('This preview could not be loaded. Open the original below.'); }).finally(() => { if (active)
33
+ setBusy(false); }); return () => { active = false; }; }, [client, message.id, origin]);
34
+ return _jsxs(MessagingPopup, { background: chatSurface(background), onClose: onClose, children: [_jsxs(View, { style: { flexDirection: 'row', alignItems: 'center' }, children: [_jsx(Text, { accessibilityRole: "header", style: { flex: 1, color: foreground, fontSize: 17, fontWeight: '600' }, children: provider }), _jsx(Pressable, { accessibilityRole: "button", accessibilityLabel: "Close social preview", onPress: onClose, style: { width: 44, height: 44, alignItems: 'center', justifyContent: 'center' }, children: _jsx(MessageIcon, { name: "close", color: foreground }) })] }), busy && _jsx(ActivityIndicator, { accessibilityLabel: "Loading social preview", color: accent, style: { margin: 20 } }), !!preview?.html && _jsx(View, { style: { height: Math.max(180, Math.min(['YouTube', 'Rumble', 'Vimeo', 'Dailymotion', 'Twitch'].includes(provider) ? 220 : 400, height * .65 - 60)), 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: foreground, 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] }) })] });
35
+ }
@@ -1,6 +1,7 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  // Generated by scripts/design/generate-icons.mjs from the approved Robono artwork.
3
3
  import React, { useId } from 'react';
4
+ import { Platform } from 'react-native';
4
5
  import Svg, { Path, Circle, Rect, Ellipse, Line, G, Defs, Mask } from 'react-native-svg';
5
6
  import { iconPaths } from './icon-paths.js';
6
7
  import { iconNodes, iconNames } from './icon-nodes.js';
@@ -14,7 +15,7 @@ function draw(node, index, prefix, color) {
14
15
  }
15
16
  export function RobonoIcon({ name, size = 24, color = '#F7F7F7', ...props }) {
16
17
  const prefix = 'robono' + useId().replace(/[^a-zA-Z0-9]/g, '') + '-';
17
- return _jsx(Svg, { width: size, height: size, viewBox: "0 0 32 32", color: color, accessible: false, ...props, children: iconNodes[iconNames[name]]?.map((node, i) => draw(node, i, prefix, color)) });
18
+ return _jsx(Svg, { width: size, height: size, viewBox: "0 0 32 32", color: color, ...(Platform.OS === 'web' ? { 'aria-hidden': true } : { accessible: false }), ...props, children: iconNodes[iconNames[name]]?.map((node, i) => draw(node, i, prefix, color)) });
18
19
  }
19
20
  RobonoIcon.glyphMap = iconPaths;
20
21
  export default RobonoIcon;
@@ -10,11 +10,11 @@ export function VoiceMessageComposer({ appearance, maxSeconds, busy, onRecord, a
10
10
  const canSend = !!review && (phase === 'review' || phase === 'failed') && !!onSend;
11
11
  if (renderTextComposer && !recording && phase !== 'preparing' && phase !== 'uploading')
12
12
  return _jsxs(View, { style: { gap: 4 }, children: [renderAttachments ? renderAttachments(review, waiting) : review && _jsx(View, { style: { paddingHorizontal: 8, paddingTop: 4 }, children: review }), renderTextComposer({ sendVoice: canSend ? onSend : undefined, hasVoice: !!review, blocked: waiting }), !!error && _jsxs(View, { style: { flexDirection: 'row', alignItems: 'center', gap: 8, paddingHorizontal: 8 }, children: [_jsx(Text, { accessibilityRole: "alert", style: { color: c.foreground, fontSize: 12, flex: 1 }, children: error }), onRemove && _jsx(Pressable, { accessibilityRole: "button", accessibilityLabel: "Cancel recording", onPress: onRemove, style: { padding: 8 }, children: _jsx(MessageIcon, { name: "close", color: c.foreground, size: 18 }) })] })] });
13
- return _jsxs(View, { testID: "voice-only-composer", style: { gap: 5 }, children: [renderAttachments?.(review, waiting || recording), _jsxs(View, { style: { flexDirection: 'row', alignItems: 'flex-end', gap: 4 }, children: [_jsxs(View, { style: { flex: 1, minWidth: 0, minHeight: 44, borderRadius: 22, borderWidth: 1, borderColor: c.secondary + '20', backgroundColor: c.incoming, flexDirection: 'row', alignItems: 'center', overflow: 'hidden' }, children: [!recording && !review && !waiting && actions, review && !renderAttachments ? _jsx(View, { style: { flex: 1, minWidth: 0, paddingLeft: 10 }, children: review }) : recording ? _jsxs(View, { style: { flex: 1, minWidth: 0, flexDirection: 'row', alignItems: 'center', gap: 6, paddingLeft: 10 }, children: [_jsx(Text, { style: { fontSize: 11, fontWeight: '800', color: c.incomingText, fontVariant: ['tabular-nums'] }, children: duration(Math.floor(elapsedMs / 1000) * 1000) }), _jsx(Waveform, { samples: waveform, color: c.accent }), _jsx(Text, { style: { fontSize: 11, fontWeight: '800', color: c.incomingText, fontVariant: ['tabular-nums'] }, children: duration(maxSeconds * 1000) })] }) : _jsx(View, { style: { flex: 1, minWidth: 0, paddingHorizontal: 12 }, children: _jsx(Text, { style: { color: c.secondary, fontSize: 14, fontWeight: '800' }, children: waiting ? (phase === 'uploading' ? 'Sending…' : 'Preparing…') : `Voice up to ${maxSeconds}s` }) }), _jsx(Pressable, { accessibilityRole: "button", accessibilityLabel: recording ? 'Stop recording' : 'Voice message', disabled: waiting || !!review || phase === 'failed', onPress: recording ? onStop : onRecord, style: { width: 42, height: 44, alignItems: 'center', justifyContent: 'center', opacity: (waiting || !!review || phase === 'failed') ? .45 : 1 }, children: waiting ? _jsx(ActivityIndicator, { size: "small", color: c.accent }) : _jsx(MessageIcon, { name: "mic", color: recording ? '#c94040' : c.accent, size: 23 }) })] }), _jsx(Pressable, { accessibilityRole: "button", accessibilityLabel: canSend ? 'Send voice message' : 'Send unavailable', accessibilityState: { disabled: !canSend }, disabled: !canSend, onPress: onSend, style: { width: 44, height: 44, alignItems: 'center', justifyContent: 'center', opacity: canSend ? 1 : .4 }, children: _jsx(MessageIcon, { name: "send", color: c.accent, size: 24 }) })] }), !!error && _jsxs(View, { style: { flexDirection: 'row', alignItems: 'center', gap: 8, paddingHorizontal: 8 }, children: [_jsx(Text, { accessibilityRole: "alert", style: { color: c.foreground, fontSize: 12, flex: 1 }, children: error }), onRemove && _jsx(Pressable, { accessibilityRole: "button", accessibilityLabel: "Cancel recording", onPress: onRemove, style: { padding: 8 }, children: _jsx(MessageIcon, { name: "close", color: c.foreground, size: 18 }) })] })] });
13
+ return _jsxs(View, { testID: "voice-only-composer", style: { gap: 5 }, children: [renderAttachments?.(review, waiting || recording), _jsxs(View, { style: { flexDirection: 'row', alignItems: 'flex-end', gap: 4 }, children: [_jsxs(View, { style: { flex: 1, minWidth: 0, minHeight: 44, borderRadius: 22, borderWidth: 1, borderColor: c.secondary + '20', backgroundColor: c.incoming, flexDirection: 'row', alignItems: 'center', overflow: 'hidden' }, children: [!recording && !review && !waiting && actions, review && !renderAttachments ? _jsx(View, { style: { flex: 1, minWidth: 0, paddingLeft: 10 }, children: review }) : recording ? _jsxs(View, { style: { flex: 1, minWidth: 0, flexDirection: 'row', alignItems: 'center', gap: 6, paddingLeft: 10 }, children: [_jsx(Text, { style: { fontSize: 11, fontWeight: '800', color: c.incomingText, fontVariant: ['tabular-nums'] }, children: duration(Math.floor(elapsedMs / 1000) * 1000) }), appearance.showWaveform !== false ? _jsx(Waveform, { samples: waveform, color: c.accent }) : _jsx(View, { style: { flex: 1 } }), _jsx(Text, { style: { fontSize: 11, fontWeight: '800', color: c.incomingText, fontVariant: ['tabular-nums'] }, children: duration(maxSeconds * 1000) })] }) : _jsx(View, { style: { flex: 1, minWidth: 0, paddingHorizontal: 12 }, children: _jsx(Text, { style: { color: c.secondary, fontSize: 14, fontWeight: '800' }, children: waiting ? (phase === 'uploading' ? 'Sending…' : 'Preparing…') : `Voice up to ${maxSeconds}s` }) }), _jsx(Pressable, { accessibilityRole: "button", accessibilityLabel: recording ? 'Stop recording' : 'Voice message', disabled: waiting || !!review || phase === 'failed', onPress: recording ? onStop : onRecord, style: { width: 42, height: 44, alignItems: 'center', justifyContent: 'center', opacity: (waiting || !!review || phase === 'failed') ? .45 : 1 }, children: waiting ? _jsx(ActivityIndicator, { size: "small", color: c.accent }) : _jsx(MessageIcon, { name: "mic", color: recording ? '#c94040' : c.accent, size: 23 }) })] }), _jsx(Pressable, { accessibilityRole: "button", accessibilityLabel: canSend ? 'Send voice message' : 'Send unavailable', accessibilityState: { disabled: !canSend }, disabled: !canSend, onPress: onSend, style: { width: 44, height: 44, alignItems: 'center', justifyContent: 'center', opacity: canSend ? 1 : .4 }, children: _jsx(MessageIcon, { name: "send", color: c.accent, size: 24 }) })] }), !!error && _jsxs(View, { style: { flexDirection: 'row', alignItems: 'center', gap: 8, paddingHorizontal: 8 }, children: [_jsx(Text, { accessibilityRole: "alert", style: { color: c.foreground, fontSize: 12, flex: 1 }, children: error }), onRemove && _jsx(Pressable, { accessibilityRole: "button", accessibilityLabel: "Cancel recording", onPress: onRemove, style: { padding: 8 }, children: _jsx(MessageIcon, { name: "close", color: c.foreground, size: 18 }) })] })] });
14
14
  }
15
15
  export function VoiceDraftPlayback({ appearance, playing, positionMs = 0, durationMs = 0, waveform, onToggle, onRemove, disabled = false, error, compact = false }) {
16
16
  const c = appearance.colors;
17
17
  if (compact)
18
18
  return _jsxs(View, { style: { width: 88, height: 84, paddingTop: 8, paddingRight: 10 }, children: [_jsxs(View, { style: { width: 76, height: 76, borderRadius: 9, borderWidth: 1, borderColor: c.secondary + '40', backgroundColor: c.background, alignItems: 'center', justifyContent: 'center', gap: 2 }, children: [_jsx(Pressable, { accessibilityRole: "button", accessibilityLabel: playing ? 'Pause recorded voice message' : 'Play recorded voice message', disabled: disabled, onPress: onToggle, style: { width: 44, height: 38, alignItems: 'center', justifyContent: 'center' }, children: _jsx(View, { style: { width: 28, height: 28, borderRadius: 14, backgroundColor: c.accent, alignItems: 'center', justifyContent: 'center' }, children: _jsx(MessageIcon, { name: playing ? 'pause' : 'play', color: "#fff", size: 18 }) }) }), _jsx(Text, { style: { fontSize: 10, fontWeight: '800', color: c.foreground }, children: duration(positionMs >= durationMs ? durationMs : durationMs - positionMs) })] }), _jsx(Pressable, { accessibilityRole: "button", accessibilityLabel: "Remove voice message", disabled: disabled, onPress: onRemove, style: { position: 'absolute', right: 0, top: 0, width: 30, height: 30, alignItems: 'center', justifyContent: 'center' }, children: _jsx(View, { style: { width: 22, height: 22, borderRadius: 11, backgroundColor: c.foreground, alignItems: 'center', justifyContent: 'center' }, children: _jsx(MessageIcon, { name: "close", color: c.background, size: 14 }) }) }), !!error && _jsx(Text, { accessibilityRole: "alert", style: { color: c.foreground, fontSize: 11 }, children: error })] });
19
- return _jsxs(View, { style: { flex: 1, minWidth: 0, gap: 3 }, children: [_jsxs(View, { style: { flexDirection: 'row', alignItems: 'center', gap: 7, minHeight: 44 }, children: [_jsx(Pressable, { accessibilityRole: "button", accessibilityLabel: playing ? 'Pause recorded voice message' : 'Play recorded voice message', disabled: disabled, onPress: onToggle, style: { width: 32, height: 32, borderRadius: 16, backgroundColor: c.accent, alignItems: 'center', justifyContent: 'center' }, children: _jsx(MessageIcon, { name: playing ? 'pause' : 'play', color: "#fff", size: 17 }) }), _jsx(Waveform, { samples: waveform, color: c.accent }), _jsx(Text, { style: { fontSize: 12, fontWeight: '800', color: c.incomingText, fontVariant: ['tabular-nums'] }, children: duration(positionMs >= durationMs ? durationMs : durationMs - positionMs) }), _jsx(Pressable, { accessibilityRole: "button", accessibilityLabel: "Remove voice message", disabled: disabled, onPress: onRemove, style: { width: 30, height: 44, alignItems: 'center', justifyContent: 'center' }, children: _jsx(MessageIcon, { name: "close", color: c.incomingText, size: 16 }) })] }), !!error && _jsxs(View, { style: { flexDirection: 'row', alignItems: 'center', gap: 8, paddingHorizontal: 8 }, children: [_jsx(Text, { accessibilityRole: "alert", style: { color: c.foreground, fontSize: 12, flex: 1 }, children: error }), onRemove && _jsx(Pressable, { accessibilityRole: "button", accessibilityLabel: "Cancel recording", onPress: onRemove, style: { padding: 8 }, children: _jsx(MessageIcon, { name: "close", color: c.foreground, size: 18 }) })] })] });
19
+ return _jsxs(View, { style: { flex: 1, minWidth: 0, gap: 3 }, children: [_jsxs(View, { style: { flexDirection: 'row', alignItems: 'center', gap: 7, minHeight: 44 }, children: [_jsx(Pressable, { accessibilityRole: "button", accessibilityLabel: playing ? 'Pause recorded voice message' : 'Play recorded voice message', disabled: disabled, onPress: onToggle, style: { width: 32, height: 32, borderRadius: 16, backgroundColor: c.accent, alignItems: 'center', justifyContent: 'center' }, children: _jsx(MessageIcon, { name: playing ? 'pause' : 'play', color: "#fff", size: 17 }) }), appearance.showWaveform !== false ? _jsx(Waveform, { samples: waveform, color: c.accent }) : _jsx(View, { style: { flex: 1 } }), _jsx(Text, { style: { fontSize: 12, fontWeight: '800', color: c.incomingText, fontVariant: ['tabular-nums'] }, children: duration(positionMs >= durationMs ? durationMs : durationMs - positionMs) }), _jsx(Pressable, { accessibilityRole: "button", accessibilityLabel: "Remove voice message", disabled: disabled, onPress: onRemove, style: { width: 30, height: 44, alignItems: 'center', justifyContent: 'center' }, children: _jsx(MessageIcon, { name: "close", color: c.incomingText, size: 16 }) })] }), !!error && _jsxs(View, { style: { flexDirection: 'row', alignItems: 'center', gap: 8, paddingHorizontal: 8 }, children: [_jsx(Text, { accessibilityRole: "alert", style: { color: c.foreground, fontSize: 12, flex: 1 }, children: error }), onRemove && _jsx(Pressable, { accessibilityRole: "button", accessibilityLabel: "Cancel recording", onPress: onRemove, style: { padding: 8 }, children: _jsx(MessageIcon, { name: "close", color: c.foreground, size: 18 }) })] })] });
20
20
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@robono/messaging-react-native",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "type": "module",
5
5
  "sideEffects": false,
6
6
  "description": "Optional, composable direct and group messaging screens for Robono.",
@@ -45,19 +45,23 @@
45
45
  "prepack": "npm run build && node ../../scripts/generate-sdk-file-sbom.mjs"
46
46
  },
47
47
  "peerDependencies": {
48
- "@robono/messaging": "^0.1.0",
48
+ "@robono/messaging": "^0.2.0",
49
49
  "react": ">=18.2 <20",
50
50
  "react-native": ">=0.76 <1",
51
51
  "expo-audio": ">=55 <57",
52
52
  "expo-image-picker": ">=55 <57",
53
53
  "expo-document-picker": ">=55 <57",
54
- "expo-file-system": ">=55 <57",
54
+ "expo-file-system": ">=55.0.5 <57",
55
55
  "expo-sharing": ">=55 <57",
56
56
  "expo-video": ">=55 <57",
57
57
  "expo-crypto": ">=55 <57",
58
58
  "react-native-safe-area-context": ">=5 <6",
59
59
  "react-native-svg": ">=15 <16",
60
- "@robono/calling-react-native": "^0.3.0"
60
+ "@robono/calling-react-native": "^0.3.0",
61
+ "expo-clipboard": ">=8 <57",
62
+ "expo-image-manipulator": ">=14 <57",
63
+ "expo-video-thumbnails": ">=14 <57",
64
+ "react-native-webview": ">=13 <14"
61
65
  },
62
66
  "license": "SEE LICENSE IN LICENSE",
63
67
  "peerDependenciesMeta": {
@@ -87,6 +91,18 @@
87
91
  },
88
92
  "@robono/calling-react-native": {
89
93
  "optional": true
94
+ },
95
+ "expo-clipboard": {
96
+ "optional": true
97
+ },
98
+ "expo-image-manipulator": {
99
+ "optional": true
100
+ },
101
+ "expo-video-thumbnails": {
102
+ "optional": true
103
+ },
104
+ "react-native-webview": {
105
+ "optional": true
90
106
  }
91
107
  },
92
108
  "author": {