@experiaapp/webchat-react-native 2.0.1

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 (101) hide show
  1. package/README.md +254 -0
  2. package/app.plugin.js +6 -0
  3. package/lib/adapters/audio.d.ts +74 -0
  4. package/lib/adapters/audio.js +39 -0
  5. package/lib/adapters/audioRoute.d.ts +57 -0
  6. package/lib/adapters/audioRoute.js +77 -0
  7. package/lib/adapters/expoDefaults.d.ts +77 -0
  8. package/lib/adapters/expoDefaults.js +539 -0
  9. package/lib/adapters/picker.d.ts +67 -0
  10. package/lib/adapters/picker.js +37 -0
  11. package/lib/adapters/webrtc.d.ts +131 -0
  12. package/lib/adapters/webrtc.js +70 -0
  13. package/lib/core/VideoCallClient.d.ts +106 -0
  14. package/lib/core/VideoCallClient.js +302 -0
  15. package/lib/core/WebchatClient.d.ts +34 -0
  16. package/lib/core/WebchatClient.js +132 -0
  17. package/lib/core/configClient.d.ts +42 -0
  18. package/lib/core/configClient.js +302 -0
  19. package/lib/core/greet.d.ts +11 -0
  20. package/lib/core/greet.js +17 -0
  21. package/lib/core/ice.d.ts +31 -0
  22. package/lib/core/ice.js +48 -0
  23. package/lib/core/linkify.d.ts +11 -0
  24. package/lib/core/linkify.js +25 -0
  25. package/lib/core/logger.d.ts +17 -0
  26. package/lib/core/logger.js +53 -0
  27. package/lib/core/media.d.ts +52 -0
  28. package/lib/core/media.js +115 -0
  29. package/lib/core/mediaType.d.ts +21 -0
  30. package/lib/core/mediaType.js +66 -0
  31. package/lib/core/messagesReducer.d.ts +36 -0
  32. package/lib/core/messagesReducer.js +58 -0
  33. package/lib/core/persistence.d.ts +45 -0
  34. package/lib/core/persistence.js +63 -0
  35. package/lib/core/socketFactory.d.ts +16 -0
  36. package/lib/core/socketFactory.js +82 -0
  37. package/lib/core/types.d.ts +320 -0
  38. package/lib/core/types.js +30 -0
  39. package/lib/core/unread.d.ts +2 -0
  40. package/lib/core/unread.js +5 -0
  41. package/lib/i18n/ar.json +1 -0
  42. package/lib/i18n/en.json +1 -0
  43. package/lib/i18n/index.d.ts +7 -0
  44. package/lib/i18n/index.js +43 -0
  45. package/lib/index.d.ts +59 -0
  46. package/lib/index.js +142 -0
  47. package/lib/plugin/withWebchat.d.ts +53 -0
  48. package/lib/plugin/withWebchat.js +164 -0
  49. package/lib/state/WebchatProvider.d.ts +132 -0
  50. package/lib/state/WebchatProvider.js +906 -0
  51. package/lib/state/useWebchat.d.ts +1 -0
  52. package/lib/state/useWebchat.js +12 -0
  53. package/lib/theme/dir.d.ts +14 -0
  54. package/lib/theme/dir.js +20 -0
  55. package/lib/theme/themeFactory.d.ts +219 -0
  56. package/lib/theme/themeFactory.js +182 -0
  57. package/lib/ui/AttachButton.d.ts +35 -0
  58. package/lib/ui/AttachButton.js +26 -0
  59. package/lib/ui/AudioRecorder.d.ts +25 -0
  60. package/lib/ui/AudioRecorder.js +228 -0
  61. package/lib/ui/Bubble.d.ts +1 -0
  62. package/lib/ui/Bubble.js +265 -0
  63. package/lib/ui/CallControls.d.ts +27 -0
  64. package/lib/ui/CallControls.js +92 -0
  65. package/lib/ui/CallPlaceholder.d.ts +16 -0
  66. package/lib/ui/CallPlaceholder.js +73 -0
  67. package/lib/ui/Composer.d.ts +5 -0
  68. package/lib/ui/Composer.js +272 -0
  69. package/lib/ui/FileTile.d.ts +9 -0
  70. package/lib/ui/FileTile.js +31 -0
  71. package/lib/ui/Header.d.ts +52 -0
  72. package/lib/ui/Header.js +236 -0
  73. package/lib/ui/Icon.d.ts +21 -0
  74. package/lib/ui/Icon.js +110 -0
  75. package/lib/ui/ImageBubble.d.ts +11 -0
  76. package/lib/ui/ImageBubble.js +16 -0
  77. package/lib/ui/MediaUploadMenu.d.ts +23 -0
  78. package/lib/ui/MediaUploadMenu.js +68 -0
  79. package/lib/ui/MessageList.d.ts +1 -0
  80. package/lib/ui/MessageList.js +46 -0
  81. package/lib/ui/PoweredBy.d.ts +8 -0
  82. package/lib/ui/PoweredBy.js +14 -0
  83. package/lib/ui/PrechatForm.d.ts +1 -0
  84. package/lib/ui/PrechatForm.js +230 -0
  85. package/lib/ui/QuickReplies.d.ts +1 -0
  86. package/lib/ui/QuickReplies.js +24 -0
  87. package/lib/ui/TypingIndicator.d.ts +9 -0
  88. package/lib/ui/TypingIndicator.js +88 -0
  89. package/lib/ui/VideoBubble.d.ts +10 -0
  90. package/lib/ui/VideoBubble.js +130 -0
  91. package/lib/ui/VideoCall.d.ts +34 -0
  92. package/lib/ui/VideoCall.js +191 -0
  93. package/lib/ui/VideoTile.d.ts +25 -0
  94. package/lib/ui/VideoTile.js +13 -0
  95. package/lib/ui/VoiceMessage.d.ts +19 -0
  96. package/lib/ui/VoiceMessage.js +127 -0
  97. package/lib/ui/WebChat.d.ts +10 -0
  98. package/lib/ui/WebChat.js +386 -0
  99. package/lib/ui/openLink.d.ts +1 -0
  100. package/lib/ui/openLink.js +16 -0
  101. package/package.json +94 -0
@@ -0,0 +1,73 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.CallPlaceholder = CallPlaceholder;
4
+ const jsx_runtime_1 = require("react/jsx-runtime");
5
+ const react_native_1 = require("react-native");
6
+ const Icon_1 = require("./Icon");
7
+ const i18n_1 = require("../i18n");
8
+ /** First letter of the name for the avatar fallback (latin + arabic safe). */
9
+ function initialOf(name) {
10
+ const n = (name !== null && name !== void 0 ? name : "").trim();
11
+ return n ? n[0].toUpperCase() : "";
12
+ }
13
+ function CallPlaceholder({ variant, name, avatarUrl, theme, language, style, testID, }) {
14
+ var _a, _b;
15
+ const t = (0, i18n_1.makeT)(language);
16
+ const primary = (_b = (_a = theme === null || theme === void 0 ? void 0 : theme.colors) === null || _a === void 0 ? void 0 : _a.primary) !== null && _b !== void 0 ? _b : "#2D6CDF";
17
+ if (variant === "cameraOff") {
18
+ return ((0, jsx_runtime_1.jsxs)(react_native_1.View, { testID: testID !== null && testID !== void 0 ? testID : "call-placeholder-camera-off", accessible: true, accessibilityLabel: t("camera_off_status"), style: [
19
+ {
20
+ backgroundColor: "#1C1C1E",
21
+ alignItems: "center",
22
+ justifyContent: "center",
23
+ overflow: "hidden",
24
+ },
25
+ style,
26
+ ], children: [(0, jsx_runtime_1.jsx)(react_native_1.View, { style: {
27
+ width: 46,
28
+ height: 46,
29
+ borderRadius: 23,
30
+ backgroundColor: "rgba(255,255,255,0.12)",
31
+ alignItems: "center",
32
+ justifyContent: "center",
33
+ }, children: (0, jsx_runtime_1.jsx)(Icon_1.Icon, { name: "videocamOff", color: "#FFFFFF", size: 22 }) }), (0, jsx_runtime_1.jsx)(react_native_1.Text, { style: { color: "rgba(255,255,255,0.7)", fontSize: 11, marginTop: 8 }, children: t("you") })] }));
34
+ }
35
+ // variant === "waiting"
36
+ const displayName = (name === null || name === void 0 ? void 0 : name.trim()) || t("agent");
37
+ const initial = initialOf(name);
38
+ return ((0, jsx_runtime_1.jsxs)(react_native_1.View, { testID: testID !== null && testID !== void 0 ? testID : "call-placeholder-waiting", accessible: true, accessibilityLabel: `${displayName} — ${t("connecting")}`, style: [
39
+ {
40
+ backgroundColor: "#0B0C0F", // off-black: a touch of depth vs the pure-black video
41
+ alignItems: "center",
42
+ justifyContent: "center",
43
+ },
44
+ style,
45
+ ], children: [(0, jsx_runtime_1.jsxs)(react_native_1.View, { style: {
46
+ alignItems: "center",
47
+ justifyContent: "center",
48
+ width: 132,
49
+ height: 132,
50
+ }, children: [(0, jsx_runtime_1.jsx)(react_native_1.View, { style: {
51
+ position: "absolute",
52
+ width: 132,
53
+ height: 132,
54
+ borderRadius: 66,
55
+ borderWidth: 1,
56
+ borderColor: "rgba(255,255,255,0.06)",
57
+ } }), (0, jsx_runtime_1.jsx)(react_native_1.View, { style: {
58
+ position: "absolute",
59
+ width: 112,
60
+ height: 112,
61
+ borderRadius: 56,
62
+ borderWidth: 1,
63
+ borderColor: "rgba(255,255,255,0.10)",
64
+ } }), avatarUrl ? ((0, jsx_runtime_1.jsx)(react_native_1.Image, { testID: "call-placeholder-avatar", accessibilityRole: "image", source: { uri: avatarUrl }, resizeMode: "cover", style: { width: 92, height: 92, borderRadius: 46 } })) : ((0, jsx_runtime_1.jsx)(react_native_1.View, { testID: "call-placeholder-avatar", style: {
65
+ width: 92,
66
+ height: 92,
67
+ borderRadius: 46,
68
+ backgroundColor: primary,
69
+ alignItems: "center",
70
+ justifyContent: "center",
71
+ }, children: (0, jsx_runtime_1.jsx)(react_native_1.Text, { style: { color: "#FFFFFF", fontSize: 38, fontWeight: "600" }, children: initial }) }))] }), (0, jsx_runtime_1.jsx)(react_native_1.Text, { style: { color: "#FFFFFF", fontSize: 20, fontWeight: "600", marginTop: 20 }, children: displayName }), (0, jsx_runtime_1.jsx)(react_native_1.Text, { style: { color: "rgba(255,255,255,0.6)", fontSize: 14, marginTop: 6 }, children: t("connecting") })] }));
72
+ }
73
+ exports.default = CallPlaceholder;
@@ -0,0 +1,5 @@
1
+ export declare function Composer({ onSend, onAttachPress, // legacy single-shot trigger (back-compat; used only when no onPick* props)
2
+ onPickImages, // open OS image library → attach()
3
+ onPickVideos, // open OS video library → attach()
4
+ onPickDocuments, // open OS document picker → attach()
5
+ onAttach, dir, theme, disabledInput, audioAdapter, language, emojiEnabled, placeholder, }: any): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,272 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.Composer = Composer;
4
+ const jsx_runtime_1 = require("react/jsx-runtime");
5
+ // src/ui/Composer.tsx
6
+ const react_1 = require("react");
7
+ const react_native_1 = require("react-native");
8
+ const dir_1 = require("../theme/dir");
9
+ const AttachButton_1 = require("./AttachButton");
10
+ const AudioRecorder_1 = require("./AudioRecorder");
11
+ // Bundled i18n: a tiny, language-keyed lookup (makeT) that reads the plain JSON
12
+ // strings without pulling in any native peer (react-native-localize). It is the
13
+ // shared helper used across every prop-driven UI component, so it stays
14
+ // test-safe and works before initI18n has run (audit #8).
15
+ const i18n_1 = require("../i18n");
16
+ const Icon_1 = require("./Icon");
17
+ const MediaUploadMenu_1 = require("./MediaUploadMenu");
18
+ const themeFactory_1 = require("../theme/themeFactory");
19
+ // A small built-in set of common emojis — no extra npm dependency required.
20
+ const EMOJIS = ["😀", "😂", "👍", "🙏", "❤️", "🎉", "😢", "🔥"];
21
+ // Web parity (DigitalGovernmentFooter): the composer reads the server-driven
22
+ // `send` token group (web `sendSectionStyle`). The unit tests pass a minimal
23
+ // theme with only { colors, font }, so every token is read defensively with the
24
+ // same defaults buildTheme() would produce.
25
+ function sendTokens(theme) {
26
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r;
27
+ const s = (_a = theme === null || theme === void 0 ? void 0 : theme.send) !== null && _a !== void 0 ? _a : {};
28
+ const primary = (_c = (_b = theme === null || theme === void 0 ? void 0 : theme.colors) === null || _b === void 0 ? void 0 : _b.primary) !== null && _c !== void 0 ? _c : "#1B8354";
29
+ // back-compat folded icon color — used as the per-icon fallback so a minimal
30
+ // theme ({colors, font} only, as the unit tests pass) keeps a single tint.
31
+ const iconColor = (_d = s.iconColor) !== null && _d !== void 0 ? _d : primary;
32
+ return {
33
+ fieldBg: (_e = s.fieldBg) !== null && _e !== void 0 ? _e : "#F3F4F6",
34
+ fieldColor: (_h = (_f = s.fieldColor) !== null && _f !== void 0 ? _f : (_g = theme === null || theme === void 0 ? void 0 : theme.colors) === null || _g === void 0 ? void 0 : _g.text) !== null && _h !== void 0 ? _h : "#2d2d2d",
35
+ fieldRadius: (_j = s.fieldRadius) !== null && _j !== void 0 ? _j : 4,
36
+ hintColor: (_k = s.hintColor) !== null && _k !== void 0 ? _k : "#6C737F",
37
+ // hint typography (web hintFontSize / hintFontWeight). RN's TextInput has no
38
+ // separate placeholder font, so the field text itself carries these — the
39
+ // placeholder inherits the field's fontSize/fontWeight. undefined => inherit
40
+ // the global font size / weight (set below on the input style).
41
+ hintFontSize: s.hintFontSize,
42
+ hintFontWeight: s.hintFontWeight,
43
+ sendBtnBg: (_l = s.sendBtnBg) !== null && _l !== void 0 ? _l : primary,
44
+ sendBtnColor: (_m = s.sendBtnColor) !== null && _m !== void 0 ? _m : "#FFFFFF",
45
+ sendBtnRadius: (_o = s.sendBtnRadius) !== null && _o !== void 0 ? _o : 4,
46
+ iconColor,
47
+ // independent icon tints (web uploadFileBtnColor / emojiBtnColor / audioBtnColor).
48
+ // Each falls back to the folded iconColor so legacy single-color themes are unchanged.
49
+ uploadIconColor: (_p = s.uploadIconColor) !== null && _p !== void 0 ? _p : iconColor,
50
+ emojiIconColor: (_q = s.emojiIconColor) !== null && _q !== void 0 ? _q : iconColor,
51
+ audioIconColor: (_r = s.audioIconColor) !== null && _r !== void 0 ? _r : iconColor,
52
+ // attach-button background + radius (web uploadFileBtnBg / uploadFileBtnCornerRadius).
53
+ uploadBtnBg: s.uploadBtnBg,
54
+ uploadBtnRadius: s.uploadBtnRadius,
55
+ };
56
+ }
57
+ // `onAttachPress` (when provided) wires the 📎 AttachButton: pressing it runs the
58
+ // host's picker→attach() flow (see WebChat Surface). Omitted in pure-text contexts.
59
+ //
60
+ // `disabledInput` (audit #disabledInput) hard-disables the input + send (and the
61
+ // emoji/attach/record affordances) — e.g. while a call holds the mic, or the host
62
+ // suspends sending.
63
+ //
64
+ // `audioAdapter` (audit #12 voice) threads an injected capture surface: when present
65
+ // the composer renders <AudioRecorder> and routes the completed RecordingResult through
66
+ // the provider's attach() (handed down as `onAttach` — useWebchat().attach in the live
67
+ // Surface) as a single media entry (web parity — voice rides the same base64-in-socket
68
+ // media[] path as picked files). `onAttach`/`language` are props, not a context read, so
69
+ // a provider-less Composer (the pure-text unit tests) renders without throwing.
70
+ function Composer({ onSend, onAttachPress, // legacy single-shot trigger (back-compat; used only when no onPick* props)
71
+ onPickImages, // open OS image library → attach()
72
+ onPickVideos, // open OS video library → attach()
73
+ onPickDocuments, // open OS document picker → attach()
74
+ onAttach, dir, theme, disabledInput = false, audioAdapter, language,
75
+ // web `config.emoji` — gates the emoji affordance. Default true so legacy
76
+ // callers (and the existing unit tests) keep the emoji button; the live
77
+ // Surface passes the resolved flag (web default false) through.
78
+ emojiEnabled = true,
79
+ // web `config.inputTextFieldHint` — composer placeholder override; falls back
80
+ // to the localized t("type_message") when absent.
81
+ placeholder, }) {
82
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m;
83
+ const [value, setValue] = (0, react_1.useState)("");
84
+ const [pickerOpen, setPickerOpen] = (0, react_1.useState)(false);
85
+ const [mediaMenuOpen, setMediaMenuOpen] = (0, react_1.useState)(false);
86
+ const [recordingArmed, setRecordingArmed] = (0, react_1.useState)(false);
87
+ // Web parity (handleSetFileValue/MultiFileDisplay): picked/recorded media is
88
+ // STAGED here and previewed; it sends only on the Send tap, not on pick.
89
+ const [pending, setPending] = (0, react_1.useState)([]);
90
+ const d = (0, dir_1.dirStyles)(dir);
91
+ const t = (0, i18n_1.makeT)(language);
92
+ const s = sendTokens(theme);
93
+ // Placeholder: the tenant override (config.inputTextFieldHint) wins; otherwise
94
+ // the localized default. Treat an empty string as "unset" so it never blanks.
95
+ const placeholderText = placeholder && String(placeholder).length > 0 ? placeholder : t("type_message");
96
+ // Field/placeholder typography (web defaultFontWeight + hintFont*). The input
97
+ // text size/weight default to the global font; the placeholder inherits them
98
+ // (RN has no separate placeholder font), with the hint* tokens overriding.
99
+ const baseFontSize = (_b = (_a = theme === null || theme === void 0 ? void 0 : theme.font) === null || _a === void 0 ? void 0 : _a.size) !== null && _b !== void 0 ? _b : 14;
100
+ const baseFontWeight = (_c = theme === null || theme === void 0 ? void 0 : theme.font) === null || _c === void 0 ? void 0 : _c.weight;
101
+ const fieldFontSize = (_d = s.hintFontSize) !== null && _d !== void 0 ? _d : baseFontSize;
102
+ const fieldFontWeight = (_e = s.hintFontWeight) !== null && _e !== void 0 ? _e : baseFontWeight;
103
+ // Tenant custom font (item 28): the field text (and, since RN has no separate
104
+ // placeholder font, the placeholder) use the loaded family for the field weight.
105
+ // `undefined` => omit fontFamily (system font, today's look).
106
+ const fieldFontFamily = (0, themeFactory_1.resolveFontFamily)((_f = theme === null || theme === void 0 ? void 0 : theme.font) === null || _f === void 0 ? void 0 : _f.familyMap, fieldFontWeight);
107
+ const submit = () => {
108
+ if (disabledInput)
109
+ return;
110
+ const v = value.trim();
111
+ // Web parity (handleSend): if media is staged, send it WITH the optional caption
112
+ // as ONE message (onAttach(assets, text)) then clear both; otherwise text-only.
113
+ if (pending.length > 0) {
114
+ void (onAttach === null || onAttach === void 0 ? void 0 : onAttach(pending, v));
115
+ setPending([]);
116
+ setValue("");
117
+ return;
118
+ }
119
+ if (v) {
120
+ onSend(v);
121
+ setValue("");
122
+ }
123
+ };
124
+ // Web parity (handleSetFileValue): a pick STAGES its returned assets into the
125
+ // preview; nothing is sent until Send. `pick` returns the picked Asset[].
126
+ // Only ONE OS picker can be open at a time. A double-tap (or a row that fires
127
+ // onPress twice) would launch a second pick while the first is in flight —
128
+ // expo-document-picker throws "Different document picking in progress" on that.
129
+ // Serialize behind a ref: a re-entrant tap is a no-op until the first settles.
130
+ // NO re-entry guard: expo-document-picker already serializes (a concurrent call
131
+ // THROWS "Different document picking in progress", which we catch). A JS guard that
132
+ // waited for the promise to settle would PERMANENTLY lock the picker whenever a
133
+ // getDocumentAsync call hangs — so retries must always be allowed.
134
+ const stagePick = async (pick) => {
135
+ if (!pick)
136
+ return;
137
+ try {
138
+ const assets = await pick();
139
+ if (Array.isArray(assets) && assets.length > 0) {
140
+ setPending((prev) => [...prev, ...assets]);
141
+ }
142
+ }
143
+ catch {
144
+ /* user cancelled or the pick failed — nothing to stage */
145
+ }
146
+ };
147
+ // Document picker ONLY: present it AFTER the attach-menu <Modal> has dismissed.
148
+ // Presenting iOS UIDocumentPickerViewController while the RN Modal is tearing down
149
+ // makes iOS drop the presentation — the Files sheet never appears and the promise
150
+ // never resolves (the "hang"). Image/video pickers (PHPicker) tolerate this race,
151
+ // so only the document path defers. The beat outlasts the menu's dismissal so the
152
+ // picker presents from a settled view controller.
153
+ const requestPick = (pick) => {
154
+ setMediaMenuOpen(false);
155
+ setTimeout(() => void stagePick(pick), 300);
156
+ };
157
+ // Append the selected emoji to the current input value.
158
+ const onEmoji = (e) => setValue((v) => v + e);
159
+ // Selector rows: each capability is gated on its injected trigger / adapter.
160
+ // Picking image/document fires the host trigger (picker → attach); "record
161
+ // audio" arms the inline recorder (it mounts already-recording, see below).
162
+ const mediaOptions = [];
163
+ if (onPickImages) {
164
+ mediaOptions.push({
165
+ key: "image",
166
+ icon: "image",
167
+ label: t("upload_image"),
168
+ testID: "media-option-image",
169
+ onPress: () => void stagePick(onPickImages),
170
+ });
171
+ }
172
+ if (onPickVideos) {
173
+ mediaOptions.push({
174
+ key: "video",
175
+ icon: "videocam",
176
+ label: t("upload_video"),
177
+ testID: "media-option-video",
178
+ onPress: () => void stagePick(onPickVideos),
179
+ });
180
+ }
181
+ if (onPickDocuments) {
182
+ mediaOptions.push({
183
+ key: "document",
184
+ icon: "document",
185
+ label: t("upload_document"),
186
+ testID: "media-option-document",
187
+ onPress: () => requestPick(onPickDocuments),
188
+ });
189
+ }
190
+ // §6.2 refinement (back-compat): the menu exists only when a PICKER row exists;
191
+ // audio alone keeps the always-on inline recorder so existing tests stay green.
192
+ const hasMediaMenu = !!onPickImages || !!onPickVideos || !!onPickDocuments;
193
+ if (hasMediaMenu && audioAdapter) {
194
+ mediaOptions.push({
195
+ key: "audio",
196
+ icon: "mic",
197
+ label: t("record_audio"),
198
+ testID: "media-option-audio",
199
+ onPress: () => setRecordingArmed(true),
200
+ });
201
+ }
202
+ // Attach affordance: when the multi-option selector is wired the button opens
203
+ // the menu; otherwise fall back to the legacy single trigger.
204
+ const onAttachButton = () => {
205
+ if (disabledInput)
206
+ return;
207
+ if (hasMediaMenu)
208
+ setMediaMenuOpen(true);
209
+ else if (onAttachPress)
210
+ void onAttachPress();
211
+ };
212
+ const showAttach = hasMediaMenu || !!audioAdapter || !!onAttachPress;
213
+ // Voice note → route through attach() as a single media entry (web parity, #12).
214
+ const onRecorded = (result) => {
215
+ var _a;
216
+ setRecordingArmed(false);
217
+ // Web parity: a finished recording STAGES like a picked file and sends on the
218
+ // Send tap (with any caption) via the staged-attach path in submit().
219
+ const ext = ((_a = result.mimeType) !== null && _a !== void 0 ? _a : "").includes("ogg") ? "ogg" : "m4a";
220
+ const asset = {
221
+ uri: result.uri,
222
+ name: `voice-${Date.now()}.${ext}`,
223
+ mimeType: result.mimeType,
224
+ size: 0,
225
+ };
226
+ setPending((prev) => [...prev, asset]);
227
+ };
228
+ // Show the inline recorder when a menu is present ONLY after the user picks
229
+ // "record audio" (parity: audio is a selector option). With no menu (legacy
230
+ // audio-only Composer) keep the always-on recorder so existing flows/tests pass.
231
+ const showRecorder = !!audioAdapter && (!hasMediaMenu || recordingArmed);
232
+ return ((0, jsx_runtime_1.jsxs)(react_native_1.KeyboardAvoidingView, { behavior: react_native_1.Platform.OS === "ios" ? "padding" : undefined, children: [(0, jsx_runtime_1.jsx)(MediaUploadMenu_1.MediaUploadMenu, { visible: mediaMenuOpen && !disabledInput, onClose: () => setMediaMenuOpen(false), options: mediaOptions, dir: dir, theme: theme, language: language }), emojiEnabled && pickerOpen && !disabledInput ? ((0, jsx_runtime_1.jsx)(react_native_1.View, { testID: "emoji-picker", accessibilityRole: "menu", style: { flexDirection: d.rowDirection, flexWrap: "wrap", padding: 8 }, children: EMOJIS.map((e) => ((0, jsx_runtime_1.jsx)(react_native_1.Pressable, { testID: `emoji-${e}`, accessibilityRole: "button", accessibilityLabel: e, onPress: () => onEmoji(e), style: { padding: 6 }, children: (0, jsx_runtime_1.jsx)(react_native_1.Text, { style: { fontSize: theme.font.size + 6 }, children: e }) }, e))) })) : null, pending.length > 0 ? ((0, jsx_runtime_1.jsx)(react_native_1.View, { testID: "composer-pending", style: { flexDirection: d.rowDirection, flexWrap: "wrap", gap: 6, paddingHorizontal: 12, paddingTop: 8 }, children: pending.map((a, i) => {
233
+ var _a;
234
+ const isImage = ((_a = a.mimeType) !== null && _a !== void 0 ? _a : "").startsWith("image");
235
+ const previewUri = a.base64 ? `data:${a.mimeType};base64,${a.base64}` : a.uri;
236
+ return ((0, jsx_runtime_1.jsxs)(react_native_1.View, { style: { flexDirection: "row", alignItems: "center", gap: 4, backgroundColor: s.fieldBg, borderRadius: 6, padding: 4, paddingRight: 6 }, children: [isImage && previewUri ? ((0, jsx_runtime_1.jsx)(react_native_1.Image, { source: { uri: previewUri }, style: { width: 28, height: 28, borderRadius: 4 } })) : ((0, jsx_runtime_1.jsx)(react_native_1.Text, { style: { fontSize: 16 }, children: "\uD83D\uDCCE" })), (0, jsx_runtime_1.jsx)(react_native_1.Text, { numberOfLines: 1, style: { maxWidth: 120, color: s.fieldColor, fontSize: 12 }, children: a.name }), (0, jsx_runtime_1.jsx)(react_native_1.Pressable, { testID: `composer-pending-remove-${i}`, accessibilityRole: "button", accessibilityLabel: `Remove ${a.name}`, onPress: () => setPending((p) => p.filter((_, j) => j !== i)), style: { padding: 2 }, children: (0, jsx_runtime_1.jsx)(Icon_1.Icon, { name: "close", color: s.uploadIconColor, size: 14 }) })] }, `${a.name}-${i}`));
237
+ }) })) : null, (0, jsx_runtime_1.jsxs)(react_native_1.View, { style: {
238
+ flexDirection: d.rowDirection,
239
+ paddingVertical: 16,
240
+ paddingHorizontal: 12,
241
+ backgroundColor: (_h = (_g = theme === null || theme === void 0 ? void 0 : theme.colors) === null || _g === void 0 ? void 0 : _g.surface) !== null && _h !== void 0 ? _h : "#FCFCFD",
242
+ alignItems: "center",
243
+ gap: 4,
244
+ }, children: [showAttach ? ((0, jsx_runtime_1.jsx)(AttachButton_1.AttachButton, { onPick: onAttachButton, theme: theme, disabled: disabledInput, backgroundColor: s.uploadBtnBg, borderRadius: s.uploadBtnRadius, iconColor: s.uploadIconColor })) : null, showRecorder ? ((0, jsx_runtime_1.jsx)(AudioRecorder_1.AudioRecorder, { adapter: audioAdapter, onRecorded: onRecorded, onError: () => setRecordingArmed(false), disabled: disabledInput, autoStart: hasMediaMenu, theme: theme, iconColor: s.audioIconColor })) : null, (0, jsx_runtime_1.jsxs)(react_native_1.View, { style: {
245
+ flex: 1,
246
+ flexDirection: d.rowDirection,
247
+ alignItems: "center",
248
+ backgroundColor: s.fieldBg,
249
+ borderRadius: s.fieldRadius,
250
+ paddingHorizontal: 4,
251
+ opacity: disabledInput ? 0.4 : 1,
252
+ }, children: [(0, jsx_runtime_1.jsx)(react_native_1.TextInput, { testID: "composer-input", value: value, onChangeText: setValue, editable: !disabledInput, placeholder: placeholderText, placeholderTextColor: s.hintColor, accessibilityLabel: placeholderText, accessibilityState: { disabled: disabledInput }, style: {
253
+ flex: 1,
254
+ color: s.fieldColor,
255
+ // field/placeholder typography: defaultFontWeight + hintFont* (web parity).
256
+ fontSize: fieldFontSize,
257
+ fontWeight: fieldFontWeight,
258
+ fontFamily: fieldFontFamily,
259
+ textAlign: d.textAlign,
260
+ paddingVertical: 6,
261
+ paddingHorizontal: 8,
262
+ } }), emojiEnabled ? ((0, jsx_runtime_1.jsx)(react_native_1.Pressable, { testID: "composer-emoji", accessibilityRole: "button", accessibilityLabel: "Emoji", accessibilityState: { disabled: disabledInput }, disabled: disabledInput, onPress: () => setPickerOpen((o) => !o), style: { padding: 4, opacity: disabledInput ? 0.4 : 1 }, children: (0, jsx_runtime_1.jsx)(Icon_1.Icon, { name: "emoji", color: s.emojiIconColor, size: ((_k = (_j = theme === null || theme === void 0 ? void 0 : theme.font) === null || _j === void 0 ? void 0 : _j.size) !== null && _k !== void 0 ? _k : 14) + 6 }) })) : null] }), (0, jsx_runtime_1.jsx)(react_native_1.Pressable, { testID: "composer-send", accessibilityRole: "button", accessibilityLabel: t("send"), accessibilityState: { disabled: disabledInput }, disabled: disabledInput, onPress: submit, style: {
263
+ backgroundColor: s.sendBtnBg,
264
+ borderRadius: s.sendBtnRadius,
265
+ minWidth: 32,
266
+ height: 32,
267
+ paddingHorizontal: 8,
268
+ alignItems: "center",
269
+ justifyContent: "center",
270
+ opacity: disabledInput ? 0.4 : 1,
271
+ }, children: (0, jsx_runtime_1.jsx)(Icon_1.Icon, { name: "send", color: s.sendBtnColor, size: ((_m = (_l = theme === null || theme === void 0 ? void 0 : theme.font) === null || _l === void 0 ? void 0 : _l.size) !== null && _m !== void 0 ? _m : 14) + 4 }) })] })] }));
272
+ }
@@ -0,0 +1,9 @@
1
+ export interface FileTileProps {
2
+ /** hosted URL (or data-URL) for the attachment */
3
+ uri: string;
4
+ /** display name; falls back to the last URL path segment */
5
+ name?: string;
6
+ theme: any;
7
+ testID?: string;
8
+ }
9
+ export declare function FileTile({ uri, name, theme, testID }: FileTileProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,31 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.FileTile = FileTile;
4
+ const jsx_runtime_1 = require("react/jsx-runtime");
5
+ const react_native_1 = require("react-native");
6
+ const openLink_1 = require("./openLink");
7
+ function fileName(uri, name) {
8
+ if (name)
9
+ return name;
10
+ const tail = (uri || "").split(/[?#]/)[0].split("/").pop();
11
+ return tail || "file";
12
+ }
13
+ function FileTile({ uri, name, theme, testID }) {
14
+ var _a, _b, _c, _d;
15
+ const label = fileName(uri, name);
16
+ return ((0, jsx_runtime_1.jsxs)(react_native_1.Pressable, { testID: testID, accessibilityRole: "button", accessibilityLabel: `Open ${label}`, onPress: () => (0, openLink_1.openLink)(uri), style: {
17
+ flexDirection: "row",
18
+ alignItems: "center",
19
+ gap: 8,
20
+ padding: 8,
21
+ borderRadius: 6,
22
+ backgroundColor: (_a = theme === null || theme === void 0 ? void 0 : theme.colors) === null || _a === void 0 ? void 0 : _a.surface,
23
+ }, children: [(0, jsx_runtime_1.jsx)(react_native_1.View, { accessibilityElementsHidden: true, importantForAccessibility: "no-hide-descendants", style: {
24
+ width: 32,
25
+ height: 32,
26
+ borderRadius: 6,
27
+ alignItems: "center",
28
+ justifyContent: "center",
29
+ backgroundColor: (_b = theme === null || theme === void 0 ? void 0 : theme.colors) === null || _b === void 0 ? void 0 : _b.primary,
30
+ }, children: (0, jsx_runtime_1.jsx)(react_native_1.Text, { style: { color: "#fff", fontSize: 16 }, children: "📎" }) }), (0, jsx_runtime_1.jsx)(react_native_1.Text, { numberOfLines: 1, style: { color: (_c = theme === null || theme === void 0 ? void 0 : theme.colors) === null || _c === void 0 ? void 0 : _c.text, fontSize: (_d = theme === null || theme === void 0 ? void 0 : theme.font) === null || _d === void 0 ? void 0 : _d.size, flexShrink: 1 }, children: label })] }));
31
+ }
@@ -0,0 +1,52 @@
1
+ import { type Dir } from "../theme/dir";
2
+ /** A configurable image reference (web parity — `{ url, name }`). */
3
+ interface ConfigImage {
4
+ url?: string;
5
+ name?: string;
6
+ }
7
+ interface HeaderProps {
8
+ variant?: string;
9
+ dir?: Dir;
10
+ theme: any;
11
+ title?: string;
12
+ /** optional subtitle under the title (web `config.subTitle`). */
13
+ subtitle?: string;
14
+ /** resolved language — localizes the back affordance label (audit #8). */
15
+ language?: string;
16
+ /** Close the chat surface. Renders a ✕ button unless showClose is false. */
17
+ onClose?: () => void;
18
+ /** Gate the close button (config.showCloseButton !== false). Default shown. */
19
+ showClose?: boolean;
20
+ /**
21
+ * Start a video call. When provided, the header renders an in-header
22
+ * video-call button on the trailing edge (web parity) — the floating
23
+ * surface button is gone. Omitted -> no call button (video disabled).
24
+ */
25
+ onStartCall?: () => void;
26
+ /** Connection status — drives the online/offline status indicator. */
27
+ connected?: boolean;
28
+ /** Show the online/offline status indicator (web `config.statusIndicator`). */
29
+ statusIndicator?: boolean;
30
+ /**
31
+ * Connecting-banner text (web `config.connectingText`). When provided AND the
32
+ * surface is not yet connected, a banner renders under the title styled from
33
+ * the theme.header.connecting* tokens (web parity). Omitted -> no banner.
34
+ */
35
+ connectingText?: string;
36
+ /**
37
+ * Agent avatar image (web `config.avatar`). When `showAvatar` is true AND a url
38
+ * is present, the header renders this as an avatar <Image> (sized via the
39
+ * theme.header.avatar* tokens) with the online/offline status dot — replacing
40
+ * the DG logo placeholder. Absent url => fall back to the built-in logo/monogram.
41
+ */
42
+ avatar?: ConfigImage;
43
+ /** Gate the agent avatar (web `config.showAvatar`; default true). */
44
+ showAvatar?: boolean;
45
+ /**
46
+ * Custom header close-button image (web `config.closeImage`). When a url is
47
+ * present it replaces the built-in close ✕ Icon; otherwise the Icon is used.
48
+ */
49
+ closeImage?: ConfigImage;
50
+ }
51
+ export declare function Header({ variant, dir, theme, title, subtitle, language, onClose, showClose, onStartCall, connected, statusIndicator, connectingText, avatar, showAvatar, closeImage, }: HeaderProps): import("react/jsx-runtime").JSX.Element;
52
+ export {};