@robono/messaging-react-native 0.0.0-stage → 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 (76) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/LICENSE +130 -0
  3. package/README.md +240 -2
  4. package/SBOM.spdx.json +1847 -0
  5. package/app.plugin.cjs +5 -0
  6. package/dist/call-message.d.ts +6 -0
  7. package/dist/call-message.js +109 -0
  8. package/dist/calling.d.ts +15 -0
  9. package/dist/calling.js +22 -0
  10. package/dist/expo-calling.d.ts +6 -0
  11. package/dist/expo-calling.js +7 -0
  12. package/dist/expo-download.d.ts +3 -0
  13. package/dist/expo-download.js +43 -0
  14. package/dist/expo-media.d.ts +19 -0
  15. package/dist/expo-media.js +411 -0
  16. package/dist/expo-outbox.d.ts +13 -0
  17. package/dist/expo-outbox.js +81 -0
  18. package/dist/expo-saved.d.ts +24 -0
  19. package/dist/expo-saved.js +76 -0
  20. package/dist/expo-social.d.ts +3 -0
  21. package/dist/expo-social.js +21 -0
  22. package/dist/expo-thumbnails.d.ts +9 -0
  23. package/dist/expo-thumbnails.js +68 -0
  24. package/dist/expo.d.ts +42 -0
  25. package/dist/expo.js +549 -0
  26. package/dist/forward.d.ts +13 -0
  27. package/dist/forward.js +61 -0
  28. package/dist/icons.d.ts +7 -0
  29. package/dist/icons.js +10 -0
  30. package/dist/inbox.d.ts +16 -0
  31. package/dist/inbox.js +48 -0
  32. package/dist/index.d.ts +144 -0
  33. package/dist/index.js +696 -0
  34. package/dist/media-controller.d.ts +116 -0
  35. package/dist/media-controller.js +372 -0
  36. package/dist/media-readiness.d.ts +4 -0
  37. package/dist/media-readiness.js +4 -0
  38. package/dist/media-ui.d.ts +62 -0
  39. package/dist/media-ui.js +46 -0
  40. package/dist/message-links.d.ts +5 -0
  41. package/dist/message-links.js +30 -0
  42. package/dist/message-order.d.ts +4 -0
  43. package/dist/message-order.js +17 -0
  44. package/dist/native-outbox.d.ts +49 -0
  45. package/dist/native-outbox.js +51 -0
  46. package/dist/pollVoteQueue.d.ts +3 -0
  47. package/dist/pollVoteQueue.js +29 -0
  48. package/dist/polls.d.ts +25 -0
  49. package/dist/polls.js +121 -0
  50. package/dist/popup.d.ts +9 -0
  51. package/dist/popup.js +7 -0
  52. package/dist/recent-media.d.ts +21 -0
  53. package/dist/recent-media.js +123 -0
  54. package/dist/safety.d.ts +12 -0
  55. package/dist/safety.js +53 -0
  56. package/dist/saved.d.ts +10 -0
  57. package/dist/saved.js +46 -0
  58. package/dist/search.d.ts +9 -0
  59. package/dist/search.js +41 -0
  60. package/dist/social.d.ts +19 -0
  61. package/dist/social.js +35 -0
  62. package/dist/use-quote-gesture.d.ts +24 -0
  63. package/dist/use-quote-gesture.js +127 -0
  64. package/dist/visual/RobonoIcon.d.ts +232 -0
  65. package/dist/visual/RobonoIcon.js +21 -0
  66. package/dist/visual/icon-nodes.d.ts +3 -0
  67. package/dist/visual/icon-nodes.js +2 -0
  68. package/dist/visual/icon-paths.d.ts +221 -0
  69. package/dist/visual/icon-paths.js +221 -0
  70. package/dist/voice-composer.d.ts +37 -0
  71. package/dist/voice-composer.js +20 -0
  72. package/licenses/robono-icons-lucide-license.txt +43 -0
  73. package/licenses/robono-icons-weather-attribution.txt +1 -0
  74. package/licenses/robono-icons-weather-license.txt +21 -0
  75. package/package.json +126 -4
  76. package/plugin/preserve-call-audio.cjs +22 -0
@@ -0,0 +1,7 @@
1
+ import React from "react";
2
+ import { type RobonoIconProps } from "./visual/RobonoIcon.js";
3
+ export declare function MessageIcon(props: RobonoIconProps): React.JSX.Element;
4
+ export declare function duration(ms?: number): string;
5
+ export declare function fileSize(bytes?: number): string;
6
+ /** Neutral app chrome stays legible for light and dark customer palettes. */
7
+ export declare function chatSurface(background: string): "#1e262b" | "#ffffff";
package/dist/icons.js ADDED
@@ -0,0 +1,10 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { RobonoIcon } from "./visual/RobonoIcon.js";
3
+ export function MessageIcon(props) { return _jsx(RobonoIcon, { ...props }); }
4
+ export function duration(ms = 0) { const seconds = Math.ceil(ms / 1000); return `${Math.floor(seconds / 60)}:${String(seconds % 60).padStart(2, '0')}`; }
5
+ export function fileSize(bytes = 0) { return bytes >= 1048576 ? `${(bytes / 1048576).toFixed(1)} MB` : bytes >= 1024 ? `${Math.ceil(bytes / 1024)} KB` : `${bytes} B`; }
6
+ /** Neutral app chrome stays legible for light and dark customer palettes. */
7
+ export function chatSurface(background) {
8
+ const rgb = background.slice(1).match(/../g)?.map(v => parseInt(v, 16)) ?? [255, 255, 255];
9
+ return rgb[0] * 0.299 + rgb[1] * 0.587 + rgb[2] * 0.114 < 128 ? "#1e262b" : "#ffffff";
10
+ }
@@ -0,0 +1,16 @@
1
+ import React from 'react';
2
+ import { type MessagingAppearance, type MessagingInboxClient, type ConversationSummary } from '@robono/messaging';
3
+ export interface MessagingInboxProps {
4
+ client: MessagingInboxClient;
5
+ userId: string;
6
+ contacts?: Record<string, {
7
+ name: string;
8
+ avatarUrl?: string;
9
+ }>;
10
+ appearance?: MessagingAppearance;
11
+ active?: boolean;
12
+ onSelect: (conversation: ConversationSummary) => void;
13
+ onDeleted?: (conversationId: string) => Promise<void> | void;
14
+ }
15
+ /** Compact Messenger chat rows: avatar, name, last message, time and unread badge. */
16
+ export declare function MessagingInbox({ client, userId, contacts, appearance, active, onSelect, onDeleted }: MessagingInboxProps): React.JSX.Element;
package/dist/inbox.js ADDED
@@ -0,0 +1,48 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useEffect, useState, useSyncExternalStore } from 'react';
3
+ import { AppState, FlatList, Image, Modal, Platform, Pressable, Text, View, useWindowDimensions } from 'react-native';
4
+ import { defaultMessagingSettings } from '@robono/messaging';
5
+ import { MessagingPopup } from './popup.js';
6
+ /** Compact Messenger chat rows: avatar, name, last message, time and unread badge. */
7
+ export function MessagingInbox({ client, userId, contacts = {}, appearance, active = true, onSelect, onDeleted }) {
8
+ const state = useSyncExternalStore(client.subscribe, client.getSnapshot, client.getSnapshot), design = appearance ?? state.appearance ?? defaultMessagingSettings().appearance, c = design.colors;
9
+ useEffect(() => { const update = (value) => { if (active && (value === null || value === 'active'))
10
+ client.start();
11
+ else
12
+ client.stop(); }; update(AppState.currentState); const sub = AppState.addEventListener('change', update); return () => { sub.remove(); client.stop(); }; }, [client, active]);
13
+ const [selected, setSelected] = useState(null), [confirm, setConfirm] = useState(false), [busy, setBusy] = useState(false), [error, setError] = useState(''), [anchor, setAnchor] = useState({ x: 8, y: 8 });
14
+ const window = useWindowDimensions();
15
+ const title = (row) => row.kind === 'group' ? row.name ?? 'Group' : contacts[row.contactId ?? '']?.name ?? row.contactId ?? 'Chat';
16
+ const openOptions = (row, x, y) => { if (!state.inboxActionsAvailable)
17
+ return; setSelected(row); setAnchor({ x: Math.max(8, Math.min(x, window.width - 288)), y: Math.max(8, Math.min(y, window.height - 200)) }); setConfirm(false); setError(''); };
18
+ useEffect(() => { if (selected && !state.conversations.some(c => c.id === selected.id) && !busy && !error)
19
+ setSelected(null); }, [state.conversations, busy, selected, error]);
20
+ async function act(remove) { if (!selected || busy)
21
+ return; setBusy(true); setError(''); try {
22
+ if (remove) {
23
+ await client.deleteConversation(selected);
24
+ await onDeleted?.(selected.id);
25
+ }
26
+ else
27
+ await client.setMuted(selected.id, !selected.muted);
28
+ setSelected(null);
29
+ setConfirm(false);
30
+ }
31
+ catch (e) {
32
+ setError(e instanceof Error ? e.message : 'Chat options could not be saved.');
33
+ }
34
+ finally {
35
+ setBusy(false);
36
+ } }
37
+ const preview = (row) => { const m = row.lastMessage; if (!m)
38
+ return 'No messages yet'; const text = m.kind === 'text' ? m.text ?? '' : ({ voice: 'Voice message', image: 'Photo', video: 'Video', document: 'Document', poll: 'Poll' }[m.kind] ?? 'Message'); return (m.senderId === userId ? 'You: ' : '') + text; };
39
+ return _jsxs(View, { style: { flex: 1, minHeight: 0, backgroundColor: c.background }, children: [_jsx(Text, { accessibilityRole: "header", style: { color: c.foreground, fontSize: 20, fontWeight: '700', padding: 12, borderBottomWidth: 1, borderBottomColor: c.foreground + '12' }, children: "Chats" }), state.error && _jsxs(View, { style: { padding: 12 }, children: [_jsx(Text, { accessibilityRole: "alert", style: { color: c.foreground }, children: state.error }), _jsx(Pressable, { accessibilityRole: "button", onPress: () => void client.refresh().catch(() => { }), children: _jsx(Text, { style: { color: c.accent, paddingVertical: 8 }, children: "Try again" }) })] }), _jsx(FlatList, { data: state.conversations, keyExtractor: row => row.id, contentContainerStyle: { flexGrow: 1 }, ItemSeparatorComponent: () => _jsx(View, { style: { height: 1, backgroundColor: c.foreground + '12', marginLeft: 64 } }), ListEmptyComponent: _jsx(Text, { style: { padding: 20, textAlign: 'center', color: c.secondary }, children: state.loading ? 'Loading chats…' : state.error ? '' : 'No chats yet' }), ListFooterComponent: state.hasMore ? _jsx(Pressable, { accessibilityRole: "button", disabled: state.loading, onPress: () => void client.loadMore().catch(() => { }), children: _jsx(Text, { style: { padding: 12, textAlign: 'center', color: c.accent }, children: state.loading ? 'Loading…' : 'More chats' }) }) : null, renderItem: ({ item }) => {
40
+ const person = item.contactId ? contacts[item.contactId] : undefined, title = item.kind === 'group' ? item.name ?? 'Group' : person?.name ?? item.contactId ?? 'Chat';
41
+ const date = item.lastMessage ? new Date(item.lastMessage.createdAt) : null;
42
+ return _jsxs(Pressable, { accessibilityRole: "button", accessibilityLabel: `${title}${item.unreadCount ? `, ${item.unreadCount} unread` : ''}`, onPress: () => onSelect(item), onLongPress: e => openOptions(item, e.nativeEvent.pageX, e.nativeEvent.pageY), accessibilityActions: state.inboxActionsAvailable ? [{ name: 'showOptions', label: 'Chat options' }] : [], onAccessibilityAction: e => { if (e.nativeEvent.actionName === 'showOptions')
43
+ openOptions(item, window.width / 2 - 140, window.height / 2 - 100); }, ...(Platform.OS === 'web' ? { onContextMenu: (e) => { e.preventDefault(); openOptions(item, e.clientX, e.clientY); } } : {}), style: { flexDirection: 'row', alignItems: 'center', gap: 12, paddingHorizontal: 12, paddingVertical: 10, minHeight: 68 }, children: [person?.avatarUrl ? _jsx(Image, { source: { uri: person.avatarUrl }, style: { width: 40, height: 40, borderRadius: 20 } }) : _jsx(View, { style: { width: 40, height: 40, borderRadius: 20, backgroundColor: c.accent + '20', alignItems: 'center', justifyContent: 'center' }, children: _jsx(Text, { style: { color: c.accent, fontSize: 18, fontWeight: '700' }, children: title.slice(0, 1).toUpperCase() }) }), _jsxs(View, { style: { flex: 1, minWidth: 0 }, children: [_jsxs(Text, { numberOfLines: 1, style: { color: c.foreground, fontSize: 16, fontWeight: '700' }, children: [title, item.muted ? ' · Muted' : ''] }), _jsx(Text, { numberOfLines: 1, style: { color: c.secondary, marginTop: 3, fontSize: 14 }, children: preview(item) })] }), _jsxs(View, { style: { alignItems: 'flex-end', gap: 4 }, children: [_jsx(Text, { style: { color: item.unreadCount ? c.accent : c.secondary, fontSize: 12 }, children: date ? date.toDateString() === new Date().toDateString() ? date.toLocaleTimeString(undefined, { hour: 'numeric', minute: '2-digit' }) : date.toLocaleDateString(undefined, { month: 'short', day: 'numeric' }) : '' }), item.unreadCount > 0 && _jsx(View, { style: { minWidth: 20, height: 20, borderRadius: 10, borderWidth: 1.5, borderColor: c.accent, alignItems: 'center', justifyContent: 'center', paddingHorizontal: 5 }, children: _jsx(Text, { style: { color: c.accent, fontSize: 10, fontWeight: '900' }, children: item.unreadCount > 99 ? '99+' : item.unreadCount }) })] })] });
44
+ } }), selected && !confirm && _jsx(Modal, { visible: true, transparent: true, animationType: "fade", onRequestClose: () => { if (!busy)
45
+ setSelected(null); }, children: _jsx(Pressable, { accessibilityLabel: "Close chat options", onPress: () => { if (!busy)
46
+ setSelected(null); }, style: { flex: 1, backgroundColor: '#0004' }, children: _jsxs(View, { onStartShouldSetResponder: () => true, style: { position: 'absolute', left: anchor.x, top: anchor.y, width: 280, borderRadius: 18, padding: 8, backgroundColor: c.background }, children: [_jsx(Pressable, { accessibilityRole: "button", disabled: busy, onPress: () => void act(false), style: { padding: 16 }, children: _jsx(Text, { style: { color: c.foreground }, children: selected.muted ? 'Unmute' : 'Mute' }) }), _jsx(Pressable, { accessibilityRole: "button", disabled: busy, onPress: () => setConfirm(true), style: { padding: 16 }, children: _jsx(Text, { style: { color: c.foreground }, children: "Delete chat" }) }), _jsx(Pressable, { accessibilityRole: "button", disabled: busy, onPress: () => setSelected(null), style: { padding: 16 }, children: _jsx(Text, { style: { color: c.secondary }, children: "Cancel" }) }), !!error && _jsx(Text, { accessibilityRole: "alert", style: { color: c.foreground, padding: 12 }, children: error })] }) }) }), selected && confirm && _jsxs(MessagingPopup, { background: c.background, onClose: () => { if (!busy)
47
+ setConfirm(false); }, children: [_jsxs(Text, { accessibilityRole: "header", style: { color: c.foreground, fontSize: 17, fontWeight: '600' }, children: ["Delete chat with ", title(selected), "?"] }), _jsx(Text, { style: { color: c.secondary, marginTop: 8 }, children: "This removes the chat and its history from your view. Other people keep their copy. New messages can bring the chat back." }), !!error && _jsx(Text, { accessibilityRole: "alert", style: { color: c.foreground, marginTop: 8 }, children: error }), _jsxs(View, { style: { flexDirection: 'row', justifyContent: 'flex-end', gap: 20, marginTop: 12 }, children: [_jsx(Pressable, { accessibilityRole: "button", disabled: busy, onPress: () => setConfirm(false), style: { minHeight: 44, justifyContent: 'center' }, children: _jsx(Text, { style: { color: c.foreground }, children: "Cancel" }) }), _jsx(Pressable, { accessibilityRole: "button", accessibilityLabel: "Confirm delete chat", disabled: busy, onPress: () => void act(true), style: { minHeight: 44, justifyContent: 'center' }, children: _jsx(Text, { style: { color: c.accent }, children: "Delete" }) })] })] })] });
48
+ }
@@ -0,0 +1,144 @@
1
+ import { type SocialPlayerProps } from './social.js';
2
+ export type { SocialPlayerProps } from './social.js';
3
+ export { MessagingSearch } from './search.js';
4
+ export { MessagingSafety } from './safety.js';
5
+ export { PollMessage, PollComposer } from "./polls.js";
6
+ import React, { type ComponentType, type ReactNode } from "react";
7
+ import { type StyleProp, type ViewStyle } from "react-native";
8
+ import { type VoiceComposerProps } from "./voice-composer.js";
9
+ export { VoiceMessageComposer, VoiceDraftPlayback } from "./voice-composer.js";
10
+ export type { VoiceComposerProps } from "./voice-composer.js";
11
+ import { type VoiceBubbleProps } from "./media-ui.js";
12
+ export { VoicePlayback, MediaViewer, MediaPlaybackControls, ZoomablePhoto } from "./media-ui.js";
13
+ export type { VoiceBubbleProps } from "./media-ui.js";
14
+ import { type MessageKind, type MessageMedia, type MessagingAppearance, type MessagingClient, type MessagingMessage, type MessagingState, type MessageTransform, type SavedMessage } from "@robono/messaging";
15
+ export interface VoiceAttachmentProps {
16
+ media: MessageMedia;
17
+ appearance: MessagingAppearance;
18
+ disabled: boolean;
19
+ onRemove: () => void;
20
+ }
21
+ export interface TranslationVoiceProps {
22
+ transform: MessageTransform;
23
+ ink: string;
24
+ accent: string;
25
+ showWaveform?: boolean;
26
+ }
27
+ export interface MessagingComponents {
28
+ SocialPlayer?: ComponentType<SocialPlayerProps>;
29
+ TranslationVoice?: ComponentType<TranslationVoiceProps>;
30
+ VoiceAttachment?: ComponentType<VoiceAttachmentProps>;
31
+ Header?: ComponentType;
32
+ Message?: ComponentType<{
33
+ message: MessagingMessage;
34
+ }>;
35
+ /** Optional inline audio adapter. The shared VoicePlayback supplies all visible controls. */
36
+ VoiceMessage?: ComponentType<VoiceBubbleProps>;
37
+ VoiceComposer?: ComponentType<VoiceComposerProps>;
38
+ List?: ComponentType;
39
+ Composer?: ComponentType;
40
+ }
41
+ /** Assets supplied by the host's authorized media library; choosing one adds a pending attachment. */
42
+ export interface AttachmentChoice {
43
+ id: string;
44
+ kind: 'image' | 'video' | 'document';
45
+ name: string;
46
+ thumbnailUri?: string;
47
+ durationMs?: number;
48
+ }
49
+ export interface MessagingScreenProps {
50
+ client: MessagingClient;
51
+ /** Browser adapter supplies the hosting origin; native embeds use Robono's domain. */
52
+ socialOrigin?: string;
53
+ /** Optional self-hosted copy of messaging-embed.html, for strict website policies. */
54
+ socialEmbedPageUrl?: string;
55
+ /** Target language for this viewer; defaults to the device locale. */
56
+ language?: string;
57
+ userId: string;
58
+ contact: {
59
+ name: string;
60
+ avatarUrl?: string;
61
+ };
62
+ /** Resolve group member names through your app's authorized contact directory. */
63
+ participants?: Record<string, {
64
+ name: string;
65
+ avatarUrl?: string;
66
+ }>;
67
+ appearance?: MessagingAppearance;
68
+ components?: MessagingComponents;
69
+ style?: StyleProp<ViewStyle>;
70
+ /** Set false when this conversation is covered by another route. */
71
+ active?: boolean;
72
+ onBack?: () => void;
73
+ /** Optional override for the supplied contact safety menu; use your existing safety flow. */
74
+ onContactOptions?: () => void;
75
+ onMessageOptions?: (message: MessagingMessage) => void;
76
+ /** Used by the media preview to open the same message actions. */
77
+ requestedMessageAction?: {
78
+ kind: 'forward' | 'delete';
79
+ messageId: string;
80
+ sequence: number;
81
+ };
82
+ /** Native clipboard adapter. Supplied by ExpoMessagingScreen. Web uses its clipboard. */
83
+ copyText?: (text: string) => Promise<void>;
84
+ /** Optional recent media shown beneath the attachment buttons. */
85
+ attachmentChoices?: AttachmentChoice[];
86
+ /** Pick, capture or record, then upload to your private store. Return an opaque asset reference. */
87
+ pickAttachment?: (kind: Exclude<MessageKind, "text" | "poll">, options: {
88
+ maxDurationMs?: number;
89
+ maxFileBytes?: number;
90
+ allowedMimeTypes?: string[];
91
+ source?: "camera" | "call-video";
92
+ attachmentId?: string;
93
+ intent?: "send" | "preview" | "attach";
94
+ }) => Promise<MessageMedia | null>;
95
+ /** Release local draft resources after removal or submission; keep uploaded assets needed by retries. */
96
+ releaseAttachment?: (media: MessageMedia) => void | Promise<void>;
97
+ /** Upload a locally selected attachment after its outgoing bubble is visible. */
98
+ prepareAttachment?: (kind: Exclude<MessageKind, 'text' | 'poll'>, media: MessageMedia) => Promise<MessageMedia>;
99
+ retainDraftAttachments?: boolean;
100
+ /** Resolve an authorized thumbnail and/or normalized waveform. Never put signed URLs in message data. */
101
+ attachmentPreview?: (message: MessagingMessage) => Promise<{
102
+ uri?: string;
103
+ waveform?: number[];
104
+ }>;
105
+ /** Authorize the asset and open it using your app's media player/viewer. */
106
+ openAttachment?: (message: MessagingMessage) => Promise<void>;
107
+ openSavedAttachment?: (record: SavedMessage) => Promise<void>;
108
+ }
109
+ interface MessagingContext extends MessagingScreenProps {
110
+ state: MessagingState;
111
+ design: MessagingAppearance;
112
+ reply: MessagingMessage | null;
113
+ editing: MessagingMessage | null;
114
+ setEditing: (message: MessagingMessage | null) => void;
115
+ setForwarding: (messages: MessagingMessage[]) => void;
116
+ setDeleting: (messages: MessagingMessage[]) => void;
117
+ selection: MessagingMessage[];
118
+ setSelection: (messages: MessagingMessage[]) => void;
119
+ saved: SavedMessage[];
120
+ saving: boolean;
121
+ toggleSaved: (message: MessagingMessage) => Promise<void>;
122
+ setSavedOpen: (open: boolean) => void;
123
+ setReply: (message: MessagingMessage | null) => void;
124
+ }
125
+ export declare function useMessagingUI(): MessagingContext;
126
+ export declare function MessagingProvider({ children, ...props }: MessagingScreenProps & {
127
+ children: ReactNode;
128
+ }): React.JSX.Element;
129
+ export declare function messagingHeaderColors(design: MessagingAppearance, group: boolean): {
130
+ background: string;
131
+ foreground: string;
132
+ };
133
+ export declare function MessagingHeader({ children }?: {
134
+ children?: ReactNode;
135
+ }): React.JSX.Element;
136
+ export declare function MessagingBubble({ message, pendingStatus }: {
137
+ message: MessagingMessage;
138
+ pendingStatus?: 'sending' | 'failed';
139
+ }): React.JSX.Element;
140
+ export declare function MessagingList(): React.JSX.Element;
141
+ export declare function MessagingComposer(): React.JSX.Element;
142
+ /** Embed inside your app's safe area and navigation. No engine, push or global audio side effects. */
143
+ export declare function MessagingScreen(props: MessagingScreenProps): React.JSX.Element;
144
+ export { MessagingInbox, type MessagingInboxProps } from './inbox.js';